Skip to content

Task 3-1: Integration Test: Connection Lifecycle

Phase: 3 Priority: Medium Module: WawaServer, ConnectedClient, DynamicJmsListenerService Depends on: task-1-1, task-1-2 Reference: docs/BountyHunter-ControlServer/details/feature-iot-bridge/SPEC.md

Background

Connection lifecycle bao gồm nhiều component tương tác: WawaServer accept loop, ConnectedClient handshake/message handling, DynamicJmsListenerService listener creation/destruction, và RedisMachineRepository status tracking. Không có integration test nào hiện tại — chỉ có manual testing với máy thật. Cần automated test để verify (1) fix task-1-1 hoạt động đúng, (2) fix task-1-2 không gây corruption, và (3) concurrent connections không race condition.

Tasks

Note: Integration test này cần start actual ServerSocket trên port ngẫu nhiên (tránh port conflict với production server 9999). Dùng @SpringBootTest với test-specific application-test.properties để override port. Mock adminRMCCraneMachineClient, DynamicJmsListenerService, và Redis bằng @MockBean hoặc embedded alternatives (Testcontainers Redis). Test thread safety bằng CountDownLatchExecutorService.

  • [ ] Setup test infrastructure:
@SpringBootTest
@ActiveProfiles("test")
class ConnectionLifecycleIntegrationTest {

    @MockBean
    private AdminRMCCraneMachineClient adminClient;

    @MockBean
    private DynamicJmsListenerService jmsListenerService;

    @Autowired
    private WawaServer wawaServer;

    // Helper: tạo mock TCP client và gửi handshake
    private Socket connectMockMachine(String macAddress) throws IOException {
        Socket socket = new Socket("localhost", testPort);
        // Gửi AddNewIp packet với macAddress
        byte[] handshakePacket = buildAddNewIpPacket(macAddress);
        socket.getOutputStream().write(handshakePacket);
        return socket;
    }
}
  • [ ] Test scenario 1: Single connect → allMachine updated, JMS listeners created:
@Test
void whenMachineConnects_thenRegisteredInAllMachineAndJmsListenersCreated() throws Exception {
    Socket mockMachine = connectMockMachine("00:11:22:33:44:55");
    Thread.sleep(100); // allow async processing

    assertThat(WawaServer.allMachine).containsKey("00:11:22:33:44:55");
    verify(jmsListenerService).startListeningGameplay("00:11:22:33:44:55");
    verify(jmsListenerService).startListeningHealthCheck("00:11:22:33:44:55");

    mockMachine.close();
}
  • [ ] Test scenario 2: Disconnect → allMachine cleared, JMS listeners stopped:
@Test
void whenMachineDisconnects_thenRemovedFromAllMachineAndJmsListenersStopped() throws Exception {
    Socket mockMachine = connectMockMachine("AA:BB:CC:DD:EE:FF");
    Thread.sleep(100);
    mockMachine.close(); // trigger disconnect
    Thread.sleep(200);

    assertThat(WawaServer.allMachine).doesNotContainKey("AA:BB:CC:DD:EE:FF");
    verify(jmsListenerService).stopListeningGameplay("AA:BB:CC:DD:EE:FF");
    verify(jmsListenerService).stopListeningHealthCheck("AA:BB:CC:DD:EE:FF");
}
  • [ ] Test scenario 3: handleSocketClose với reason → machineSocketClose được gọi (validates task-1-1 fix):
@Test
void whenSocketCloseWithReason_thenBackendNotified() throws Exception {
    Socket mockMachine = connectMockMachine("11:22:33:44:55:66");
    Thread.sleep(100);

    // Gửi SocketClose packet với reason code
    byte[] closePacket = buildSocketClosePacket(SocketCloseReason.POWER_FAILURE);
    mockMachine.getOutputStream().write(closePacket);
    Thread.sleep(200);

    verify(adminClient).machineSocketClose("11:22:33:44:55:66");
}
  • [ ] Test scenario 4: Concurrent 10 machines connect simultaneously → no race condition:
@Test
void whenMultipleMachinesConnectConcurrently_thenAllRegistered() throws Exception {
    int machineCount = 10;
    CountDownLatch allConnected = new CountDownLatch(machineCount);
    ExecutorService pool = Executors.newFixedThreadPool(machineCount);

    for (int i = 0; i < machineCount; i++) {
        final String mac = String.format("00:00:00:00:00:%02X", i);
        pool.submit(() -> {
            connectMockMachine(mac);
            allConnected.countDown();
        });
    }

    allConnected.await(5, TimeUnit.SECONDS);
    Thread.sleep(300);

    assertThat(WawaServer.allMachine).hasSize(machineCount);
}
  • [ ] Test scenario 5: gPackageId không bị corrupt dưới concurrent load (validates task-1-2 fix):
@Test
void whenConcurrentCommandsSent_thenPackageIdsAreUnique() throws Exception {
    // Connect 1 machine, send 100 concurrent startGame + moveLeft commands
    // Collect all packet IDs from captured outbound data
    // Assert: all IDs are unique
}

Verification / Acceptance Criteria

  • [ ] allMachine.size() đúng sau mỗi connect (tăng 1) và disconnect (giảm 1)
  • [ ] Không có orphaned JMS listeners sau disconnect (verify via mock verification)
  • [ ] machineSocketClose() được gọi khi socket close với reason (task-1-1 fix verified)
  • [ ] 10 concurrent connections → allMachine.size() == 10, không có ConcurrentModificationException
  • [ ] Tất cả tests pass trong CI (không flaky do timing — dùng Awaitility nếu cần)

Files to Modify

  • src/test/java/.../integration/ConnectionLifecycleIntegrationTest.java ← file mới tạo
  • src/test/resources/application-test.properties ← override server.port to random