Task 3-2: Integration Test: Dynamic JMS Listener Lifecycle
Phase: 3 Priority: Medium Module:
DynamicJmsListenerServiceDepends 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-activemqtrong test classpath vàspring.activemq.broker-url=vm://localhost?broker.persistent=falsetrong test config.DynamicJmsListenerService.containerslàpublic 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:
containerssize = 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 →
ConnectedClientcủ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ạosrc/test/resources/application-test.properties(thêm embedded ActiveMQ URL)build.gradlehoặcpom.xml(verifyspring-boot-starter-activemqcó trong test scope)