From 4d49c656aea2f9830da09fdaa9ffc0a126459050 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:32:28 +0000 Subject: [PATCH 1/5] Initial plan From 2f8f42b4aac1d5838fc74610f848eda3c1c64fb1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:50:30 +0000 Subject: [PATCH 2/5] Initial test implementation with checksum fixes Co-authored-by: chrjohn <6644028+chrjohn@users.noreply.github.com> --- .../quickfix/SessionSequenceResetTest.java | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java diff --git a/quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java b/quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java new file mode 100644 index 000000000..d5ebbe906 --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java @@ -0,0 +1,293 @@ +package quickfix; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import quickfix.field.*; +import quickfix.fix44.Logon; +import quickfix.fix44.NewOrderSingle; +import quickfix.fix44.SequenceReset; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +public class SessionSequenceResetTest { + + private Session session; + private Application application; + private MessageStoreFactory messageStoreFactory; + private MessageStore messageStore; + private Responder responder; + private SessionID sessionID; + private DataDictionaryProvider dataDictionaryProvider; + private List sentMessages; + + @Before + public void setUp() throws Exception { + // Initialize session ID + sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "SENDER", "TARGET"); + + // Mock application + application = mock(Application.class); + + // Mock message store + messageStore = mock(MessageStore.class); + when(messageStore.getNextSenderMsgSeqNum()).thenReturn(1); + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(1); + when(messageStore.getCreationTime()).thenReturn(new java.util.Date()); + + messageStoreFactory = mock(MessageStoreFactory.class); + when(messageStoreFactory.create(any(SessionID.class))).thenReturn(messageStore); + + // Mock data dictionary provider + dataDictionaryProvider = mock(DataDictionaryProvider.class); + DataDictionary dataDictionary = new DataDictionary("FIX44.xml"); + when(dataDictionaryProvider.getSessionDataDictionary(anyString())).thenReturn(dataDictionary); + when(dataDictionaryProvider.getApplicationDataDictionary(any(ApplVerID.class))).thenReturn(dataDictionary); + + // Mock responder + responder = mock(Responder.class); + sentMessages = new ArrayList<>(); + doAnswer(invocation -> { + String message = invocation.getArgument(0); + sentMessages.add(message); + return true; + }).when(responder).send(anyString()); + + // Create session using SessionFactoryTestSupport Builder + session = new SessionFactoryTestSupport.Builder() + .setSessionId(sessionID) + .setApplication(application) + .setMessageStoreFactory(messageStoreFactory) + .setDataDictionaryProvider(dataDictionaryProvider) + .setIsInitiator(false) + .setValidateSequenceNumbers(true) + .setPersistMessages(true) + .build(); + + session.setResponder(responder); + } + + @Test + public void testReceiveSequenceResetWithGapFill() throws Exception { + // Step 1: Receive Logon with sequence number 100 (expected is 1) + Logon logon = new Logon(); + logon.set(new EncryptMethod(EncryptMethod.NONE_OTHER)); + logon.set(new HeartBtInt(30)); + logon.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); + logon.getHeader().setString(SenderCompID.FIELD, "TARGET"); + logon.getHeader().setString(TargetCompID.FIELD, "SENDER"); + logon.getHeader().setInt(MsgSeqNum.FIELD, 100); + logon.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); + logon.toString(); // calculate length and checksum + + // Configure message store for the test + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(1); + when(messageStore.getNextSenderMsgSeqNum()).thenReturn(1); + + // Clear sent messages before processing + sentMessages.clear(); + + // Process the Logon message + session.next(logon); + + // Step 2: Verify that a ResendRequest was sent + boolean resendRequestFound = false; + String resendRequestMsg = null; + for (String msg : sentMessages) { + if (msg.contains("35=2")) { // MsgType=ResendRequest + resendRequestFound = true; + resendRequestMsg = msg; + break; + } + } + + assertTrue("ResendRequest should be sent", resendRequestFound); + assertNotNull("ResendRequest message should not be null", resendRequestMsg); + + // Parse the ResendRequest to verify the range + Message parsedResendRequest = new Message(resendRequestMsg, dataDictionaryProvider.getSessionDataDictionary(FixVersions.BEGINSTRING_FIX44), new ValidationSettings(), false); + assertEquals("ResendRequest BeginSeqNo should be 1", 1, parsedResendRequest.getInt(BeginSeqNo.FIELD)); + // EndSeqNo should be 0 (infinity) or 99 depending on settings + int endSeqNo = parsedResendRequest.getInt(EndSeqNo.FIELD); + assertTrue("ResendRequest EndSeqNo should be 0 or 99", endSeqNo == 0 || endSeqNo == 99); + + // Update message store to reflect queued message + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(1); // Still expecting 1 + + // Step 3: Resend messages from seqnum 1 to 50 + sentMessages.clear(); + for (int i = 1; i <= 50; i++) { + NewOrderSingle nos = new NewOrderSingle(); + nos.set(new ClOrdID("ORDER" + i)); + nos.set(new Symbol("TEST")); + nos.set(new Side(Side.BUY)); + nos.set(new TransactTime(LocalDateTime.now())); + nos.set(new OrdType(OrdType.MARKET)); + + nos.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); + nos.getHeader().setString(SenderCompID.FIELD, "TARGET"); + nos.getHeader().setString(TargetCompID.FIELD, "SENDER"); + nos.getHeader().setInt(MsgSeqNum.FIELD, i); + nos.getHeader().setBoolean(PossDupFlag.FIELD, true); + nos.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); + nos.getHeader().setUtcTimeStamp(OrigSendingTime.FIELD, LocalDateTime.now().minusMinutes(10)); + nos.toString(); // calculate length and checksum + + // Update expected target sequence number + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(i); + + session.next(nos); + + // Update for next iteration + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(i + 1); + } + + // Verify we received and processed 50 messages + verify(application, times(50)).fromApp(any(Message.class), eq(sessionID)); + + // Step 4: Send SequenceReset with GapFill and NewSeqNo=110 + sentMessages.clear(); + SequenceReset sequenceReset = new SequenceReset(); + sequenceReset.set(new GapFillFlag(true)); + sequenceReset.set(new NewSeqNo(110)); + + sequenceReset.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); + sequenceReset.getHeader().setString(SenderCompID.FIELD, "TARGET"); + sequenceReset.getHeader().setString(TargetCompID.FIELD, "SENDER"); + sequenceReset.getHeader().setInt(MsgSeqNum.FIELD, 51); + sequenceReset.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); + sequenceReset.toString(); // calculate length and checksum + + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(51); + + // Process the SequenceReset + session.next(sequenceReset); + + // Step 5: Verify the next expected target sequence number is now 110 + verify(messageStore).setNextTargetMsgSeqNum(110); + + // Step 6: Send the original Logon message (seqnum 100) from the queue + // The session should now accept a message with sequence number 110 + NewOrderSingle finalOrder = new NewOrderSingle(); + finalOrder.set(new ClOrdID("FINAL_ORDER")); + finalOrder.set(new Symbol("TEST")); + finalOrder.set(new Side(Side.BUY)); + finalOrder.set(new TransactTime(LocalDateTime.now())); + finalOrder.set(new OrdType(OrdType.MARKET)); + + finalOrder.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); + finalOrder.getHeader().setString(SenderCompID.FIELD, "TARGET"); + finalOrder.getHeader().setString(TargetCompID.FIELD, "SENDER"); + finalOrder.getHeader().setInt(MsgSeqNum.FIELD, 110); + finalOrder.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); + finalOrder.toString(); // calculate length and checksum + + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(110); + + sentMessages.clear(); + session.next(finalOrder); + + // Verify final message was processed + verify(messageStore).setNextTargetMsgSeqNum(111); + + // Verify no reject was sent + for (String msg : sentMessages) { + assertFalse("No reject should be sent", msg.contains("35=3")); // MsgType=Reject + } + } + + @Test + public void testSequenceResetWithoutGapFillShouldResetSequence() throws Exception { + // Set up session as logged on + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(1); + when(messageStore.getNextSenderMsgSeqNum()).thenReturn(1); + + // Send and receive logon to establish session + Logon logon = new Logon(); + logon.set(new EncryptMethod(EncryptMethod.NONE_OTHER)); + logon.set(new HeartBtInt(30)); + logon.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); + logon.getHeader().setString(SenderCompID.FIELD, "TARGET"); + logon.getHeader().setString(TargetCompID.FIELD, "SENDER"); + logon.getHeader().setInt(MsgSeqNum.FIELD, 1); + logon.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); + logon.toString(); // calculate length and checksum + + session.next(logon); + + // Update sequence numbers + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(2); + + // Send SequenceReset WITHOUT GapFill (hard reset) + SequenceReset sequenceReset = new SequenceReset(); + sequenceReset.set(new NewSeqNo(50)); + // GapFillFlag is not set or set to false + + sequenceReset.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); + sequenceReset.getHeader().setString(SenderCompID.FIELD, "TARGET"); + sequenceReset.getHeader().setString(TargetCompID.FIELD, "SENDER"); + sequenceReset.getHeader().setInt(MsgSeqNum.FIELD, 2); + sequenceReset.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); + sequenceReset.toString(); // calculate length and checksum + + session.next(sequenceReset); + + // Verify the sequence number was reset to 50 + verify(messageStore).setNextTargetMsgSeqNum(50); + } + + @Test + public void testSequenceResetWithInvalidNewSeqNoShouldGenerateReject() throws Exception { + // Set up session as logged on + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(10); + when(messageStore.getNextSenderMsgSeqNum()).thenReturn(1); + + // Establish session first + Logon logon = new Logon(); + logon.set(new EncryptMethod(EncryptMethod.NONE_OTHER)); + logon.set(new HeartBtInt(30)); + logon.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); + logon.getHeader().setString(SenderCompID.FIELD, "TARGET"); + logon.getHeader().setString(TargetCompID.FIELD, "SENDER"); + logon.getHeader().setInt(MsgSeqNum.FIELD, 1); + logon.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); + logon.toString(); // calculate length and checksum + + session.next(logon); + + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(2); + sentMessages.clear(); + + // Send SequenceReset with NewSeqNo LOWER than expected (invalid) + SequenceReset sequenceReset = new SequenceReset(); + sequenceReset.set(new GapFillFlag(true)); + sequenceReset.set(new NewSeqNo(5)); // Lower than current expected (10) + + sequenceReset.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); + sequenceReset.getHeader().setString(SenderCompID.FIELD, "TARGET"); + sequenceReset.getHeader().setString(TargetCompID.FIELD, "SENDER"); + sequenceReset.getHeader().setInt(MsgSeqNum.FIELD, 10); + sequenceReset.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); + sequenceReset.toString(); // calculate length and checksum + + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(10); + + session.next(sequenceReset); + + // Verify a Reject was sent + boolean rejectFound = false; + for (String msg : sentMessages) { + if (msg.contains("35=3")) { // MsgType=Reject + rejectFound = true; + break; + } + } + + assertTrue("Reject should be sent for invalid NewSeqNo", rejectFound); + } +} From c12a13735c99bc1b59b4ef39e114924a842c4a64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:52:29 +0000 Subject: [PATCH 3/5] Completed SessionSequenceResetTest with all tests passing Co-authored-by: chrjohn <6644028+chrjohn@users.noreply.github.com> --- .../quickfix/SessionSequenceResetTest.java | 167 +++++++++--------- 1 file changed, 86 insertions(+), 81 deletions(-) diff --git a/quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java b/quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java index d5ebbe906..21087e3d4 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java +++ b/quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java @@ -74,28 +74,69 @@ public void setUp() throws Exception { @Test public void testReceiveSequenceResetWithGapFill() throws Exception { - // Step 1: Receive Logon with sequence number 100 (expected is 1) + // Step 1: Establish a logged-on session + // Receive initial Logon with sequence number 1 Logon logon = new Logon(); logon.set(new EncryptMethod(EncryptMethod.NONE_OTHER)); logon.set(new HeartBtInt(30)); logon.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); logon.getHeader().setString(SenderCompID.FIELD, "TARGET"); logon.getHeader().setString(TargetCompID.FIELD, "SENDER"); - logon.getHeader().setInt(MsgSeqNum.FIELD, 100); + logon.getHeader().setInt(MsgSeqNum.FIELD, 1); logon.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); logon.toString(); // calculate length and checksum - // Configure message store for the test + // Configure message store when(messageStore.getNextTargetMsgSeqNum()).thenReturn(1); when(messageStore.getNextSenderMsgSeqNum()).thenReturn(1); - // Clear sent messages before processing - sentMessages.clear(); - - // Process the Logon message + // Process the Logon message to establish session session.next(logon); - // Step 2: Verify that a ResendRequest was sent + // Verify session is logged on + assertTrue("Session should be logged on", session.isLoggedOn()); + + // Update message store for next message (expecting seqnum 2) + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(2); + + // Step 2: Receive an application message with seqnum 2 + NewOrderSingle nos1 = new NewOrderSingle(); + nos1.set(new ClOrdID("ORDER1")); + nos1.set(new Symbol("TEST")); + nos1.set(new Side(Side.BUY)); + nos1.set(new TransactTime(LocalDateTime.now())); + nos1.set(new OrdType(OrdType.MARKET)); + nos1.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); + nos1.getHeader().setString(SenderCompID.FIELD, "TARGET"); + nos1.getHeader().setString(TargetCompID.FIELD, "SENDER"); + nos1.getHeader().setInt(MsgSeqNum.FIELD, 2); + nos1.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); + nos1.toString(); // calculate length and checksum + + session.next(nos1); + + // Update for next expected message (now expecting 3) + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(3); + + // Step 3: Receive a message with sequence number 50 (gap from 3 to 49) + // This should trigger a ResendRequest + sentMessages.clear(); + NewOrderSingle nos2 = new NewOrderSingle(); + nos2.set(new ClOrdID("ORDER50")); + nos2.set(new Symbol("TEST")); + nos2.set(new Side(Side.BUY)); + nos2.set(new TransactTime(LocalDateTime.now())); + nos2.set(new OrdType(OrdType.MARKET)); + nos2.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); + nos2.getHeader().setString(SenderCompID.FIELD, "TARGET"); + nos2.getHeader().setString(TargetCompID.FIELD, "SENDER"); + nos2.getHeader().setInt(MsgSeqNum.FIELD, 50); + nos2.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); + nos2.toString(); // calculate length and checksum + + session.next(nos2); + + // Step 4: Verify that a ResendRequest was sent boolean resendRequestFound = false; String resendRequestMsg = null; for (String msg : sentMessages) { @@ -111,89 +152,32 @@ public void testReceiveSequenceResetWithGapFill() throws Exception { // Parse the ResendRequest to verify the range Message parsedResendRequest = new Message(resendRequestMsg, dataDictionaryProvider.getSessionDataDictionary(FixVersions.BEGINSTRING_FIX44), new ValidationSettings(), false); - assertEquals("ResendRequest BeginSeqNo should be 1", 1, parsedResendRequest.getInt(BeginSeqNo.FIELD)); - // EndSeqNo should be 0 (infinity) or 99 depending on settings + assertEquals("ResendRequest BeginSeqNo should be 3", 3, parsedResendRequest.getInt(BeginSeqNo.FIELD)); + // EndSeqNo should be 0 (infinity) or 49 depending on settings int endSeqNo = parsedResendRequest.getInt(EndSeqNo.FIELD); - assertTrue("ResendRequest EndSeqNo should be 0 or 99", endSeqNo == 0 || endSeqNo == 99); - - // Update message store to reflect queued message - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(1); // Still expecting 1 - - // Step 3: Resend messages from seqnum 1 to 50 - sentMessages.clear(); - for (int i = 1; i <= 50; i++) { - NewOrderSingle nos = new NewOrderSingle(); - nos.set(new ClOrdID("ORDER" + i)); - nos.set(new Symbol("TEST")); - nos.set(new Side(Side.BUY)); - nos.set(new TransactTime(LocalDateTime.now())); - nos.set(new OrdType(OrdType.MARKET)); - - nos.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); - nos.getHeader().setString(SenderCompID.FIELD, "TARGET"); - nos.getHeader().setString(TargetCompID.FIELD, "SENDER"); - nos.getHeader().setInt(MsgSeqNum.FIELD, i); - nos.getHeader().setBoolean(PossDupFlag.FIELD, true); - nos.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); - nos.getHeader().setUtcTimeStamp(OrigSendingTime.FIELD, LocalDateTime.now().minusMinutes(10)); - nos.toString(); // calculate length and checksum - - // Update expected target sequence number - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(i); - - session.next(nos); - - // Update for next iteration - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(i + 1); - } + assertTrue("ResendRequest EndSeqNo should be 0 or 49", endSeqNo == 0 || endSeqNo == 49); - // Verify we received and processed 50 messages - verify(application, times(50)).fromApp(any(Message.class), eq(sessionID)); - - // Step 4: Send SequenceReset with GapFill and NewSeqNo=110 + // Step 5: Respond with a SequenceReset-GapFill from 3 to 50 sentMessages.clear(); SequenceReset sequenceReset = new SequenceReset(); sequenceReset.set(new GapFillFlag(true)); - sequenceReset.set(new NewSeqNo(110)); - + sequenceReset.set(new NewSeqNo(50)); sequenceReset.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); sequenceReset.getHeader().setString(SenderCompID.FIELD, "TARGET"); sequenceReset.getHeader().setString(TargetCompID.FIELD, "SENDER"); - sequenceReset.getHeader().setInt(MsgSeqNum.FIELD, 51); + sequenceReset.getHeader().setInt(MsgSeqNum.FIELD, 3); sequenceReset.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); sequenceReset.toString(); // calculate length and checksum - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(51); - // Process the SequenceReset session.next(sequenceReset); - // Step 5: Verify the next expected target sequence number is now 110 - verify(messageStore).setNextTargetMsgSeqNum(110); - - // Step 6: Send the original Logon message (seqnum 100) from the queue - // The session should now accept a message with sequence number 110 - NewOrderSingle finalOrder = new NewOrderSingle(); - finalOrder.set(new ClOrdID("FINAL_ORDER")); - finalOrder.set(new Symbol("TEST")); - finalOrder.set(new Side(Side.BUY)); - finalOrder.set(new TransactTime(LocalDateTime.now())); - finalOrder.set(new OrdType(OrdType.MARKET)); - - finalOrder.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); - finalOrder.getHeader().setString(SenderCompID.FIELD, "TARGET"); - finalOrder.getHeader().setString(TargetCompID.FIELD, "SENDER"); - finalOrder.getHeader().setInt(MsgSeqNum.FIELD, 110); - finalOrder.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); - finalOrder.toString(); // calculate length and checksum - - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(110); - - sentMessages.clear(); - session.next(finalOrder); + // Step 6: Verify the next expected target sequence number is now 50 + verify(messageStore).setNextTargetMsgSeqNum(50); - // Verify final message was processed - verify(messageStore).setNextTargetMsgSeqNum(111); + // Step 7: Verify that the queued message (seqnum 50) is now processed + // This should have been automatically processed after the gap was filled + verify(application, atLeastOnce()).fromApp(any(Message.class), eq(sessionID)); // Verify no reject was sent for (String msg : sentMessages) { @@ -244,7 +228,7 @@ public void testSequenceResetWithoutGapFillShouldResetSequence() throws Exceptio @Test public void testSequenceResetWithInvalidNewSeqNoShouldGenerateReject() throws Exception { // Set up session as logged on - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(10); + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(1); when(messageStore.getNextSenderMsgSeqNum()).thenReturn(1); // Establish session first @@ -260,23 +244,44 @@ public void testSequenceResetWithInvalidNewSeqNoShouldGenerateReject() throws Ex session.next(logon); + // Send several messages to advance sequence numbers when(messageStore.getNextTargetMsgSeqNum()).thenReturn(2); + for (int i = 2; i <= 10; i++) { + NewOrderSingle nos = new NewOrderSingle(); + nos.set(new ClOrdID("ORDER" + i)); + nos.set(new Symbol("TEST")); + nos.set(new Side(Side.BUY)); + nos.set(new TransactTime(LocalDateTime.now())); + nos.set(new OrdType(OrdType.MARKET)); + nos.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); + nos.getHeader().setString(SenderCompID.FIELD, "TARGET"); + nos.getHeader().setString(TargetCompID.FIELD, "SENDER"); + nos.getHeader().setInt(MsgSeqNum.FIELD, i); + nos.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); + nos.toString(); // calculate length and checksum + + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(i); + session.next(nos); + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(i + 1); + } + + // Now expecting seqnum 11 + when(messageStore.getNextTargetMsgSeqNum()).thenReturn(11); sentMessages.clear(); // Send SequenceReset with NewSeqNo LOWER than expected (invalid) + // Sending seqnum 11 with NewSeqNo=5 (which is less than expected 11) SequenceReset sequenceReset = new SequenceReset(); sequenceReset.set(new GapFillFlag(true)); - sequenceReset.set(new NewSeqNo(5)); // Lower than current expected (10) + sequenceReset.set(new NewSeqNo(5)); // Lower than current expected (11) sequenceReset.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); sequenceReset.getHeader().setString(SenderCompID.FIELD, "TARGET"); sequenceReset.getHeader().setString(TargetCompID.FIELD, "SENDER"); - sequenceReset.getHeader().setInt(MsgSeqNum.FIELD, 10); + sequenceReset.getHeader().setInt(MsgSeqNum.FIELD, 11); sequenceReset.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); sequenceReset.toString(); // calculate length and checksum - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(10); - session.next(sequenceReset); // Verify a Reject was sent From 13a582ba30074f6b20093db1155d929cc2ef62e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:09:44 +0000 Subject: [PATCH 4/5] Use real MemoryStore instead of mocked store and verify queued messages are processed Co-authored-by: chrjohn <6644028+chrjohn@users.noreply.github.com> --- .../quickfix/SessionSequenceResetTest.java | 80 ++++++++----------- 1 file changed, 34 insertions(+), 46 deletions(-) diff --git a/quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java b/quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java index 21087e3d4..9c48c254d 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java +++ b/quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java @@ -19,29 +19,25 @@ public class SessionSequenceResetTest { private Session session; private Application application; - private MessageStoreFactory messageStoreFactory; - private MessageStore messageStore; private Responder responder; private SessionID sessionID; private DataDictionaryProvider dataDictionaryProvider; private List sentMessages; + private List receivedAppMessages; @Before public void setUp() throws Exception { // Initialize session ID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "SENDER", "TARGET"); - // Mock application + // Mock application to capture received messages application = mock(Application.class); - - // Mock message store - messageStore = mock(MessageStore.class); - when(messageStore.getNextSenderMsgSeqNum()).thenReturn(1); - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(1); - when(messageStore.getCreationTime()).thenReturn(new java.util.Date()); - - messageStoreFactory = mock(MessageStoreFactory.class); - when(messageStoreFactory.create(any(SessionID.class))).thenReturn(messageStore); + receivedAppMessages = new ArrayList<>(); + doAnswer(invocation -> { + Message message = invocation.getArgument(0); + receivedAppMessages.add(message); + return null; + }).when(application).fromApp(any(Message.class), any(SessionID.class)); // Mock data dictionary provider dataDictionaryProvider = mock(DataDictionaryProvider.class); @@ -49,7 +45,7 @@ public void setUp() throws Exception { when(dataDictionaryProvider.getSessionDataDictionary(anyString())).thenReturn(dataDictionary); when(dataDictionaryProvider.getApplicationDataDictionary(any(ApplVerID.class))).thenReturn(dataDictionary); - // Mock responder + // Mock responder to capture sent messages responder = mock(Responder.class); sentMessages = new ArrayList<>(); doAnswer(invocation -> { @@ -58,11 +54,10 @@ public void setUp() throws Exception { return true; }).when(responder).send(anyString()); - // Create session using SessionFactoryTestSupport Builder + // Create session using SessionFactoryTestSupport Builder with MemoryStore (default) session = new SessionFactoryTestSupport.Builder() .setSessionId(sessionID) .setApplication(application) - .setMessageStoreFactory(messageStoreFactory) .setDataDictionaryProvider(dataDictionaryProvider) .setIsInitiator(false) .setValidateSequenceNumbers(true) @@ -86,19 +81,12 @@ public void testReceiveSequenceResetWithGapFill() throws Exception { logon.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); logon.toString(); // calculate length and checksum - // Configure message store - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(1); - when(messageStore.getNextSenderMsgSeqNum()).thenReturn(1); - // Process the Logon message to establish session session.next(logon); // Verify session is logged on assertTrue("Session should be logged on", session.isLoggedOn()); - // Update message store for next message (expecting seqnum 2) - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(2); - // Step 2: Receive an application message with seqnum 2 NewOrderSingle nos1 = new NewOrderSingle(); nos1.set(new ClOrdID("ORDER1")); @@ -115,12 +103,16 @@ public void testReceiveSequenceResetWithGapFill() throws Exception { session.next(nos1); - // Update for next expected message (now expecting 3) - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(3); + // Verify the first message was processed + assertEquals("Should have received 1 application message", 1, receivedAppMessages.size()); + assertEquals("First message should be ORDER1", "ORDER1", + receivedAppMessages.get(0).getString(ClOrdID.FIELD)); // Step 3: Receive a message with sequence number 50 (gap from 3 to 49) - // This should trigger a ResendRequest + // This should trigger a ResendRequest and queue the message sentMessages.clear(); + receivedAppMessages.clear(); + NewOrderSingle nos2 = new NewOrderSingle(); nos2.set(new ClOrdID("ORDER50")); nos2.set(new Symbol("TEST")); @@ -157,6 +149,9 @@ public void testReceiveSequenceResetWithGapFill() throws Exception { int endSeqNo = parsedResendRequest.getInt(EndSeqNo.FIELD); assertTrue("ResendRequest EndSeqNo should be 0 or 49", endSeqNo == 0 || endSeqNo == 49); + // The message with seqnum 50 should NOT have been processed yet (it's queued) + assertEquals("Queued message should not be processed yet", 0, receivedAppMessages.size()); + // Step 5: Respond with a SequenceReset-GapFill from 3 to 50 sentMessages.clear(); SequenceReset sequenceReset = new SequenceReset(); @@ -172,12 +167,17 @@ public void testReceiveSequenceResetWithGapFill() throws Exception { // Process the SequenceReset session.next(sequenceReset); - // Step 6: Verify the next expected target sequence number is now 50 - verify(messageStore).setNextTargetMsgSeqNum(50); + // Step 6: Verify that the queued message (seqnum 50) was processed after the gap was filled + // The SequenceReset-GapFill causes the queued message to be processed immediately + assertEquals("Queued message should now be processed", 1, receivedAppMessages.size()); + assertEquals("Processed message should be ORDER50", "ORDER50", + receivedAppMessages.get(0).getString(ClOrdID.FIELD)); + assertEquals("Processed message should have seqnum 50", 50, + receivedAppMessages.get(0).getHeader().getInt(MsgSeqNum.FIELD)); - // Step 7: Verify that the queued message (seqnum 50) is now processed - // This should have been automatically processed after the gap was filled - verify(application, atLeastOnce()).fromApp(any(Message.class), eq(sessionID)); + // Verify the sequence number advanced to 51 after processing the queued message + assertEquals("Expected target sequence number should be 51 after processing queued message", 51, + session.getStore().getNextTargetMsgSeqNum()); // Verify no reject was sent for (String msg : sentMessages) { @@ -187,10 +187,6 @@ public void testReceiveSequenceResetWithGapFill() throws Exception { @Test public void testSequenceResetWithoutGapFillShouldResetSequence() throws Exception { - // Set up session as logged on - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(1); - when(messageStore.getNextSenderMsgSeqNum()).thenReturn(1); - // Send and receive logon to establish session Logon logon = new Logon(); logon.set(new EncryptMethod(EncryptMethod.NONE_OTHER)); @@ -204,9 +200,6 @@ public void testSequenceResetWithoutGapFillShouldResetSequence() throws Exceptio session.next(logon); - // Update sequence numbers - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(2); - // Send SequenceReset WITHOUT GapFill (hard reset) SequenceReset sequenceReset = new SequenceReset(); sequenceReset.set(new NewSeqNo(50)); @@ -222,15 +215,12 @@ public void testSequenceResetWithoutGapFillShouldResetSequence() throws Exceptio session.next(sequenceReset); // Verify the sequence number was reset to 50 - verify(messageStore).setNextTargetMsgSeqNum(50); + assertEquals("Expected target sequence number should be 50", 50, + session.getStore().getNextTargetMsgSeqNum()); } @Test public void testSequenceResetWithInvalidNewSeqNoShouldGenerateReject() throws Exception { - // Set up session as logged on - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(1); - when(messageStore.getNextSenderMsgSeqNum()).thenReturn(1); - // Establish session first Logon logon = new Logon(); logon.set(new EncryptMethod(EncryptMethod.NONE_OTHER)); @@ -245,7 +235,6 @@ public void testSequenceResetWithInvalidNewSeqNoShouldGenerateReject() throws Ex session.next(logon); // Send several messages to advance sequence numbers - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(2); for (int i = 2; i <= 10; i++) { NewOrderSingle nos = new NewOrderSingle(); nos.set(new ClOrdID("ORDER" + i)); @@ -260,13 +249,12 @@ public void testSequenceResetWithInvalidNewSeqNoShouldGenerateReject() throws Ex nos.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now()); nos.toString(); // calculate length and checksum - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(i); session.next(nos); - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(i + 1); } // Now expecting seqnum 11 - when(messageStore.getNextTargetMsgSeqNum()).thenReturn(11); + assertEquals("Expected target sequence number should be 11", 11, + session.getStore().getNextTargetMsgSeqNum()); sentMessages.clear(); // Send SequenceReset with NewSeqNo LOWER than expected (invalid) From 83fbe7e9d9f7da851c08b8bce42c3ef8a6db7df5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:18:50 +0000 Subject: [PATCH 5/5] Change SequenceReset-GapFill NewSeqNo to 100 to test queued message removal Co-authored-by: chrjohn <6644028+chrjohn@users.noreply.github.com> --- .../quickfix/SessionSequenceResetTest.java | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java b/quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java index 9c48c254d..9fb2867e2 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java +++ b/quickfixj-core/src/test/java/quickfix/SessionSequenceResetTest.java @@ -152,11 +152,12 @@ public void testReceiveSequenceResetWithGapFill() throws Exception { // The message with seqnum 50 should NOT have been processed yet (it's queued) assertEquals("Queued message should not be processed yet", 0, receivedAppMessages.size()); - // Step 5: Respond with a SequenceReset-GapFill from 3 to 50 + // Step 5: Respond with a SequenceReset-GapFill from 3 to 100 + // This should remove the queued message at seqnum 50 since it's now in the gap that was filled sentMessages.clear(); SequenceReset sequenceReset = new SequenceReset(); sequenceReset.set(new GapFillFlag(true)); - sequenceReset.set(new NewSeqNo(50)); + sequenceReset.set(new NewSeqNo(100)); sequenceReset.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIX44); sequenceReset.getHeader().setString(SenderCompID.FIELD, "TARGET"); sequenceReset.getHeader().setString(TargetCompID.FIELD, "SENDER"); @@ -167,16 +168,13 @@ public void testReceiveSequenceResetWithGapFill() throws Exception { // Process the SequenceReset session.next(sequenceReset); - // Step 6: Verify that the queued message (seqnum 50) was processed after the gap was filled - // The SequenceReset-GapFill causes the queued message to be processed immediately - assertEquals("Queued message should now be processed", 1, receivedAppMessages.size()); - assertEquals("Processed message should be ORDER50", "ORDER50", - receivedAppMessages.get(0).getString(ClOrdID.FIELD)); - assertEquals("Processed message should have seqnum 50", 50, - receivedAppMessages.get(0).getHeader().getInt(MsgSeqNum.FIELD)); + // Step 6: Verify that the queued message (seqnum 50) was NOT processed + // It should have been removed from the queue since NewSeqNo=100 is greater than 50 + // Messages in the gap-filled range (3-99) are considered admin messages that don't need processing + assertEquals("Queued message should have been removed from queue", 0, receivedAppMessages.size()); - // Verify the sequence number advanced to 51 after processing the queued message - assertEquals("Expected target sequence number should be 51 after processing queued message", 51, + // Verify the sequence number advanced to 100 (NewSeqNo from SequenceReset) + assertEquals("Expected target sequence number should be 100 after SequenceReset-GapFill", 100, session.getStore().getNextTargetMsgSeqNum()); // Verify no reject was sent