package com.ruoyi.config; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.ruoyi.device.model.DeviceWorkStatus; import com.ruoyi.device.simulator.BtsSimulatorStateStore.ChannelState; import com.ruoyi.device.simulator.BtsSimulatorStateStore.ChannelTombstone; import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorState; import com.ruoyi.device.simulator.BtsSimulatorStateStore.DeviceState; import com.ruoyi.device.simulator.BtsSimulatorStateStore.SimulatorState; import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore.CabinetCandidate; import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore.ChannelCandidate; import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore.CollectorCandidate; import com.ruoyi.device.model.ChannelIdentity; import com.ruoyi.framework.config.FastJson2JsonRedisSerializer; class RedisStateStoreTest { @Test void initializesOnlyWhenStateIsAbsentAndRestoresSavedState() { AtomicReference stored = new AtomicReference<>(); RedisBtsSimulatorStateStore store = store(stored); SimulatorState defaults = state(1, "127.0.0.2"); assertEquals(defaults, store.loadOrInitialize(defaults)); SimulatorState changed = state(7, "127.0.0.9"); store.save(changed); assertEquals(changed, store.loadOrInitialize(defaults)); } @Test void restoresNestedSimulatorStateFromRedisSerializer() { SimulatorState expected = new SimulatorState(1, List.of(new DeviceState(2, "127.0.0.3", false, List.of(new ChannelState(1, true, DeviceWorkStatus.WORKING)))), List.of(new CollectorState(3, "dev-sim-collector-3", "127.0.1.3", "02:43:00:00:00:03", "模拟采集器", "开发模拟器", "Virtual Collector", 3, true)), 3, 4, List.of(new ChannelTombstone(1, "127.0.0.2", 4, 2))); FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer<>(Object.class); AtomicReference stored = new AtomicReference<>( serializer.deserialize(serializer.serialize(JSON.toJSONString(expected)))); SimulatorState restored = store(stored).loadOrInitialize(state(1, "127.0.0.2")); assertEquals(expected, restored); assertEquals(DeviceWorkStatus.WORKING, restored.devices().get(0).channels().get(0).workStatus()); } @Test void upgradesLegacyCollectorDiscoveryStateWithSamplingDefaults() { AtomicReference stored = new AtomicReference<>("{\"version\":1,\"devices\":[]," + "\"collectors\":[{\"collectorId\":1,\"deviceCode\":\"dev-sim-collector-1\"," + "\"ip\":\"127.0.1.2\",\"mac\":\"02:43:00:00:00:01\"," + "\"name\":\"模拟采集器\",\"manufacturer\":\"开发模拟器\"," + "\"model\":\"Virtual Collector\",\"channelCount\":2,\"online\":true}]," + "\"nextDeviceId\":1,\"nextCollectorId\":2,\"tombstones\":[]}"); SimulatorState restored = store(stored).loadOrInitialize(state(1, "127.0.0.2")); CollectorState collector = restored.collectors().get(0); assertEquals(1000, collector.sampleIntervalMs()); assertEquals(1L, collector.nextSequence()); assertEquals(List.of("S-01", "S-02"), collector.channels().stream().map(channel -> channel.channelCode()).toList()); assertEquals(List.of(), collector.history()); } @Test void rejectsIncompatibleVersionWithoutOverwritingIt() { AtomicReference stored = new AtomicReference<>("{\"version\":2,\"devices\":[]," + "\"collectors\":[],\"nextDeviceId\":1,\"nextCollectorId\":1,\"tombstones\":[]}"); RedisBtsSimulatorStateStore store = store(stored); IllegalStateException exception = assertThrows(IllegalStateException.class, () -> store.loadOrInitialize(state(1, "127.0.0.2"))); assertEquals("Unsupported BTS simulator state version: 2", exception.getMessage()); assertEquals(true, stored.get().toString().contains("\"version\":2")); } @Test void restoresNestedDiscoveryCandidatesFromJson() { AtomicReference stored = new AtomicReference<>(); RedisDeviceDiscoveryCandidateStore first = new RedisDeviceDiscoveryCandidateStore(redis(stored)); java.time.Instant observedAt = java.time.Instant.parse("2026-08-27T00:00:00Z"); CabinetCandidate candidate = new CabinetCandidate("dev-sim", null, "dev-sim-22-1", 22, 1, "127.0.0.2", null, "模拟柜", "开发模拟器", "BTS Virtual Cabinet", List.of(new ChannelCandidate(new ChannelIdentity("dev-sim", 22, 1, 1, 1), true, DeviceWorkStatus.WORKING, observedAt)), observedAt, observedAt.plusSeconds(30)); first.recordCabinets(List.of(candidate)); RedisDeviceDiscoveryCandidateStore restored = new RedisDeviceDiscoveryCandidateStore(redis(stored)); CabinetCandidate actual = restored.snapshot(observedAt.plusSeconds(1)).cabinets().get(0); assertEquals("dev-sim-22-1", actual.deviceCode()); assertEquals(DeviceWorkStatus.WORKING, actual.channels().get(0).workStatus()); assertEquals(observedAt, actual.channels().get(0).observedAt()); } @Test void removesDevelopmentCandidatesFromRedisState() { AtomicReference stored = new AtomicReference<>(); RedisDeviceDiscoveryCandidateStore store = new RedisDeviceDiscoveryCandidateStore(redis(stored)); java.time.Instant observedAt = java.time.Instant.parse("2026-08-27T00:00:00Z"); store.recordCabinets(List.of(new CabinetCandidate("dev-sim", null, "dev-sim-22-1", 22, 1, "127.0.0.2", null, "模拟柜", null, null, List.of(new ChannelCandidate(new ChannelIdentity("dev-sim", 22, 1, 1, 1), true, DeviceWorkStatus.STOPPED, observedAt)), observedAt, observedAt.plusSeconds(30)))); store.removeAdapter("dev-sim"); assertEquals(List.of(), store.snapshot(observedAt).cabinets()); } @Test void restoresDiscoveryCandidatesFromLegacyAutoTypeJson() { AtomicReference stored = new AtomicReference<>(); RedisDeviceDiscoveryCandidateStore first = new RedisDeviceDiscoveryCandidateStore(redis(stored)); java.time.Instant observedAt = java.time.Instant.parse("2026-08-27T00:00:00Z"); first.recordCabinets(List.of(new CabinetCandidate("dev-sim", null, "dev-sim-22-1", 22, 1, "127.0.0.2", null, "模拟柜", null, null, List.of(new ChannelCandidate(new ChannelIdentity("dev-sim", 22, 1, 1, 1), true, DeviceWorkStatus.STOPPED, observedAt)), observedAt, observedAt.plusSeconds(30)))); first.replaceCollectors("dev-sim", List.of(new CollectorCandidate("dev-sim", "dev-sim-collector-1", "127.0.1.2", null, "模拟采集器", null, null, 8, true, observedAt, observedAt.plusSeconds(30)))); JSONObject legacy = JSON.parseObject(stored.get().toString()); legacy.put("@type", "com.ruoyi.config.RedisDeviceDiscoveryCandidateStore$StoredState"); JSONObject cabinet = legacy.getJSONArray("cabinets").getJSONObject(0); cabinet.put("@type", JSONObject.class.getName()); cabinet.getJSONArray("channels").getJSONObject(0) .put("@type", JSONObject.class.getName()); legacy.getJSONArray("collectors").getJSONObject(0) .put("@type", JSONObject.class.getName()); FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer<>(Object.class); stored.set(serializer.deserialize(serializer.serialize(legacy))); var snapshot = new RedisDeviceDiscoveryCandidateStore(redis(stored)).snapshot(observedAt.plusSeconds(1)); CabinetCandidate restored = snapshot.cabinets().get(0); assertEquals("dev-sim-22-1", restored.deviceCode()); assertEquals(DeviceWorkStatus.STOPPED, restored.channels().get(0).workStatus()); assertEquals("dev-sim-collector-1", snapshot.collectors().get(0).deviceCode()); } @SuppressWarnings({ "unchecked", "rawtypes" }) private RedisBtsSimulatorStateStore store(AtomicReference stored) { return new RedisBtsSimulatorStateStore(redis(stored)); } @SuppressWarnings({ "unchecked", "rawtypes" }) private RedisTemplate redis(AtomicReference stored) { RedisTemplate redis = mock(RedisTemplate.class); ValueOperations values = mock(ValueOperations.class); when(redis.opsForValue()).thenReturn(values); when(values.get(any())).thenAnswer(invocation -> stored.get()); when(values.setIfAbsent(any(), any())).thenAnswer(invocation -> { if (stored.get() != null) return false; stored.set(invocation.getArgument(1)); return true; }); doAnswer(invocation -> { stored.set(invocation.getArgument(1)); return null; }) .when(values).set(any(), any()); return redis; } private SimulatorState state(int deviceId, String ip) { return new SimulatorState(1, List.of(new DeviceState(deviceId, ip, true, List.of(new ChannelState(1, true, DeviceWorkStatus.STOPPED)))), List.of(), deviceId + 1, 1, List.of()); } }