Task 3-1: Integration Test: Redis State Sync
Phase: 3 Priority: Medium Module:
RedisMachineRepository,ConnectedClientDepends on: task-1-1 Reference: docs/BountyHunter-ControlServer/details/feature-machine-management/SPEC.md
Background
RedisMachineRepository là single point of truth cho machine status trong ControlServer. Sau fix task-1-1 (externalized TTL), cần integration test để verify (1) TTL đúng với config, không còn hardcoded 11s, (2) status được update đúng sau mỗi machine event, và (3) JedisPool không bị connection leak dưới concurrent load. Dùng embedded Redis (Testcontainers hoặc embedded-redis library) để test mà không cần Redis server thật.
Tasks
Note:
Testcontainers(testcontainers-redis) là lựa chọn tốt nhất vì dùng Docker container Redis thật. Nếu Docker không available trong CI, dùngit.ozimov:embedded-redis(nhẹ hơn). Verify TTL bằngjedis.ttl(key)ngay sausave()— TTL có thể nhỏ hơn config value 1-2 giây do execution time, dùngassertThat(ttl).isBetween(configTtl - 2, configTtl). JedisPool leak: verifyjedisPool.getNumActive()trở về 0 sau test.
- [ ] Setup embedded Redis trong test:
@SpringBootTest
@ActiveProfiles("test")
class RedisStateSyncIntegrationTest {
// Option A: Testcontainers
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
// Option B: embedded-redis (không cần Docker)
// @BeforeAll static void startRedis() { embeddedRedis = new RedisServer(16379); embeddedRedis.start(); }
@Autowired
private RedisMachineRepository repository;
@Autowired
private JedisPool jedisPool;
@BeforeEach
void clearRedis() {
try (Jedis jedis = jedisPool.getResource()) {
jedis.flushDb();
}
}
}
- [ ] Test scenario 1: Machine connect → Redis key created với đúng status:
@Test
void whenMachineConnects_thenRedisKeyCreatedWithStatus0() {
String macIp = "00:11:22:33:44:55";
repository.save(0, macIp);
try (Jedis jedis = jedisPool.getResource()) {
String key = "RCraneMachineStatus:" + macIp;
assertThat(jedis.exists(key)).isTrue();
assertThat(jedis.hget(key, "status")).isEqualTo("0");
}
}
- [ ] Test scenario 2: Status update → Redis updated đúng:
@Test
void whenMachineStatusChanges_thenRedisUpdatedWithNewStatus() {
String macIp = "AA:BB:CC:DD:EE:FF";
repository.save(0, macIp); // initial: status=0
repository.save(1, macIp); // update: status=1
try (Jedis jedis = jedisPool.getResource()) {
assertThat(jedis.hget("RCraneMachineStatus:" + macIp, "status")).isEqualTo("1");
}
}
- [ ] Test scenario 3: TTL đúng với config (validates task-1-1 fix):
@Test
void whenSave_thenRedisTtlMatchesConfig() {
int configuredTtl = 30; // value from test config: redis.machine.ttl-seconds=30
String macIp = "11:22:33:44:55:66";
repository.save(0, macIp);
try (Jedis jedis = jedisPool.getResource()) {
long ttl = jedis.ttl("RCraneMachineStatus:" + macIp);
// TTL should be within 2 seconds of configured value
assertThat(ttl).isBetween((long) configuredTtl - 2, (long) configuredTtl);
}
}
- [ ] Test scenario 4: Concurrent
save()calls → no JedisPool connection leak:
@Test
void whenConcurrentSaves_thenNoConnectionLeak() throws InterruptedException {
int threadCount = 20;
CountDownLatch latch = new CountDownLatch(threadCount);
ExecutorService pool = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
final String mac = String.format("00:00:00:00:00:%02X", i);
pool.submit(() -> {
for (int j = 0; j < 50; j++) {
repository.save(j % 2, mac);
}
latch.countDown();
});
}
latch.await(10, TimeUnit.SECONDS);
pool.shutdown();
// All connections returned to pool
assertThat(jedisPool.getNumActive()).isEqualTo(0);
}
- [ ] Test scenario 5: Redis key expire tự nhiên sau TTL:
@Test
void whenKeyExpires_thenGetStatusReturnsNull() throws InterruptedException {
// Use short TTL for test: redis.machine.ttl-seconds=2 (test profile)
repository.save(1, "FF:EE:DD:CC:BB:AA");
Thread.sleep(3000); // wait for expiry
assertThat(repository.getStatus("FF:EE:DD:CC:BB:AA")).isNull();
}
- [ ] Tạo
application-test.propertiesvới Redis test config:
redis.machine.ttl-seconds=30
# Point to embedded Redis port
spring.redis.host=localhost
spring.redis.port=${embedded.redis.port}
Verification / Acceptance Criteria
- [ ]
jedis.hget("RCraneMachineStatus:{mac}", "status")trả đúng value sau mỗisave()call - [ ]
jedis.ttl("RCraneMachineStatus:{mac}")≤redis.machine.ttl-secondsconfig value (không phải hardcoded 11) - [ ] 1000 concurrent
save()calls →jedisPool.getNumActive() == 0sau test (no leak) - [ ] Key tự expire sau TTL →
getStatus()trả null - [ ] Tất cả tests pass với embedded Redis (không cần external Redis server)
Files to Modify
src/test/java/.../integration/RedisStateSyncIntegrationTest.java← file mới tạosrc/test/resources/application-test.propertiesbuild.gradlehoặcpom.xml(thêm Testcontainers hoặc embedded-redis test dependency)