package com.ruoyi.system.service.impl; 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.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.math.BigDecimal; 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 com.ruoyi.common.exception.ServiceException; import com.ruoyi.device.api.DeviceAdapter; import com.ruoyi.device.api.DeviceAdapterException; import com.ruoyi.device.model.ChannelIdentity; import com.ruoyi.device.model.DetectedChannel; 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; @BeforeEach void setUp() { service = new DeviceAdapterServiceImpl(adapterMapper, registry); 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.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 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(); when(adapterMapper.selectCabinetIdByIdentity(7L, 22, 1)).thenReturn(10L); assertEquals(2, service.discover(7L).channelCount()); assertEquals(2, service.discover(7L).channelCount()); verify(adapterMapper, times(4)).upsertDetectedChannel(any(TDetectionChannel.class)); verify(adapterMapper, times(2)).updateDiscoverySuccess(eq(7L), any()); verify(adapterMapper, times(2)).updateDetectedCabinet(any(TDetectionCabinet.class)); } @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()))); when(adapterMapper.selectCabinetIdByIdentity(7L, 22, 1)).thenReturn(10L); stubRegistryExecution(); service.discover(7L); var channel = org.mockito.ArgumentCaptor.forClass(TDetectionChannel.class); verify(adapterMapper).upsertDetectedChannel(channel.capture()); assertEquals("WORKING", channel.getValue().getWorkStatus()); } @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()); } @SuppressWarnings("unchecked") private void stubRegistryExecution() { when(registry.execute(eq("lab-bts"), any())).thenAnswer(invocation -> ((Function) invocation.getArgument(1)).apply(deviceAdapter)); } }