package com.ruoyi.system.service.impl; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.InputStream; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.device.api.DeviceAdapter; import com.ruoyi.device.api.DeviceAdapterException; import com.ruoyi.device.discovery.InMemoryDeviceDiscoveryCandidateStore; import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore; import com.ruoyi.device.model.ChannelIdentity; import com.ruoyi.device.model.DetectedChannel; import com.ruoyi.device.model.DetectedDeviceMetadata; import com.ruoyi.device.model.DeviceWorkStatus; import com.ruoyi.device.model.RealtimeSnapshot; import com.ruoyi.device.registry.DeviceAdapterRegistry; import com.ruoyi.system.domain.TDetectionCabinet; import com.ruoyi.system.domain.TDetectionChannel; import com.ruoyi.system.domain.TDeviceAdapter; import com.ruoyi.system.mapper.TDeviceAdapterMapper; import com.ruoyi.system.service.DeviceAdapterErrorCodes; @ExtendWith(MockitoExtension.class) class DeviceAdapterServiceImplTest { @Mock private TDeviceAdapterMapper adapterMapper; @Mock private DeviceAdapterRegistry registry; @Mock private DeviceAdapter deviceAdapter; private DeviceAdapterServiceImpl service; private TDeviceAdapter adapter; private InMemoryDeviceDiscoveryCandidateStore candidateStore; @BeforeEach void setUp() { candidateStore = new InMemoryDeviceDiscoveryCandidateStore(); service = new DeviceAdapterServiceImpl(adapterMapper, registry, new DeviceDiscoveryPersistenceService(candidateStore), candidateStore); adapter = new TDeviceAdapter(); adapter.setAdapterId(7L); adapter.setAdapterCode("lab-bts"); adapter.setAdapterName("一号 BTS"); adapter.setAdapterType("BTS"); adapter.setHost("127.0.0.1"); adapter.setPort(502); adapter.setPollIntervalMs(5000); adapter.setTimezoneId("Asia/Hong_Kong"); adapter.setReadOnly(true); adapter.setCapabilities("{\"discovery\":true,\"realtimeObservation\":true,\"historyDownload\":true,\"control\":false}"); adapter.setStatus("0"); } @Test void enablingAdapterSynchronizesRuntimeWithoutConstructingVendorClient() { adapter.setStatus("1"); when(adapterMapper.selectDeviceAdapterById(7L)).thenReturn(adapter); when(adapterMapper.updateDeviceAdapterStatus(7L, "0", "admin")).thenReturn(1); int rows = service.changeStatus(7L, true, "admin"); assertEquals(1, rows); verify(registry).synchronize(any()); } @Test void springSelectsProductionConstructor() { try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { context.registerBean(TDeviceAdapterMapper.class, () -> org.mockito.Mockito.mock(TDeviceAdapterMapper.class)); context.registerBean(DeviceAdapterRegistry.class, () -> org.mockito.Mockito.mock(DeviceAdapterRegistry.class)); context.registerBean(DeviceDiscoveryPersistenceService.class, () -> org.mockito.Mockito.mock(DeviceDiscoveryPersistenceService.class)); context.registerBean(DeviceDiscoveryCandidateStore.class, () -> org.mockito.Mockito.mock(DeviceDiscoveryCandidateStore.class)); context.register(DeviceAdapterServiceImpl.class); context.refresh(); assertNotNull(context.getBean(DeviceAdapterServiceImpl.class)); } } @Test void repeatedDiscoveryUpsertsStableChannelIdentityWithoutDeletingMissingChannels() { when(adapterMapper.selectDeviceAdapterById(7L)).thenReturn(adapter); when(deviceAdapter.discoverChannels()).thenReturn(List.of( new DetectedChannel(new ChannelIdentity("lab-bts", 22, 1, 2, 3), "192.168.1.35", true), new DetectedChannel(new ChannelIdentity("lab-bts", 22, 1, 2, 4), "192.168.1.35", false))); stubRegistryExecution(); assertEquals(2, service.discover(7L).channelCount()); assertEquals(2, service.discover(7L).channelCount()); var snapshot = candidateStore.snapshot(Instant.now()); assertEquals(1, snapshot.cabinets().size()); assertEquals(2, snapshot.cabinets().get(0).channels().size()); verify(adapterMapper, times(2)).updateDiscoverySuccess(eq(7L), any()); } @Test void discoveryPersistsOptionalDeviceMetadataFromAdapterBoundary() { ChannelIdentity identity = new ChannelIdentity("lab-bts", 22, 8, 1, 1); when(adapterMapper.selectDeviceAdapterById(7L)).thenReturn(adapter); when(deviceAdapter.discoverChannels()).thenReturn(List.of(new DetectedChannel(identity, "127.0.0.8", true, new DetectedDeviceMetadata("开发设备 8", "开发模拟器", "BTS Virtual Cabinet", "02:44:00:16:00:08")))); stubRegistryExecution(); service.discover(7L); var cabinet = candidateStore.snapshot(Instant.now()).cabinets().get(0); assertEquals("开发设备 8", cabinet.name()); assertEquals("开发模拟器", cabinet.manufacturer()); assertEquals("BTS Virtual Cabinet", cabinet.model()); assertEquals("02:44:00:16:00:08", cabinet.mac()); } @Test void discoveryPersistsRealtimeWorkStatusForAvailableChannels() { ChannelIdentity identity = new ChannelIdentity("lab-bts", 22, 1, 2, 3); when(adapterMapper.selectDeviceAdapterById(7L)).thenReturn(adapter); when(deviceAdapter.discoverChannels()).thenReturn(List.of( new DetectedChannel(identity, "192.168.1.35", true))); when(deviceAdapter.inquire(Set.of(identity))).thenReturn(Map.of(identity, new RealtimeSnapshot(identity, DeviceWorkStatus.WORKING, 1, 2, "cc", null, BigDecimal.ONE, BigDecimal.TEN, null, null, 3L, 2L, null, Instant.parse("2026-08-26T01:02:03Z"), Map.of()))); stubRegistryExecution(); service.discover(7L); var channel = candidateStore.snapshot(Instant.now()).cabinets().get(0).channels().get(0); assertEquals(DeviceWorkStatus.WORKING, channel.workStatus()); } @Test void unavailableDevicePersistsExplicitOfflineFactsWithoutRealtimeInquiry() { when(adapterMapper.selectDeviceAdapterById(7L)).thenReturn(adapter); when(deviceAdapter.discoverChannels()).thenReturn(List.of( new DetectedChannel(new ChannelIdentity("lab-bts", 22, 1, 1, 1), "127.0.0.1", false), new DetectedChannel(new ChannelIdentity("lab-bts", 22, 1, 1, 2), "127.0.0.1", false))); stubRegistryExecution(); service.discover(7L); var cabinet = candidateStore.snapshot(Instant.now()).cabinets().get(0); verify(deviceAdapter, never()).inquire(any()); assertEquals(false, cabinet.online()); assertTrue(cabinet.channels().stream().noneMatch(channel -> channel.online())); } @Test void connectionFailureHasStableCodeAndLaterDiscoveryClearsError() { when(adapterMapper.selectDeviceAdapterById(7L)).thenReturn(adapter); when(deviceAdapter.discoverChannels()) .thenThrow(new DeviceAdapterException("CONNECTION_FAILED", "connection refused", null)) .thenReturn(List.of()); stubRegistryExecution(); ServiceException failure = assertThrows(ServiceException.class, () -> service.discover(7L)); assertEquals(DeviceAdapterErrorCodes.DISCOVERY_FAILED, failure.getCode()); verify(adapterMapper).updateDiscoveryFailure(7L, "CONNECTION_FAILED", "connection refused"); assertEquals(0, service.discover(7L).channelCount()); verify(adapterMapper).updateDiscoverySuccess(eq(7L), any()); } @Test void ensuresManagedAdapterFromApplicationProvidedConfiguration() { when(adapterMapper.selectDeviceAdapterByCode("lab-bts")).thenReturn(adapter); TDeviceAdapter registered = service.ensureManagedAdapter(adapter); verify(adapterMapper).upsertManagedDeviceAdapter(adapter); assertEquals(adapter, registered); } @Test void rejectsReservedDevelopmentSimulatorCodeFromDatabaseConfiguration() { adapter.setAdapterCode("dev-sim"); assertThrows(ServiceException.class, () -> service.insert(adapter)); assertThrows(ServiceException.class, () -> service.ensureManagedAdapter(adapter)); verify(adapterMapper, never()).insertDeviceAdapter(any()); verify(adapterMapper, never()).upsertManagedDeviceAdapter(any()); } @Test void developmentSimulatorProvisioningIsIdempotentAndReadOnly() { String mapperXml; try (InputStream input = getClass().getResourceAsStream("/mapper/system/TDeviceAdapterMapper.xml")) { if (input == null) throw new AssertionError("缺少设备适配器 Mapper"); mapperXml = new String(input.readAllBytes(), StandardCharsets.UTF_8); } catch (Exception exception) { throw new AssertionError("无法读取设备适配器 Mapper", exception); } assertTrue(mapperXml.contains("upsertManagedDeviceAdapter")); assertTrue(mapperXml.contains("#{adapterCode}")); assertTrue(mapperXml.contains("#{capabilities}")); assertTrue(mapperXml.contains("on duplicate key update")); assertTrue(mapperXml.contains("del_flag=0")); } @SuppressWarnings("unchecked") private void stubRegistryExecution() { when(registry.execute(eq("lab-bts"), any())).thenAnswer(invocation -> ((Function) invocation.getArgument(1)).apply(deviceAdapter)); } }