diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListenerTest.java b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListenerTest.java new file mode 100644 index 0000000..7b16739 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListenerTest.java @@ -0,0 +1,277 @@ +package com.viewsh.module.ops.security.integration.listener; + +import com.viewsh.module.ops.core.dispatch.DispatchEngine; +import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext; +import com.viewsh.module.ops.core.event.OrderCompletedEvent; +import com.viewsh.module.ops.core.event.OrderCreatedEvent; +import com.viewsh.module.ops.core.event.OrderStateChangedEvent; +import com.viewsh.module.ops.enums.PriorityEnum; +import com.viewsh.module.ops.enums.WorkOrderStatusEnum; +import com.viewsh.module.ops.security.dal.dataobject.workorder.OpsOrderSecurityExtDO; +import com.viewsh.module.ops.security.dal.mysql.workorder.OpsOrderSecurityExtMapper; +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 static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * 安保工单事件监听器测试 + * + * @author lzh + */ +@ExtendWith(MockitoExtension.class) +public class SecurityOrderEventListenerTest { + + @InjectMocks + private SecurityOrderEventListener listener; + + @Mock + private OpsOrderSecurityExtMapper securityExtMapper; + + @Mock + private DispatchEngine dispatchEngine; + + private static final Long TEST_ORDER_ID = 10001L; + private static final String TEST_ORDER_CODE = "SECURITY-20260310-0001"; + + // ==================== onOrderCreated 测试 ==================== + + @Test + void testOnOrderCreated_SecurityType_TriggersDispatch() { + OrderCreatedEvent event = OrderCreatedEvent.builder() + .orderId(TEST_ORDER_ID) + .orderType("SECURITY") + .orderCode(TEST_ORDER_CODE) + .title("入侵告警") + .areaId(100L) + .priority(PriorityEnum.P1.getPriority()) + .build(); + + // 执行 + listener.onOrderCreated(event); + + // 验证触发了派单(asyncDispatchAfterCreated 是自调用,实际同步执行) + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(OrderDispatchContext.class); + verify(dispatchEngine).dispatch(contextCaptor.capture()); + OrderDispatchContext ctx = contextCaptor.getValue(); + assertEquals(TEST_ORDER_ID, ctx.getOrderId()); + assertEquals(TEST_ORDER_CODE, ctx.getOrderCode()); + assertEquals("入侵告警", ctx.getOrderTitle()); + assertEquals("SECURITY", ctx.getBusinessType()); + assertEquals(100L, ctx.getAreaId()); + assertEquals(PriorityEnum.P1, ctx.getPriority()); + } + + @Test + void testOnOrderCreated_CleanType_Ignored() { + OrderCreatedEvent event = OrderCreatedEvent.builder() + .orderId(TEST_ORDER_ID) + .orderType("CLEAN") + .build(); + + // 执行 + listener.onOrderCreated(event); + + // 验证不触发派单 + verify(dispatchEngine, never()).dispatch(any()); + } + + @Test + void testOnOrderCreated_DispatchException_Caught() { + OrderCreatedEvent event = OrderCreatedEvent.builder() + .orderId(TEST_ORDER_ID) + .orderType("SECURITY") + .orderCode(TEST_ORDER_CODE) + .areaId(100L) + .priority(PriorityEnum.P1.getPriority()) + .build(); + + when(dispatchEngine.dispatch(any())).thenThrow(new RuntimeException("调度引擎异常")); + + // 执行:不应抛出异常 + assertDoesNotThrow(() -> listener.onOrderCreated(event)); + } + + // ==================== onOrderStateChanged 测试 ==================== + + @Test + void testOnOrderStateChanged_Dispatched_RecordsTime() { + OrderStateChangedEvent event = buildStateChangedEvent( + WorkOrderStatusEnum.PENDING, WorkOrderStatusEnum.DISPATCHED); + + lenient().when(securityExtMapper.insertOrUpdateSelective(any())).thenReturn(1); + + // 执行 + listener.onOrderStateChanged(event); + + // 验证扩展表写入 dispatchedTime + ArgumentCaptor captor = ArgumentCaptor.forClass(OpsOrderSecurityExtDO.class); + verify(securityExtMapper).insertOrUpdateSelective(captor.capture()); + OpsOrderSecurityExtDO ext = captor.getValue(); + assertEquals(TEST_ORDER_ID, ext.getOpsOrderId()); + assertNotNull(ext.getDispatchedTime()); + } + + @Test + void testOnOrderStateChanged_Confirmed_RecordsTime() { + OrderStateChangedEvent event = buildStateChangedEvent( + WorkOrderStatusEnum.DISPATCHED, WorkOrderStatusEnum.CONFIRMED); + + lenient().when(securityExtMapper.insertOrUpdateSelective(any())).thenReturn(1); + + // 执行 + listener.onOrderStateChanged(event); + + // 验证扩展表写入 confirmedTime + ArgumentCaptor captor = ArgumentCaptor.forClass(OpsOrderSecurityExtDO.class); + verify(securityExtMapper).insertOrUpdateSelective(captor.capture()); + OpsOrderSecurityExtDO ext = captor.getValue(); + assertEquals(TEST_ORDER_ID, ext.getOpsOrderId()); + assertNotNull(ext.getConfirmedTime()); + } + + @Test + void testOnOrderStateChanged_Completed_RecordsTime() { + OrderStateChangedEvent event = buildStateChangedEvent( + WorkOrderStatusEnum.ARRIVED, WorkOrderStatusEnum.COMPLETED); + + OpsOrderSecurityExtDO existingExt = OpsOrderSecurityExtDO.builder() + .id(1L).opsOrderId(TEST_ORDER_ID).build(); + when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(existingExt); + lenient().when(securityExtMapper.insertOrUpdateSelective(any())).thenReturn(1); + + // 执行 + listener.onOrderStateChanged(event); + + // 验证扩展表写入 completedTime + ArgumentCaptor captor = ArgumentCaptor.forClass(OpsOrderSecurityExtDO.class); + verify(securityExtMapper).insertOrUpdateSelective(captor.capture()); + OpsOrderSecurityExtDO ext = captor.getValue(); + assertEquals(TEST_ORDER_ID, ext.getOpsOrderId()); + assertNotNull(ext.getCompletedTime()); + } + + @Test + void testOnOrderStateChanged_Completed_NoExt_Skipped() { + OrderStateChangedEvent event = buildStateChangedEvent( + WorkOrderStatusEnum.ARRIVED, WorkOrderStatusEnum.COMPLETED); + + when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(null); + + // 执行 + listener.onOrderStateChanged(event); + + // 验证:扩展记录不存在时不写入 + verify(securityExtMapper, never()).insertOrUpdateSelective(any()); + } + + @Test + void testOnOrderStateChanged_CleanType_Ignored() { + OrderStateChangedEvent event = OrderStateChangedEvent.builder() + .orderId(TEST_ORDER_ID) + .orderType("CLEAN") + .oldStatus(WorkOrderStatusEnum.PENDING) + .newStatus(WorkOrderStatusEnum.DISPATCHED) + .build(); + + // 执行 + listener.onOrderStateChanged(event); + + // 验证不触发任何扩展表操作 + verify(securityExtMapper, never()).insertOrUpdateSelective(any()); + verify(securityExtMapper, never()).selectByOpsOrderId(anyLong()); + } + + @Test + void testOnOrderStateChanged_OtherStatus_NoAction() { + // CANCELLED 等状态无需额外处理 + OrderStateChangedEvent event = buildStateChangedEvent( + WorkOrderStatusEnum.PENDING, WorkOrderStatusEnum.CANCELLED); + + // 执行 + listener.onOrderStateChanged(event); + + // 验证无扩展表操作 + verify(securityExtMapper, never()).insertOrUpdateSelective(any()); + } + + // ==================== onOrderCompleted 测试 ==================== + + @Test + void testOnOrderCompleted_HasAssignee_DispatchNext() { + OrderCompletedEvent event = OrderCompletedEvent.builder() + .orderId(TEST_ORDER_ID) + .orderType("SECURITY") + .assigneeId(2001L) + .build(); + + // 执行 + listener.onOrderCompleted(event); + + // 验证调用自动派送下一个 + verify(dispatchEngine).autoDispatchNext(TEST_ORDER_ID, 2001L); + } + + @Test + void testOnOrderCompleted_NoAssignee_SkipDispatch() { + OrderCompletedEvent event = OrderCompletedEvent.builder() + .orderId(TEST_ORDER_ID) + .orderType("SECURITY") + .assigneeId(null) // 无分配人 + .build(); + + // 执行 + listener.onOrderCompleted(event); + + // 验证不调用自动派送 + verify(dispatchEngine, never()).autoDispatchNext(anyLong(), anyLong()); + } + + @Test + void testOnOrderCompleted_CleanType_Ignored() { + OrderCompletedEvent event = OrderCompletedEvent.builder() + .orderId(TEST_ORDER_ID) + .orderType("CLEAN") + .assigneeId(2001L) + .build(); + + // 执行 + listener.onOrderCompleted(event); + + // 验证不触发 + verify(dispatchEngine, never()).autoDispatchNext(anyLong(), anyLong()); + } + + @Test + void testOnOrderCompleted_DispatchException_Caught() { + OrderCompletedEvent event = OrderCompletedEvent.builder() + .orderId(TEST_ORDER_ID) + .orderType("SECURITY") + .assigneeId(2001L) + .build(); + + doThrow(new RuntimeException("派送异常")).when(dispatchEngine) + .autoDispatchNext(anyLong(), anyLong()); + + // 执行:不应抛出异常 + assertDoesNotThrow(() -> listener.onOrderCompleted(event)); + } + + // ==================== 辅助方法 ==================== + + private OrderStateChangedEvent buildStateChangedEvent( + WorkOrderStatusEnum oldStatus, WorkOrderStatusEnum newStatus) { + return OrderStateChangedEvent.builder() + .orderId(TEST_ORDER_ID) + .orderType("SECURITY") + .oldStatus(oldStatus) + .newStatus(newStatus) + .build(); + } +} diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/area/OpsAreaSecurityUserServiceTest.java b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/area/OpsAreaSecurityUserServiceTest.java new file mode 100644 index 0000000..dd43103 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/area/OpsAreaSecurityUserServiceTest.java @@ -0,0 +1,122 @@ +package com.viewsh.module.ops.security.service.area; + +import com.viewsh.framework.common.exception.ServiceException; +import com.viewsh.module.ops.security.dal.dataobject.area.OpsAreaSecurityUserDO; +import com.viewsh.module.ops.security.dal.mysql.area.OpsAreaSecurityUserMapper; +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 java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * 区域安保人员绑定服务单元测试 + * + * @author lzh + */ +@ExtendWith(MockitoExtension.class) +public class OpsAreaSecurityUserServiceTest { + + @InjectMocks + private OpsAreaSecurityUserServiceImpl service; + + @Mock + private OpsAreaSecurityUserMapper areaSecurityUserMapper; + + @Test + void bindUser_success() { + when(areaSecurityUserMapper.selectByAreaIdAndUserId(100L, 2001L)).thenReturn(null); + when(areaSecurityUserMapper.insert(any(OpsAreaSecurityUserDO.class))).thenReturn(1); + + Long id = service.bindUser(100L, 2001L, "张三", 10L, 0); + + ArgumentCaptor captor = ArgumentCaptor.forClass(OpsAreaSecurityUserDO.class); + verify(areaSecurityUserMapper).insert(captor.capture()); + OpsAreaSecurityUserDO saved = captor.getValue(); + assertEquals(100L, saved.getAreaId()); + assertEquals(2001L, saved.getUserId()); + assertEquals("张三", saved.getUserName()); + assertEquals(10L, saved.getTeamId()); + assertTrue(saved.getEnabled()); + assertEquals(0, saved.getSort()); + } + + @Test + void bindUser_duplicate_throws() { + OpsAreaSecurityUserDO existing = OpsAreaSecurityUserDO.builder() + .id(1L).areaId(100L).userId(2001L).build(); + when(areaSecurityUserMapper.selectByAreaIdAndUserId(100L, 2001L)).thenReturn(existing); + + assertThrows(ServiceException.class, () -> service.bindUser(100L, 2001L, "张三", null, null)); + verify(areaSecurityUserMapper, never()).insert(any(OpsAreaSecurityUserDO.class)); + } + + @Test + void unbindUser_success() { + OpsAreaSecurityUserDO existing = OpsAreaSecurityUserDO.builder() + .id(1L).areaId(100L).userId(2001L).build(); + when(areaSecurityUserMapper.selectById(1L)).thenReturn(existing); + when(areaSecurityUserMapper.deleteById(1L)).thenReturn(1); + + service.unbindUser(1L); + + verify(areaSecurityUserMapper).deleteById(1L); + } + + @Test + void unbindUser_notFound_throws() { + when(areaSecurityUserMapper.selectById(999L)).thenReturn(null); + + assertThrows(ServiceException.class, () -> service.unbindUser(999L)); + verify(areaSecurityUserMapper, never()).deleteById(any()); + } + + @Test + void listByAreaId_returnsEnabledOnly() { + OpsAreaSecurityUserDO user1 = OpsAreaSecurityUserDO.builder() + .id(1L).areaId(100L).userId(2001L).userName("张三").enabled(true).build(); + OpsAreaSecurityUserDO user2 = OpsAreaSecurityUserDO.builder() + .id(2L).areaId(100L).userId(2002L).userName("李四").enabled(true).build(); + + when(areaSecurityUserMapper.selectListByAreaId(100L)).thenReturn(Arrays.asList(user1, user2)); + + List result = service.listByAreaId(100L); + + assertEquals(2, result.size()); + verify(areaSecurityUserMapper).selectListByAreaId(100L); + } + + @Test + void updateBinding_success() { + OpsAreaSecurityUserDO existing = OpsAreaSecurityUserDO.builder() + .id(1L).areaId(100L).userId(2001L).enabled(true).sort(0).build(); + when(areaSecurityUserMapper.selectById(1L)).thenReturn(existing); + when(areaSecurityUserMapper.updateById(any(OpsAreaSecurityUserDO.class))).thenReturn(1); + + service.updateBinding(1L, false, 5, 20L); + + ArgumentCaptor captor = ArgumentCaptor.forClass(OpsAreaSecurityUserDO.class); + verify(areaSecurityUserMapper).updateById(captor.capture()); + OpsAreaSecurityUserDO updated = captor.getValue(); + assertEquals(1L, updated.getId()); + assertFalse(updated.getEnabled()); + assertEquals(5, updated.getSort()); + assertEquals(20L, updated.getTeamId()); + } + + @Test + void updateBinding_notFound_throws() { + when(areaSecurityUserMapper.selectById(999L)).thenReturn(null); + + assertThrows(ServiceException.class, () -> service.updateBinding(999L, true, null, null)); + } + +} diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/dispatch/SecurityAreaAssignStrategyTest.java b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/dispatch/SecurityAreaAssignStrategyTest.java new file mode 100644 index 0000000..1387fd1 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/dispatch/SecurityAreaAssignStrategyTest.java @@ -0,0 +1,108 @@ +package com.viewsh.module.ops.security.service.dispatch; + +import com.viewsh.module.ops.core.dispatch.model.AssigneeRecommendation; +import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext; +import com.viewsh.module.ops.security.dal.dataobject.area.OpsAreaSecurityUserDO; +import com.viewsh.module.ops.security.dal.mysql.area.OpsAreaSecurityUserMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * 安保区域分配策略单元测试 + * + * @author lzh + */ +@ExtendWith(MockitoExtension.class) +public class SecurityAreaAssignStrategyTest { + + @InjectMocks + private SecurityAreaAssignStrategy strategy; + + @Mock + private OpsAreaSecurityUserMapper areaSecurityUserMapper; + + @Test + void recommend_noAreaId_returnsNone() { + OrderDispatchContext context = OrderDispatchContext.builder() + .orderId(1001L) + .areaId(null) + .build(); + + AssigneeRecommendation result = strategy.recommend(context); + + assertFalse(result.hasRecommendation()); + verify(areaSecurityUserMapper, never()).selectListByAreaId(any()); + } + + @Test + void recommend_noUsersInArea_returnsNone() { + Long areaId = 100L; + OrderDispatchContext context = OrderDispatchContext.builder() + .orderId(1001L) + .areaId(areaId) + .build(); + + when(areaSecurityUserMapper.selectListByAreaId(areaId)).thenReturn(Collections.emptyList()); + + AssigneeRecommendation result = strategy.recommend(context); + + assertFalse(result.hasRecommendation()); + verify(areaSecurityUserMapper).selectListByAreaId(areaId); + } + + @Test + void recommend_hasUsers_returnsRecommendation() { + Long areaId = 100L; + OrderDispatchContext context = OrderDispatchContext.builder() + .orderId(1001L) + .areaId(areaId) + .build(); + + OpsAreaSecurityUserDO user1 = OpsAreaSecurityUserDO.builder() + .id(1L).areaId(areaId).userId(2001L).userName("张三").enabled(true).build(); + OpsAreaSecurityUserDO user2 = OpsAreaSecurityUserDO.builder() + .id(2L).areaId(areaId).userId(2002L).userName("李四").enabled(true).build(); + + when(areaSecurityUserMapper.selectListByAreaId(areaId)).thenReturn(Arrays.asList(user1, user2)); + + AssigneeRecommendation result = strategy.recommend(context); + + assertTrue(result.hasRecommendation()); + assertNotNull(result.getAssigneeId()); + assertTrue(result.getAssigneeId().equals(2001L) || result.getAssigneeId().equals(2002L)); + assertEquals(50, result.getScore()); + assertEquals("区域随机分配", result.getReason()); + assertEquals(areaId, result.getAreaId()); + } + + @Test + void recommend_singleUser_returnsThatUser() { + Long areaId = 100L; + OrderDispatchContext context = OrderDispatchContext.builder() + .orderId(1001L) + .areaId(areaId) + .build(); + + OpsAreaSecurityUserDO user = OpsAreaSecurityUserDO.builder() + .id(1L).areaId(areaId).userId(2001L).userName("张三").enabled(true).build(); + + when(areaSecurityUserMapper.selectListByAreaId(areaId)).thenReturn(Collections.singletonList(user)); + + AssigneeRecommendation result = strategy.recommend(context); + + assertTrue(result.hasRecommendation()); + assertEquals(2001L, result.getAssigneeId()); + assertEquals("张三", result.getAssigneeName()); + assertEquals(areaId, result.getAreaId()); + } + +} diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderExtQueryHandlerTest.java b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderExtQueryHandlerTest.java new file mode 100644 index 0000000..0382862 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderExtQueryHandlerTest.java @@ -0,0 +1,190 @@ +package com.viewsh.module.ops.security.service.securityorder; + +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.enums.PriorityEnum; +import com.viewsh.module.ops.enums.WorkOrderStatusEnum; +import com.viewsh.module.ops.security.dal.dataobject.workorder.OpsOrderSecurityExtDO; +import com.viewsh.module.ops.security.dal.mysql.workorder.OpsOrderSecurityExtMapper; +import com.viewsh.module.ops.service.OrderDetailVO; +import com.viewsh.module.ops.service.OrderSummaryVO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * 安保工单扩展查询处理器测试 + * + * @author lzh + */ +@ExtendWith(MockitoExtension.class) +public class SecurityOrderExtQueryHandlerTest { + + @InjectMocks + private SecurityOrderExtQueryHandler handler; + + @Mock + private OpsOrderSecurityExtMapper securityExtMapper; + + private static final Long TEST_ORDER_ID = 10001L; + private OpsOrderSecurityExtDO testSecurityExt; + private OpsOrderDO testOrder; + + @BeforeEach + void setUp() { + testSecurityExt = OpsOrderSecurityExtDO.builder() + .id(1L) + .opsOrderId(TEST_ORDER_ID) + .alarmId("ALM20260310001") + .alarmType("intrusion") + .cameraId("CAM_001") + .roiId("ROI_001") + .imageUrl("https://oss.example.com/snapshot.jpg") + .assignedUserId(2001L) + .assignedUserName("张安保") + .assignedTeamId(301L) + .result("已排查,系误报") + .resultImgUrls("[\"https://oss/r1.jpg\"]") + .dispatchedTime(LocalDateTime.of(2026, 3, 10, 10, 0)) + .confirmedTime(LocalDateTime.of(2026, 3, 10, 10, 2)) + .completedTime(LocalDateTime.of(2026, 3, 10, 10, 30)) + .build(); + + testOrder = OpsOrderDO.builder() + .id(TEST_ORDER_ID) + .orderCode("SECURITY-20260310-0001") + .orderType("SECURITY") + .sourceType("ALARM") + .title("A栋3层入侵告警") + .description("摄像头检测到异常人员") + .priority(PriorityEnum.P1.getPriority()) + .status(WorkOrderStatusEnum.COMPLETED.getStatus()) + .areaId(100L) + .location("A栋3层东侧走廊") + .assigneeId(2001L) + .assigneeName("张安保") + .startTime(LocalDateTime.of(2026, 3, 10, 10, 5)) + .endTime(LocalDateTime.of(2026, 3, 10, 10, 30)) + .responseSeconds(300) + .completionSeconds(1500) + .build(); + } + + // ==================== supports() 测试 ==================== + + @Test + void testSupports_Security_ReturnsTrue() { + assertTrue(handler.supports("SECURITY")); + } + + @Test + void testSupports_Clean_ReturnsFalse() { + assertFalse(handler.supports("CLEAN")); + } + + @Test + void testSupports_Null_ReturnsFalse() { + assertFalse(handler.supports(null)); + } + + @Test + void testSupports_Empty_ReturnsFalse() { + assertFalse(handler.supports("")); + } + + // ==================== enrichWithExtInfo() 测试 ==================== + + @Test + void testEnrichWithExtInfo_HasExt_FieldsPopulated() { + when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(testSecurityExt); + + OrderSummaryVO vo = new OrderSummaryVO(); + handler.enrichWithExtInfo(vo, TEST_ORDER_ID); + + // 验证 extInfo 非空 + assertNotNull(vo.getExtInfo()); + Map extInfo = vo.getExtInfo(); + + // 验证各字段映射 + assertEquals("ALM20260310001", extInfo.get("alarmId")); + assertEquals("intrusion", extInfo.get("alarmType")); + assertEquals("CAM_001", extInfo.get("cameraId")); + assertEquals("https://oss.example.com/snapshot.jpg", extInfo.get("imageUrl")); + assertEquals("张安保", extInfo.get("assignedUserName")); + assertEquals(testSecurityExt.getConfirmedTime(), extInfo.get("confirmedTime")); + assertEquals(testSecurityExt.getCompletedTime(), extInfo.get("completedTime")); + assertEquals("已排查,系误报", extInfo.get("result")); + } + + @Test + void testEnrichWithExtInfo_NoExt_ExtInfoNotSet() { + when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(null); + + OrderSummaryVO vo = new OrderSummaryVO(); + handler.enrichWithExtInfo(vo, TEST_ORDER_ID); + + // 无扩展记录时,extInfo 未被填充 + assertTrue(vo.getExtInfo() == null || vo.getExtInfo().isEmpty()); + } + + // ==================== buildDetailVO() 测试 ==================== + + @Test + void testBuildDetailVO_HasExt_FullVO() { + when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(testSecurityExt); + + OrderDetailVO vo = handler.buildDetailVO(testOrder); + + // 验证主表字段 + assertNotNull(vo); + assertEquals(TEST_ORDER_ID, vo.getId()); + assertEquals("SECURITY-20260310-0001", vo.getOrderCode()); + assertEquals("SECURITY", vo.getOrderType()); + assertEquals("ALARM", vo.getSourceType()); + assertEquals("A栋3层入侵告警", vo.getTitle()); + assertEquals(PriorityEnum.P1.getPriority(), vo.getPriority()); + assertEquals(WorkOrderStatusEnum.COMPLETED.getStatus(), vo.getStatus()); + assertEquals(100L, vo.getAreaId()); + assertEquals("A栋3层东侧走廊", vo.getLocation()); + + // 验证扩展字段 + assertNotNull(vo.getExtInfo()); + Map extInfo = vo.getExtInfo(); + assertEquals("ALM20260310001", extInfo.get("alarmId")); + assertEquals("intrusion", extInfo.get("alarmType")); + assertEquals("CAM_001", extInfo.get("cameraId")); + assertEquals("ROI_001", extInfo.get("roiId")); + assertEquals("https://oss.example.com/snapshot.jpg", extInfo.get("imageUrl")); + assertEquals(2001L, extInfo.get("assignedUserId")); + assertEquals("张安保", extInfo.get("assignedUserName")); + assertEquals(301L, extInfo.get("assignedTeamId")); + assertEquals("已排查,系误报", extInfo.get("result")); + assertEquals("[\"https://oss/r1.jpg\"]", extInfo.get("resultImgUrls")); + assertEquals(testSecurityExt.getDispatchedTime(), extInfo.get("dispatchedTime")); + assertEquals(testSecurityExt.getConfirmedTime(), extInfo.get("confirmedTime")); + assertEquals(testSecurityExt.getCompletedTime(), extInfo.get("completedTime")); + } + + @Test + void testBuildDetailVO_NoExt_ExtInfoNull() { + when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(null); + + OrderDetailVO vo = handler.buildDetailVO(testOrder); + + // 主表字段正常 + assertNotNull(vo); + assertEquals(TEST_ORDER_ID, vo.getId()); + assertEquals("SECURITY", vo.getOrderType()); + + // 无扩展记录时,extInfo 未被填充 + assertTrue(vo.getExtInfo() == null || vo.getExtInfo().isEmpty()); + } +} diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceTest.java b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceTest.java new file mode 100644 index 0000000..8be3593 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/service/securityorder/SecurityOrderServiceTest.java @@ -0,0 +1,437 @@ +package com.viewsh.module.ops.security.service.securityorder; + +import com.viewsh.framework.common.exception.ServiceException; +import com.viewsh.module.ops.core.event.OrderCreatedEvent; +import com.viewsh.module.ops.core.event.OrderEventPublisher; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; +import com.viewsh.module.ops.enums.OperatorTypeEnum; +import com.viewsh.module.ops.enums.PriorityEnum; +import com.viewsh.module.ops.enums.SourceTypeEnum; +import com.viewsh.module.ops.enums.WorkOrderStatusEnum; +import com.viewsh.module.ops.infrastructure.code.OrderCodeGenerator; +import com.viewsh.module.ops.infrastructure.id.OrderIdGenerator; +import com.viewsh.module.ops.security.dal.dataobject.workorder.OpsOrderSecurityExtDO; +import com.viewsh.module.ops.security.dal.mysql.workorder.OpsOrderSecurityExtMapper; +import com.viewsh.module.ops.service.fsm.OrderStateMachine; +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 java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * 安保工单服务单元测试 + * + * @author lzh + */ +@ExtendWith(MockitoExtension.class) +public class SecurityOrderServiceTest { + + @InjectMocks + private SecurityOrderServiceImpl securityOrderService; + + @Mock + private OpsOrderMapper opsOrderMapper; + @Mock + private OpsOrderSecurityExtMapper securityExtMapper; + @Mock + private OrderIdGenerator orderIdGenerator; + @Mock + private OrderCodeGenerator orderCodeGenerator; + @Mock + private OrderEventPublisher orderEventPublisher; + @Mock + private OrderStateMachine orderStateMachine; + + // 模拟数据库 + private Map orderDB; + + private static final Long TEST_ORDER_ID = 10001L; + private static final String TEST_ORDER_CODE = "SECURITY-20260310-0001"; + + @BeforeEach + void setUp() { + orderDB = new HashMap<>(); + + // 配置 ID/编号 生成器 + lenient().when(orderIdGenerator.generate()).thenReturn(TEST_ORDER_ID); + lenient().when(orderCodeGenerator.generate("SECURITY")).thenReturn(TEST_ORDER_CODE); + + // 配置 Mapper 模拟 + lenient().when(opsOrderMapper.insert(any(OpsOrderDO.class))).thenAnswer(i -> { + OpsOrderDO order = i.getArgument(0); + orderDB.put(order.getId(), order); + return 1; + }); + lenient().when(opsOrderMapper.selectById(anyLong())).thenAnswer(i -> orderDB.get(i.getArgument(0))); + lenient().when(opsOrderMapper.updateById(any(OpsOrderDO.class))).thenReturn(1); + lenient().when(securityExtMapper.insert(any(OpsOrderSecurityExtDO.class))).thenReturn(1); + lenient().when(securityExtMapper.insertOrUpdateSelective(any(OpsOrderSecurityExtDO.class))).thenReturn(1); + } + + // ==================== 创建工单测试 ==================== + + @Test + void testCreateSecurityOrder_AlarmSource_Success() { + // 准备:告警来源请求 + SecurityOrderCreateReqDTO req = new SecurityOrderCreateReqDTO(); + req.setTitle("A栋3层入侵告警"); + req.setDescription("摄像头检测到异常人员"); + req.setPriority(PriorityEnum.P1.getPriority()); + req.setAreaId(100L); + req.setLocation("A栋3层东侧走廊"); + req.setAlarmId("ALM20260310001"); + req.setAlarmType("intrusion"); + req.setCameraId("CAM_001"); + req.setRoiId("ROI_001"); + req.setImageUrl("https://oss.example.com/alarm/snapshot.jpg"); + + // 执行 + Long orderId = securityOrderService.createSecurityOrder(req); + + // 验证返回值 + assertEquals(TEST_ORDER_ID, orderId); + + // 验证主表写入 + ArgumentCaptor orderCaptor = ArgumentCaptor.forClass(OpsOrderDO.class); + verify(opsOrderMapper).insert(orderCaptor.capture()); + OpsOrderDO savedOrder = orderCaptor.getValue(); + assertEquals(TEST_ORDER_ID, savedOrder.getId()); + assertEquals(TEST_ORDER_CODE, savedOrder.getOrderCode()); + assertEquals("SECURITY", savedOrder.getOrderType()); + assertEquals(SourceTypeEnum.ALARM.getType(), savedOrder.getSourceType()); + assertEquals("A栋3层入侵告警", savedOrder.getTitle()); + assertEquals(PriorityEnum.P1.getPriority(), savedOrder.getPriority()); + assertEquals(WorkOrderStatusEnum.PENDING.getStatus(), savedOrder.getStatus()); + assertEquals(100L, savedOrder.getAreaId()); + + // 验证扩展表写入 + ArgumentCaptor extCaptor = ArgumentCaptor.forClass(OpsOrderSecurityExtDO.class); + verify(securityExtMapper).insert(extCaptor.capture()); + OpsOrderSecurityExtDO savedExt = extCaptor.getValue(); + assertEquals(TEST_ORDER_ID, savedExt.getOpsOrderId()); + assertEquals("ALM20260310001", savedExt.getAlarmId()); + assertEquals("intrusion", savedExt.getAlarmType()); + assertEquals("CAM_001", savedExt.getCameraId()); + assertEquals("ROI_001", savedExt.getRoiId()); + assertEquals("https://oss.example.com/alarm/snapshot.jpg", savedExt.getImageUrl()); + + // 验证事件发布 + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(OrderCreatedEvent.class); + verify(orderEventPublisher).publishOrderCreated(eventCaptor.capture()); + OrderCreatedEvent event = eventCaptor.getValue(); + assertEquals(TEST_ORDER_ID, event.getOrderId()); + assertEquals("SECURITY", event.getOrderType()); + assertEquals(TEST_ORDER_CODE, event.getOrderCode()); + assertEquals(100L, event.getAreaId()); + } + + @Test + void testCreateSecurityOrder_ManualSource_Success() { + // 准备:手动创建(无告警ID) + SecurityOrderCreateReqDTO req = new SecurityOrderCreateReqDTO(); + req.setTitle("保安巡查发现异常"); + req.setAreaId(200L); + + // 执行 + Long orderId = securityOrderService.createSecurityOrder(req); + + // 验证来源类型为 MANUAL + ArgumentCaptor orderCaptor = ArgumentCaptor.forClass(OpsOrderDO.class); + verify(opsOrderMapper).insert(orderCaptor.capture()); + assertEquals(SourceTypeEnum.MANUAL.getType(), orderCaptor.getValue().getSourceType()); + } + + @Test + void testCreateSecurityOrder_ExplicitSourceType_Success() { + // 准备:显式指定来源类型 + SecurityOrderCreateReqDTO req = new SecurityOrderCreateReqDTO(); + req.setTitle("手动创建"); + req.setAreaId(200L); + req.setAlarmId("ALM001"); // 有告警ID + req.setSourceType("CUSTOM_SOURCE"); // 但显式指定了sourceType + + // 执行 + securityOrderService.createSecurityOrder(req); + + // 验证:显式指定的 sourceType 优先于自动推断 + ArgumentCaptor orderCaptor = ArgumentCaptor.forClass(OpsOrderDO.class); + verify(opsOrderMapper).insert(orderCaptor.capture()); + assertEquals("CUSTOM_SOURCE", orderCaptor.getValue().getSourceType()); + } + + @Test + void testCreateSecurityOrder_DefaultPriority_P2() { + // 准备:不设置优先级 + SecurityOrderCreateReqDTO req = new SecurityOrderCreateReqDTO(); + req.setTitle("默认优先级工单"); + req.setAreaId(100L); + + // 执行 + securityOrderService.createSecurityOrder(req); + + // 验证默认 P2 + ArgumentCaptor orderCaptor = ArgumentCaptor.forClass(OpsOrderDO.class); + verify(opsOrderMapper).insert(orderCaptor.capture()); + assertEquals(PriorityEnum.P2.getPriority(), orderCaptor.getValue().getPriority()); + } + + // ==================== 确认工单测试 ==================== + + @Test + void testConfirmOrder_Success() { + // 准备:已存在的 SECURITY 工单 + OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.DISPATCHED); + orderDB.put(TEST_ORDER_ID, order); + + Long userId = 2001L; + + // 执行 + securityOrderService.confirmOrder(TEST_ORDER_ID, userId); + + // 验证状态机调用 + verify(orderStateMachine).transition( + eq(order), + eq(WorkOrderStatusEnum.CONFIRMED), + eq(OperatorTypeEnum.SECURITY_GUARD), + eq(userId), + eq("安保人员确认接单") + ); + + // 验证不再直接写扩展表时间(由 EventListener 统一处理) + verify(securityExtMapper, never()).insertOrUpdateSelective(any()); + } + + @Test + void testConfirmOrder_OrderNotFound_ThrowsException() { + // 执行 + 验证:工单不存在抛异常 + ServiceException exception = assertThrows(ServiceException.class, + () -> securityOrderService.confirmOrder(999L, 2001L)); + assertTrue(exception.getMessage().contains("工单不存在")); + } + + @Test + void testConfirmOrder_WrongOrderType_ThrowsException() { + // 准备:CLEAN 类型工单 + OpsOrderDO cleanOrder = OpsOrderDO.builder() + .id(TEST_ORDER_ID) + .orderType("CLEAN") + .status(WorkOrderStatusEnum.DISPATCHED.getStatus()) + .build(); + orderDB.put(TEST_ORDER_ID, cleanOrder); + + // 执行 + 验证:类型不匹配抛异常 + ServiceException exception = assertThrows(ServiceException.class, + () -> securityOrderService.confirmOrder(TEST_ORDER_ID, 2001L)); + assertTrue(exception.getMessage().contains("工单类型不匹配")); + + // 验证状态机未被调用 + verify(orderStateMachine, never()).transition( + any(), any(), any(), anyLong(), anyString()); + } + + // ==================== 自动完单测试 ==================== + + @Test + void testAutoCompleteOrder_WithRemark_Success() { + // 准备 + OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.ARRIVED); + orderDB.put(TEST_ORDER_ID, order); + + // 执行 + securityOrderService.autoCompleteOrder(TEST_ORDER_ID, "告警自动解除"); + + // 验证状态机调用 + verify(orderStateMachine).transition( + eq(order), + eq(WorkOrderStatusEnum.COMPLETED), + eq(OperatorTypeEnum.SYSTEM), + isNull(), + eq("告警自动解除") + ); + + // 验证主表 endTime 更新 + ArgumentCaptor updateCaptor = ArgumentCaptor.forClass(OpsOrderDO.class); + verify(opsOrderMapper).updateById(updateCaptor.capture()); + OpsOrderDO updated = updateCaptor.getValue(); + assertEquals(TEST_ORDER_ID, updated.getId()); + assertNotNull(updated.getEndTime()); + + // 验证不再直接写扩展表时间 + verify(securityExtMapper, never()).insertOrUpdateSelective(any()); + } + + @Test + void testAutoCompleteOrder_WithoutRemark_DefaultReason() { + // 准备 + OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.ARRIVED); + orderDB.put(TEST_ORDER_ID, order); + + // 执行:remark 为空 + securityOrderService.autoCompleteOrder(TEST_ORDER_ID, null); + + // 验证使用默认备注 + verify(orderStateMachine).transition( + any(), eq(WorkOrderStatusEnum.COMPLETED), + eq(OperatorTypeEnum.SYSTEM), isNull(), + eq("系统自动完单") + ); + } + + @Test + void testAutoCompleteOrder_BlankRemark_DefaultReason() { + // 准备 + OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.ARRIVED); + orderDB.put(TEST_ORDER_ID, order); + + // 执行:remark 为空白字符串 + securityOrderService.autoCompleteOrder(TEST_ORDER_ID, " "); + + // 验证使用默认备注 + verify(orderStateMachine).transition( + any(), eq(WorkOrderStatusEnum.COMPLETED), + eq(OperatorTypeEnum.SYSTEM), isNull(), + eq("系统自动完单") + ); + } + + @Test + void testAutoCompleteOrder_OrderNotFound_ThrowsException() { + assertThrows(ServiceException.class, + () -> securityOrderService.autoCompleteOrder(999L, "test")); + } + + // ==================== 人工完单测试 ==================== + + @Test + void testManualCompleteOrder_WithImages_Success() { + // 准备 + OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.ARRIVED); + orderDB.put(TEST_ORDER_ID, order); + + // 准备完单请求,包含result和图片 + SecurityOrderCompleteReqDTO req = new SecurityOrderCompleteReqDTO(); + req.setOrderId(TEST_ORDER_ID); + req.setResult("已到现场排查,系误报"); + req.setResultImgUrls(Arrays.asList("https://oss/result1.jpg", "https://oss/result2.jpg")); + req.setOperatorId(2001L); + + // 执行 + securityOrderService.manualCompleteOrder(req); + + // 验证状态机调用 + verify(orderStateMachine).transition( + eq(order), + eq(WorkOrderStatusEnum.COMPLETED), + eq(OperatorTypeEnum.SECURITY_GUARD), + eq(2001L), + eq("安保人员提交处理结果") + ); + + // 验证扩展表:写入 result + resultImgUrls(不含 completedTime) + ArgumentCaptor extCaptor = ArgumentCaptor.forClass(OpsOrderSecurityExtDO.class); + verify(securityExtMapper).insertOrUpdateSelective(extCaptor.capture()); + OpsOrderSecurityExtDO extUpdate = extCaptor.getValue(); + assertEquals(TEST_ORDER_ID, extUpdate.getOpsOrderId()); + assertEquals("已到现场排查,系误报", extUpdate.getResult()); + assertNotNull(extUpdate.getResultImgUrls()); + assertTrue(extUpdate.getResultImgUrls().contains("result1.jpg")); + assertNull(extUpdate.getCompletedTime()); // 时间由 EventListener 写入 + + // 验证主表 endTime 更新 + ArgumentCaptor orderUpdateCaptor = ArgumentCaptor.forClass(OpsOrderDO.class); + verify(opsOrderMapper).updateById(orderUpdateCaptor.capture()); + assertNotNull(orderUpdateCaptor.getValue().getEndTime()); + } + + @Test + void testManualCompleteOrder_WithoutImages_Success() { + // 准备 + OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.ARRIVED); + orderDB.put(TEST_ORDER_ID, order); + + SecurityOrderCompleteReqDTO req = new SecurityOrderCompleteReqDTO(); + req.setOrderId(TEST_ORDER_ID); + req.setResult("已处理完毕"); + req.setResultImgUrls(null); // 无图片 + req.setOperatorId(2001L); + + // 执行 + securityOrderService.manualCompleteOrder(req); + + // 验证扩展表:result 有值,resultImgUrls 为 null + ArgumentCaptor extCaptor = ArgumentCaptor.forClass(OpsOrderSecurityExtDO.class); + verify(securityExtMapper).insertOrUpdateSelective(extCaptor.capture()); + assertEquals("已处理完毕", extCaptor.getValue().getResult()); + assertNull(extCaptor.getValue().getResultImgUrls()); + } + + @Test + void testManualCompleteOrder_EmptyImagesList_NoJsonWrite() { + // 准备 + OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.ARRIVED); + orderDB.put(TEST_ORDER_ID, order); + + SecurityOrderCompleteReqDTO req = new SecurityOrderCompleteReqDTO(); + req.setOrderId(TEST_ORDER_ID); + req.setResult("已处理"); + req.setResultImgUrls(Arrays.asList()); // 空列表 + req.setOperatorId(2001L); + + // 执行 + securityOrderService.manualCompleteOrder(req); + + // 验证:空列表不写入 resultImgUrls + ArgumentCaptor extCaptor = ArgumentCaptor.forClass(OpsOrderSecurityExtDO.class); + verify(securityExtMapper).insertOrUpdateSelective(extCaptor.capture()); + assertNull(extCaptor.getValue().getResultImgUrls()); + } + + // ==================== 查询测试 ==================== + + @Test + void testGetSecurityExt_Exists() { + OpsOrderSecurityExtDO ext = OpsOrderSecurityExtDO.builder() + .id(1L).opsOrderId(TEST_ORDER_ID).alarmId("ALM001").build(); + when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(ext); + + OpsOrderSecurityExtDO result = securityOrderService.getSecurityExt(TEST_ORDER_ID); + assertNotNull(result); + assertEquals("ALM001", result.getAlarmId()); + } + + @Test + void testGetSecurityExt_NotExists() { + when(securityExtMapper.selectByOpsOrderId(999L)).thenReturn(null); + + OpsOrderSecurityExtDO result = securityOrderService.getSecurityExt(999L); + assertNull(result); + } + + // ==================== 辅助方法 ==================== + + private OpsOrderDO buildSecurityOrder(Long orderId, WorkOrderStatusEnum status) { + return OpsOrderDO.builder() + .id(orderId) + .orderCode(TEST_ORDER_CODE) + .orderType("SECURITY") + .sourceType(SourceTypeEnum.ALARM.getType()) + .title("安保测试工单") + .priority(PriorityEnum.P1.getPriority()) + .status(status.getStatus()) + .areaId(100L) + .location("A栋3层") + .build(); + } +}