package com.ruoyi.system.service.impl;
|
|
import java.io.IOException;
|
import java.io.InputStream;
|
import java.math.BigDecimal;
|
import java.nio.charset.StandardCharsets;
|
import java.util.List;
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
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.Mockito.never;
|
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.when;
|
|
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.mockito.ArgumentCaptor;
|
import org.mockito.InjectMocks;
|
import org.mockito.Mock;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.system.domain.TDetectionCabinet;
|
import com.ruoyi.system.domain.vo.DiscoveredCabinetVo;
|
import com.ruoyi.system.mapper.TDetectionCabinetMapper;
|
import com.ruoyi.device.discovery.InMemoryDeviceDiscoveryCandidateStore;
|
import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore.CabinetCandidate;
|
import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore.ChannelCandidate;
|
import com.ruoyi.device.model.ChannelIdentity;
|
import com.ruoyi.device.model.DeviceWorkStatus;
|
|
/**
|
* 检测柜业务层单元测试,验证字段规范化、状态限制及唯一性校验等关键规则。
|
*/
|
@ExtendWith(MockitoExtension.class)
|
class TDetectionCabinetServiceImplTest
|
{
|
@Mock
|
private TDetectionCabinetMapper detectionCabinetMapper;
|
|
private TDetectionCabinetServiceImpl detectionCabinetService;
|
private InMemoryDeviceDiscoveryCandidateStore candidateStore;
|
|
private TDetectionCabinet detectionCabinet;
|
|
/**
|
* 初始化一条满足新增校验的检测柜 Mock 数据。
|
*/
|
@BeforeEach
|
void setUp()
|
{
|
candidateStore = new InMemoryDeviceDiscoveryCandidateStore();
|
detectionCabinetService = new TDetectionCabinetServiceImpl(detectionCabinetMapper, candidateStore);
|
candidateStore.recordCabinets(List.of(candidate("CABINET-001", true, DeviceWorkStatus.WORKING)));
|
detectionCabinet = new TDetectionCabinet();
|
detectionCabinet.setCabinetCode(" CABINET-001 ");
|
detectionCabinet.setCabinetName(" 一号检测柜 ");
|
detectionCabinet.setIpAddress(" 192.168.1.10 ");
|
detectionCabinet.setMacAddress("aa-bb-cc-dd-ee-ff");
|
detectionCabinet.setManufacturer(" 厂商 ");
|
detectionCabinet.setModel(" 型号A ");
|
detectionCabinet.setCapacity(new BigDecimal("120.50"));
|
detectionCabinet.setChannelCount(8);
|
detectionCabinet.setRemark(" 备注 ");
|
}
|
|
/**
|
* 验证新增时会规范化基础字段并初始化运行状态。
|
*/
|
@Test
|
void insertDetectionCabinetShouldPromoteDiscoveredCandidate()
|
{
|
when(detectionCabinetMapper.insertDetectionCabinet(any(TDetectionCabinet.class))).thenAnswer(invocation -> {
|
invocation.getArgument(0, TDetectionCabinet.class).setCabinetId(7L);
|
return 1;
|
});
|
|
int rows = detectionCabinetService.insertDetectionCabinet(detectionCabinet);
|
|
ArgumentCaptor<TDetectionCabinet> captor = ArgumentCaptor.forClass(TDetectionCabinet.class);
|
verify(detectionCabinetMapper).insertDetectionCabinet(captor.capture());
|
TDetectionCabinet savedCabinet = captor.getValue();
|
assertEquals(1, rows);
|
assertEquals(7L, savedCabinet.getCabinetId());
|
assertEquals("CABINET-001", savedCabinet.getCabinetCode());
|
assertEquals("一号检测柜", savedCabinet.getCabinetName());
|
assertEquals("192.168.1.10", savedCabinet.getIpAddress());
|
assertEquals("AA:BB:CC:DD:EE:FF", savedCabinet.getMacAddress());
|
assertEquals(new BigDecimal("120.50"), savedCabinet.getCapacity());
|
assertEquals("OFFLINE", savedCabinet.getOnlineStatus());
|
assertEquals("0", savedCabinet.getStatus());
|
assertNull(savedCabinet.getCurrentTest());
|
}
|
|
/**
|
* 验证设备选择查询不再从 MySQL 读取发现候选。
|
*/
|
@Test
|
void discoveredCabinetSqlShouldNotReadLegacyTestColumns() throws IOException
|
{
|
try (InputStream input = getClass().getResourceAsStream(
|
"/mapper/system/TDetectionCabinetMapper.xml"))
|
{
|
assertNotNull(input);
|
String mapperXml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
assertFalse(mapperXml.contains("selectDiscoveredCabinetList"));
|
assertFalse(mapperXml.contains("registration_status = 'DISCOVERED'"));
|
}
|
}
|
|
/**
|
* 验证删除正式检测柜只取消人工登记,不删除适配器持续维护的设备事实。
|
*/
|
@Test
|
void deleteDetectionCabinetSqlShouldReturnOnlineDeviceToDiscoveryPool() throws IOException
|
{
|
try (InputStream input = getClass().getResourceAsStream(
|
"/mapper/system/TDetectionCabinetMapper.xml"))
|
{
|
assertNotNull(input);
|
String mapperXml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
int deleteStart = mapperXml.indexOf("<update id=\"deleteDetectionCabinetById\"");
|
int deleteEnd = mapperXml.indexOf("</update>", deleteStart);
|
String deleteSql = mapperXml.substring(deleteStart, deleteEnd);
|
|
assertTrue(deleteSql.contains("set del_flag = cabinet_id"));
|
assertTrue(deleteSql.contains("registration_status = 'REGISTERED'"));
|
}
|
}
|
|
/**
|
* 验证检测柜主列表从通道事实派生旧页面展示字段,不读取数据库不存在的列。
|
*/
|
@Test
|
void detectionCabinetListSqlShouldDeriveLegacyPresentationFields() throws IOException
|
{
|
try (InputStream input = getClass().getResourceAsStream(
|
"/mapper/system/TDetectionCabinetMapper.xml"))
|
{
|
assertNotNull(input);
|
String mapperXml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
int selectStart = mapperXml.indexOf("<sql id=\"selectDetectionCabinetVo\"");
|
int selectEnd = mapperXml.indexOf("</sql>", selectStart);
|
String selectSql = mapperXml.substring(selectStart, selectEnd);
|
assertFalse(selectSql.contains("online_status, test_status"));
|
assertFalse(selectSql.contains("current_test, status"));
|
assertTrue(selectSql.contains("as test_status"));
|
assertTrue(selectSql.contains("as current_test"));
|
String activeTestSql = sqlFragment(mapperXml, "activeCabinetTestExists");
|
assertTrue(activeTestSql.contains("t_test_channel_binding"));
|
assertTrue(activeTestSql.contains("t_detection_channel"));
|
assertTrue(activeTestSql.contains("test_fact.test_status"));
|
assertFalse(activeTestSql.contains("t_test_battery"));
|
assertFalse(activeTestSql.contains("battery_fact.cabinet_id"));
|
int listStart = mapperXml.indexOf("<select id=\"selectDetectionCabinetList\"");
|
int listEnd = mapperXml.indexOf("</select>", listStart);
|
assertTrue(mapperXml.substring(listStart, listEnd).contains("registration_status = 'REGISTERED'"));
|
int registerStart = mapperXml.indexOf("<insert id=\"insertDetectionCabinet\"");
|
int registerEnd = mapperXml.indexOf("</insert>", registerStart);
|
String registerSql = mapperXml.substring(registerStart, registerEnd);
|
assertTrue(registerSql.contains("'REGISTERED'"));
|
assertTrue(registerSql.contains("#{createBy}"));
|
}
|
}
|
|
private String sqlFragment(String mapperXml, String id)
|
{
|
int start = mapperXml.indexOf("<sql id=\"" + id + "\"");
|
int end = mapperXml.indexOf("</sql>", start);
|
return mapperXml.substring(start, end);
|
}
|
|
private CabinetCandidate candidate(String deviceCode, boolean online, DeviceWorkStatus workStatus)
|
{
|
java.time.Instant observedAt = java.time.Instant.now();
|
List<ChannelCandidate> channels = java.util.stream.IntStream.rangeClosed(1, 8)
|
.mapToObj(channelId -> new ChannelCandidate(
|
new ChannelIdentity("dev-sim", 22, 1, 1, channelId), online, workStatus, observedAt))
|
.toList();
|
return new CabinetCandidate("dev-sim", null, deviceCode, 22, 1, "192.168.1.10",
|
"AA:BB:CC:DD:EE:FF", "一号模拟柜", "开发模拟器", "BTS Virtual Cabinet",
|
channels, observedAt, observedAt.plusSeconds(60));
|
}
|
|
/**
|
* 验证设备选择列表读取 Redis 候选,并保持原前端字段和状态文本。
|
*/
|
@Test
|
void selectDiscoveredCabinetListShouldMapPersistedCabinetFacts()
|
{
|
when(detectionCabinetMapper.selectRegisteredCabinetCodes()).thenReturn(List.of());
|
|
List<DiscoveredCabinetVo> devices = detectionCabinetService.selectDiscoveredCabinetList();
|
|
assertEquals(1, devices.size());
|
assertEquals("CABINET-001", devices.get(0).getDeviceCode());
|
assertEquals("192.168.1.10", devices.get(0).getIp());
|
assertEquals("AA:BB:CC:DD:EE:FF", devices.get(0).getMac());
|
assertEquals("在线", devices.get(0).getOnlineStatus());
|
assertEquals("测试中", devices.get(0).getTestStatus());
|
assertEquals("待确认", devices.get(0).getCurrentTest());
|
assertEquals("一号模拟柜", devices.get(0).getName());
|
assertEquals("开发模拟器", devices.get(0).getManufacturer());
|
assertEquals("BTS Virtual Cabinet", devices.get(0).getModel());
|
assertEquals(8, devices.get(0).getChannelCount());
|
}
|
|
/**
|
* 验证重复设备编码会阻止新增且不会执行写库操作。
|
*/
|
@Test
|
void insertDetectionCabinetShouldRejectDuplicateCabinetCode()
|
{
|
TDetectionCabinet existing = new TDetectionCabinet();
|
existing.setCabinetId(2L);
|
existing.setRegistrationStatus("REGISTERED");
|
when(detectionCabinetMapper.selectByCabinetCode("CABINET-001")).thenReturn(existing);
|
|
ServiceException exception = assertThrows(ServiceException.class,
|
() -> detectionCabinetService.insertDetectionCabinet(detectionCabinet));
|
|
assertEquals("设备编码已存在", exception.getMessage());
|
verify(detectionCabinetMapper, never()).insertDetectionCabinet(any(TDetectionCabinet.class));
|
}
|
|
@Test
|
void insertDetectionCabinetShouldRequireDiscoveredCandidate()
|
{
|
detectionCabinetService = new TDetectionCabinetServiceImpl(detectionCabinetMapper,
|
new InMemoryDeviceDiscoveryCandidateStore());
|
|
ServiceException exception = assertThrows(ServiceException.class,
|
() -> detectionCabinetService.insertDetectionCabinet(detectionCabinet));
|
|
assertEquals("请先选择在线的发现候选设备", exception.getMessage());
|
verify(detectionCabinetMapper, never()).insertDetectionCabinet(any());
|
}
|
|
@Test
|
void insertDetectionCabinetShouldValidateAuthoritativeCandidateTopology()
|
{
|
detectionCabinet.setIpAddress("invalid-ip");
|
detectionCabinet.setMacAddress("invalid-mac");
|
detectionCabinet.setChannelCount(99);
|
when(detectionCabinetMapper.insertDetectionCabinet(any(TDetectionCabinet.class))).thenAnswer(invocation -> {
|
invocation.getArgument(0, TDetectionCabinet.class).setCabinetId(7L);
|
return 1;
|
});
|
|
assertEquals(1, detectionCabinetService.insertDetectionCabinet(detectionCabinet));
|
|
ArgumentCaptor<TDetectionCabinet> captor = ArgumentCaptor.forClass(TDetectionCabinet.class);
|
verify(detectionCabinetMapper).insertDetectionCabinet(captor.capture());
|
assertEquals("192.168.1.10", captor.getValue().getIpAddress());
|
assertEquals("AA:BB:CC:DD:EE:FF", captor.getValue().getMacAddress());
|
assertEquals(8, captor.getValue().getChannelCount());
|
}
|
|
/**
|
* 验证测试中的检测柜不能被禁用。
|
*/
|
@Test
|
void updateDetectionCabinetStatusShouldRejectDisablingTestingCabinet()
|
{
|
TDetectionCabinet current = new TDetectionCabinet();
|
current.setCabinetId(1L);
|
current.setStatus("0");
|
current.setTestStatus("1");
|
when(detectionCabinetMapper.selectDetectionCabinetById(1L)).thenReturn(current);
|
|
TDetectionCabinet statusCommand = new TDetectionCabinet();
|
statusCommand.setCabinetId(1L);
|
statusCommand.setStatus("1");
|
|
ServiceException exception = assertThrows(ServiceException.class,
|
() -> detectionCabinetService.updateDetectionCabinetStatus(statusCommand));
|
|
assertEquals("检测柜正在测试,不能禁用", exception.getMessage());
|
verify(detectionCabinetMapper, never()).updateDetectionCabinetStatus(any(TDetectionCabinet.class));
|
}
|
|
/**
|
* 验证检测中的检测柜允许执行启用操作。
|
*/
|
@Test
|
void updateDetectionCabinetStatusShouldAllowEnablingTestingCabinet()
|
{
|
TDetectionCabinet current = new TDetectionCabinet();
|
current.setCabinetId(1L);
|
current.setStatus("1");
|
current.setTestStatus("1");
|
when(detectionCabinetMapper.selectDetectionCabinetById(1L)).thenReturn(current);
|
when(detectionCabinetMapper.updateDetectionCabinetStatus(any(TDetectionCabinet.class))).thenReturn(1);
|
|
TDetectionCabinet statusCommand = new TDetectionCabinet();
|
statusCommand.setCabinetId(1L);
|
statusCommand.setStatus("0");
|
|
int rows = detectionCabinetService.updateDetectionCabinetStatus(statusCommand);
|
|
assertEquals(1, rows);
|
verify(detectionCabinetMapper).updateDetectionCabinetStatus(statusCommand);
|
}
|
|
/**
|
* 验证重复提交相同状态时按幂等成功处理,不重复写库。
|
*/
|
@Test
|
void updateDetectionCabinetStatusShouldBeIdempotent()
|
{
|
TDetectionCabinet current = new TDetectionCabinet();
|
current.setCabinetId(1L);
|
current.setStatus("0");
|
current.setTestStatus("0");
|
when(detectionCabinetMapper.selectDetectionCabinetById(1L)).thenReturn(current);
|
|
TDetectionCabinet statusCommand = new TDetectionCabinet();
|
statusCommand.setCabinetId(1L);
|
statusCommand.setStatus("0");
|
|
int rows = detectionCabinetService.updateDetectionCabinetStatus(statusCommand);
|
|
assertEquals(1, rows);
|
verify(detectionCabinetMapper, never()).updateDetectionCabinetStatus(any(TDetectionCabinet.class));
|
}
|
|
/**
|
* 验证批量删除会先检查所有检测柜,存在测试中记录时不删除任何数据。
|
*/
|
@Test
|
void deleteDetectionCabinetByIdsShouldRejectTestingCabinetBeforeDeleting()
|
{
|
TDetectionCabinet normal = new TDetectionCabinet();
|
normal.setCabinetId(1L);
|
normal.setTestStatus("0");
|
TDetectionCabinet testing = new TDetectionCabinet();
|
testing.setCabinetId(2L);
|
testing.setTestStatus("1");
|
when(detectionCabinetMapper.selectDetectionCabinetById(1L)).thenReturn(normal);
|
when(detectionCabinetMapper.selectDetectionCabinetById(2L)).thenReturn(testing);
|
|
ServiceException exception = assertThrows(ServiceException.class,
|
() -> detectionCabinetService.deleteDetectionCabinetByIds(new Long[] { 1L, 2L }, "admin"));
|
|
assertEquals("检测柜正在测试,不能删除", exception.getMessage());
|
verify(detectionCabinetMapper, never()).deleteDetectionCabinetById(any(), any());
|
}
|
|
/**
|
* 验证编辑时唯一性查询命中当前记录仍视为唯一。
|
*/
|
@Test
|
void checkUniqueShouldAllowCurrentCabinet()
|
{
|
TDetectionCabinet current = new TDetectionCabinet();
|
current.setCabinetId(1L);
|
current.setCabinetCode("CABINET-001");
|
current.setMacAddress("AA:BB:CC:DD:EE:FF");
|
when(detectionCabinetMapper.selectByCabinetCode("CABINET-001")).thenReturn(current);
|
when(detectionCabinetMapper.selectByMacAddress("AA:BB:CC:DD:EE:FF")).thenReturn(current);
|
|
assertTrue(detectionCabinetService.checkCabinetCodeUnique(current));
|
assertTrue(detectionCabinetService.checkMacAddressUnique(current));
|
}
|
}
|