package com.ruoyi.web.controller.device; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.lang.reflect.Method; import java.math.BigDecimal; import java.net.InetAddress; import java.time.Instant; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.server.ResponseStatusException; import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.device.bts.BtsDevelopmentSimulator; import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore.CabinetCandidate; import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore.ChannelCandidate; import com.ruoyi.device.discovery.InMemoryDeviceDiscoveryCandidateStore; import com.ruoyi.device.model.ChannelIdentity; import com.ruoyi.device.model.DeviceWorkStatus; import com.ruoyi.web.config.BtsDevelopmentSimulatorWebConfig; class BtsDevelopmentSimulatorControllerTest { @Test void controllerUpdatesDeviceAndChannelState() throws Exception { try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0)) { BtsDevelopmentSimulatorController controller = controller(simulator); controller.updateDevice(1, new BtsDevelopmentSimulatorController.DeviceStateCommand("127.0.0.2", 4, false)); controller.updateChannel(1, 3, new BtsDevelopmentSimulatorController.ChannelStateCommand(true, DeviceWorkStatus.PAUSED)); assertFalse(controller.state().devices().get(0).online()); assertEquals(DeviceWorkStatus.PAUSED, controller.state().devices().get(0).channels().get(2).workStatus()); } } @Test void controllerAddsUpdatesAndDeletesVirtualDevices() throws Exception { try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0)) { BtsDevelopmentSimulatorController controller = controller(simulator); var created = controller.createDevice( new BtsDevelopmentSimulatorController.CreateDeviceCommand("127.0.0.8", 6)); int deviceId = created.devices().stream() .filter(device -> device.ip().equals("127.0.0.8")) .findFirst().orElseThrow().deviceId(); controller.updateDevice(deviceId, new BtsDevelopmentSimulatorController.DeviceStateCommand("127.0.0.9", 3, false)); assertEquals(2, controller.state().devices().size()); assertEquals(3, controller.state().devices().stream() .filter(device -> device.deviceId() == deviceId) .findFirst().orElseThrow().channels().size()); controller.deleteDevice(deviceId); assertEquals(1, controller.state().devices().size()); } } @Test void controllerCreatesNewareDeviceFromExplicitModelProfile() throws Exception { try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0)) { BtsDevelopmentSimulatorController controller = controller(simulator); var created = controller.createDevice(new BtsDevelopmentSimulatorController.CreateDeviceCommand( "127.0.0.8", 0, "CT-4008T-5V6A-S1-F")); var device = created.devices().stream().filter(item -> item.ip().equals("127.0.0.8")) .findFirst().orElseThrow(); assertEquals(8, device.channels().size()); assertEquals("新威", device.profile().manufacturer()); assertEquals("CT-4008T-5V6A-S1-F", device.profile().model()); assertEquals(100, device.profile().sampleIntervalMs()); } } @Test void controllerConfiguresCollectorsSeparatelyFromCabinets() throws Exception { try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0)) { BtsDevelopmentSimulatorController controller = controller(simulator); var created = controller.createCollector( new BtsDevelopmentSimulatorController.CreateCollectorCommand("127.0.1.8", 6)); int collectorId = created.collectors().stream() .filter(collector -> collector.ip().equals("127.0.1.8")) .findFirst().orElseThrow().collectorId(); controller.updateCollector(collectorId, new BtsDevelopmentSimulatorController.CollectorStateCommand("127.0.1.9", 3, false)); assertEquals(1, controller.state().devices().size()); assertEquals(2, controller.state().collectors().size()); assertFalse(controller.state().collectors().stream() .filter(collector -> collector.collectorId() == collectorId) .findFirst().orElseThrow().online()); controller.deleteCollector(collectorId); assertEquals(1, controller.state().collectors().size()); assertEquals(1, controller.state().devices().size()); } } @Test void controllerExposesCollectorRealtimeAndPagedHistory() throws Exception { try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0)) { BtsDevelopmentSimulatorController controller = controller(simulator); controller.configureCollectorSampling(1, new BtsDevelopmentSimulatorController.CollectorSamplingCommand( 100, List.of(), null, null, false)); controller.configureCollectorChannel(1, "S-01", new BtsDevelopmentSimulatorController.CollectorChannelCommand(List.of( new BtsDevelopmentSimulatorController.CollectorMetricCommand( "temperature", new BigDecimal("27.2"), "℃"), new BtsDevelopmentSimulatorController.CollectorMetricCommand( "custom_voltage", new BigDecimal("1.234"), "V")))); simulator.advanceCollectors(Instant.parse("2026-08-29T01:00:00Z")); var realtime = controller.collectorRealtime(1); var firstPage = controller.collectorHistory(1, 0, 2); assertTrue(realtime.online()); assertTrue(realtime.measurements().stream().anyMatch(item -> item.metricCode().equals("custom_voltage"))); assertEquals(2, firstPage.records().size()); assertTrue(firstPage.hasMore()); assertEquals(2, firstPage.nextOffset()); } } @Test void controllerPowersAllSimulatorDevicesWithoutResettingHistory() throws Exception { try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0)) { BtsDevelopmentSimulatorController controller = controller(simulator); simulator.advanceCollectors(Instant.now()); long historyCount = controller.state().collectors().get(0).historyRecordCount(); var poweredOff = controller.setPower( new BtsDevelopmentSimulatorController.SimulatorPowerCommand(false)); assertTrue(poweredOff.devices().stream().noneMatch(device -> device.online() || device.channels().stream().anyMatch(channel -> channel.online()))); assertTrue(poweredOff.collectors().stream().noneMatch(collector -> collector.online())); assertEquals(historyCount, poweredOff.collectors().get(0).historyRecordCount()); var poweredOn = controller.setPower( new BtsDevelopmentSimulatorController.SimulatorPowerCommand(true)); assertTrue(poweredOn.devices().stream().allMatch(device -> device.online() && device.channels().stream().allMatch(channel -> channel.online() && channel.workStatus() == DeviceWorkStatus.STOPPED))); assertTrue(poweredOn.collectors().stream().allMatch(collector -> collector.online())); } } @Test void simulatorPowerCommandRequiresExplicitState() throws Exception { try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0)) { BtsDevelopmentSimulatorController controller = controller(simulator); ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> controller.setPower(new BtsDevelopmentSimulatorController.SimulatorPowerCommand(null))); assertEquals(400, exception.getStatusCode().value()); assertTrue(controller.state().collectors().get(0).online()); } } @Test void virtualIpMustBeUniqueAcrossCabinetsAndCollectors() throws Exception { try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0)) { BtsDevelopmentSimulatorController controller = controller(simulator); assertThrows(org.springframework.web.server.ResponseStatusException.class, () -> controller.createCollector( new BtsDevelopmentSimulatorController.CreateCollectorCommand("127.0.0.2", 4))); assertThrows(org.springframework.web.server.ResponseStatusException.class, () -> controller.createDevice( new BtsDevelopmentSimulatorController.CreateDeviceCommand("127.0.1.2", 4))); } } @Test void developmentControlBoundaryOnlyAllowsLoopbackClients() throws Exception { var interceptor = new BtsDevelopmentSimulatorWebConfig.LocalOnlyInterceptor(); MockHttpServletResponse localResponse = new MockHttpServletResponse(); MockHttpServletRequest localRequest = requestFrom(InetAddress.getLoopbackAddress().getHostAddress()); MockHttpServletResponse remoteResponse = new MockHttpServletResponse(); MockHttpServletRequest remoteRequest = requestFrom("192.168.1.20"); assertTrue(interceptor.preHandle(localRequest, localResponse, new Object())); assertFalse(interceptor.preHandle(remoteRequest, remoteResponse, new Object())); assertEquals(403, remoteResponse.getStatus()); assertTrue(BtsDevelopmentSimulatorController.class.isAnnotationPresent(Anonymous.class)); } @Test void exposesStaticControlPageThroughAnonymousHandler() throws Exception { Method page = BtsDevelopmentSimulatorController.class.getMethod("page"); assertArrayEquals(new String[] { "/dev/bts-simulator.html" }, page.getAnnotation(GetMapping.class).value()); assertTrue(BtsDevelopmentSimulatorController.class.isAnnotationPresent(Anonymous.class)); } @Test void resettingSimulatorRemovesExistingDevelopmentCandidates() throws Exception { Instant now = Instant.parse("2026-08-27T00:00:00Z"); InMemoryDeviceDiscoveryCandidateStore candidateStore = new InMemoryDeviceDiscoveryCandidateStore(); candidateStore.recordCabinets(List.of(new CabinetCandidate("dev-sim", null, "dev-sim-22-8", 22, 8, "127.0.0.8", null, "旧模拟柜", null, null, List.of(new ChannelCandidate(new ChannelIdentity("dev-sim", 22, 8, 1, 1), true, DeviceWorkStatus.STOPPED, now)), now, now.plusSeconds(30)))); try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0)) { BtsDevelopmentSimulatorController controller = new BtsDevelopmentSimulatorController(simulator, candidateStore); controller.reset(); assertEquals(List.of(), candidateStore.snapshot(now).cabinets()); } } private BtsDevelopmentSimulatorController controller(BtsDevelopmentSimulator simulator) { return new BtsDevelopmentSimulatorController(simulator, new InMemoryDeviceDiscoveryCandidateStore()); } private MockHttpServletRequest requestFrom(String remoteAddress) { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/dev/bts-simulator/state"); request.setRemoteAddr(remoteAddress); return request; } }