Skip to content

Task 3-2: Integration Test: Dynamic JMS Listener Lifecycle

Phase: 3 Priority: Medium Module: DynamicJmsListenerService Depends on: Không có Reference: docs/BountyHunter-ControlServer/details/feature-machine-management/SPEC.md

Background

DynamicJmsListenerService tạo và destroy JMS listeners per-machine khi machine connect/disconnect. containers field là public final Map — exposed trực tiếp, dễ bị accidental mutation. Không có tests nào verify lifecycle: listener được tạo đúng khi machine connect, bị stop khi disconnect, và không có orphaned containers. Nếu listeners không được cleanup đúng, ActiveMQ sẽ tích lũy subscriptions, gây memory leak và performance degradation.

Tasks

Note: Integration test JMS lifecycle cần embedded ActiveMQ broker. Spring Boot TestAutoConfiguration có thể start embedded ActiveMQ nếu spring-boot-starter-activemq trong test classpath và spring.activemq.broker-url=vm://localhost?broker.persistent=false trong test config. DynamicJmsListenerService.containerspublic final Map — trong tests, có thể read nó trực tiếp để verify state (tuy nhiên, task kèm theo nên encapsulate field này).

  • [ ] Setup embedded ActiveMQ trong test:
@SpringBootTest
@ActiveProfiles("test")
class DynamicJmsListenerLifecycleTest {

    @Autowired
    private DynamicJmsListenerService jmsListenerService;

    @Autowired
    private JmsTemplate jmsTemplate;

    // Helper: clear all containers between tests
    @AfterEach
    void stopAllListeners() {
        // Stop and clear any leftover containers from test
        new HashSet<>(jmsListenerService.containers.keySet())
            .forEach(key -> jmsListenerService.containers.get(key).stop());
        jmsListenerService.containers.clear();
    }
}
  • [ ] Test scenario 1: Machine connect → 2 listeners created (gameplay + health-check):
@Test
void whenMachineConnects_thenGameplayAndHealthCheckListenersCreated() {
    String macIp = "00:11:22:33:44:55";

    jmsListenerService.startListeningGameplay(macIp);
    jmsListenerService.startListeningHealthCheck(macIp);

    String gameplayQueue = buildGameplayQueueName(macIp);
    String healthCheckQueue = buildHealthCheckQueueName(macIp);

    assertThat(jmsListenerService.containers).containsKey(gameplayQueue);
    assertThat(jmsListenerService.containers).containsKey(healthCheckQueue);
    assertThat(jmsListenerService.containers.get(gameplayQueue).isRunning()).isTrue();
    assertThat(jmsListenerService.containers.get(healthCheckQueue).isRunning()).isTrue();
}
  • [ ] Test scenario 2: Machine disconnect → listeners stopped và removed:
@Test
void whenMachineDisconnects_thenListenersStoppedAndRemoved() {
    String macIp = "AA:BB:CC:DD:EE:FF";
    jmsListenerService.startListeningGameplay(macIp);
    jmsListenerService.startListeningHealthCheck(macIp);

    jmsListenerService.stopListeningGameplay(macIp);
    jmsListenerService.stopListeningHealthCheck(macIp);

    assertThat(jmsListenerService.containers).doesNotContainKey(buildGameplayQueueName(macIp));
    assertThat(jmsListenerService.containers).doesNotContainKey(buildHealthCheckQueueName(macIp));
}
  • [ ] Test scenario 3: Reconnect cùng MAC → listeners recreated, không duplicate:
@Test
void whenMachineReconnects_thenListenersRecreatedWithoutDuplicate() {
    String macIp = "11:22:33:44:55:66";

    // First connect
    jmsListenerService.startListeningGameplay(macIp);
    jmsListenerService.stopListeningGameplay(macIp);

    // Reconnect
    jmsListenerService.startListeningGameplay(macIp);

    String queueName = buildGameplayQueueName(macIp);
    assertThat(jmsListenerService.containers).containsKey(queueName);
    assertThat(jmsListenerService.containers.get(queueName).isRunning()).isTrue();
    // Only 1 container for this queue, not 2
    assertThat(jmsListenerService.containers.values().stream()
        .filter(c -> /* matches this mac */)
        .count()).isEqualTo(1);
}
  • [ ] Test scenario 4: Backend gửi gameplay command → JMS listener nhận và dispatch đến đúng ConnectedClient:
@Test
void whenBackendSendsGameplayCommand_thenCorrectMachineReceivesCommand() throws Exception {
    String macIp = "FF:EE:DD:CC:BB:AA";
    MockConnectedClient mockClient = new MockConnectedClient(macIp);
    WawaServer.allMachine.put(macIp, mockClient);

    jmsListenerService.startListeningGameplay(macIp);
    Thread.sleep(100); // allow listener to initialize

    // Backend sends command
    StartGameParam gameParam = new StartGameParam(/* ... */);
    jmsTemplate.convertAndSend(buildGameplayQueueName(macIp), gameParam);
    Thread.sleep(500); // allow async processing

    assertThat(mockClient.receivedCommands).contains(gameParam);
}
  • [ ] Test scenario 5: containers size = 2× connected machines:
@Test
void containersSizeEqualsTwiceConnectedMachines() {
    int machineCount = 5;
    for (int i = 0; i < machineCount; i++) {
        String mac = String.format("00:00:00:00:00:%02X", i);
        jmsListenerService.startListeningGameplay(mac);
        jmsListenerService.startListeningHealthCheck(mac);
    }

    assertThat(jmsListenerService.containers).hasSize(machineCount * 2);
}

Verification / Acceptance Criteria

  • [ ] containers.size() = 2× số machines đang kết nối (2 listeners per machine: gameplay + health-check)
  • [ ] Sau stopListening*(): containers removed (không phải chỉ stopped — đảm bảo không memory leak từ stopped containers tích lũy)
  • [ ] Reconnect cùng MAC: không tạo duplicate listeners (chỉ 1 active container per queue)
  • [ ] Command từ backend → ConnectedClient của đúng machine nhận được command (routing đúng)
  • [ ] Tất cả tests pass với embedded ActiveMQ (vm://localhost?broker.persistent=false)

Files to Modify

  • src/test/java/.../integration/DynamicJmsListenerLifecycleTest.java ← file mới tạo
  • src/test/resources/application-test.properties (thêm embedded ActiveMQ URL)
  • build.gradle hoặc pom.xml (verify spring-boot-starter-activemq có trong test scope)