From 9750088ca6b198e867de024158d94d78aa572d11 Mon Sep 17 00:00:00 2001 From: lzh Date: Thu, 22 Jan 2026 23:55:05 +0800 Subject: [PATCH] test: Add comprehensive end-to-end tests for cleaning work order module --- .../cleanorder/CleanOrderEndToEndTest.java | 376 ++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderEndToEndTest.java diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderEndToEndTest.java b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderEndToEndTest.java new file mode 100644 index 0000000..b0f151b --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderEndToEndTest.java @@ -0,0 +1,376 @@ +package com.viewsh.module.ops.environment.service.cleanorder; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.viewsh.module.iot.api.device.IotDeviceControlApi; +import com.viewsh.module.ops.core.dispatch.DispatchEngine; +import com.viewsh.module.ops.core.dispatch.model.DispatchResult; +import com.viewsh.module.ops.core.event.OrderEventPublisher; +import com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager; +import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionRequest; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; +import com.viewsh.module.ops.enums.PriorityEnum; +import com.viewsh.module.ops.enums.WorkOrderStatusEnum; +import com.viewsh.module.ops.environment.dal.dataobject.workorder.OpsOrderCleanExtDO; +import com.viewsh.module.ops.environment.dal.mysql.workorder.OpsOrderCleanExtMapper; +import com.viewsh.module.ops.environment.integration.consumer.*; +import com.viewsh.framework.common.pojo.CommonResult; +import com.viewsh.module.ops.environment.integration.listener.CleanOrderEventListener; +import com.viewsh.module.ops.environment.service.cleanorder.CleanOrderServiceImpl; +import com.viewsh.module.ops.environment.service.voice.VoiceBroadcastService; +import com.viewsh.module.ops.infrastructure.code.OrderCodeGenerator; +import com.viewsh.module.ops.infrastructure.id.OrderIdGenerator; +import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder; +import com.viewsh.module.ops.api.queue.OrderQueueService; +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.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class CleanOrderEndToEndTest { + + // 核心组件 + @InjectMocks + private CleanOrderServiceImpl cleanOrderService; + + @InjectMocks + private CleanOrderCreateEventHandler createEventHandler; + + @InjectMocks + private CleanOrderArriveEventHandler arriveEventHandler; + + @InjectMocks + private CleanOrderCompleteEventHandler completeEventHandler; + + @InjectMocks + private CleanOrderConfirmEventHandler confirmEventHandler; + + @Mock + private CleanOrderEventListener cleanOrderEventListener; + + // 依赖 Mocks + @Mock + private OpsOrderMapper opsOrderMapper; + @Mock + private OpsOrderCleanExtMapper cleanExtMapper; + @Mock + private OrderIdGenerator orderIdGenerator; + @Mock + private OrderCodeGenerator orderCodeGenerator; + @Mock + private OrderEventPublisher orderEventPublisher; + @Mock + private OrderQueueService orderQueueService; + @Mock + private DispatchEngine dispatchEngine; + @Mock + private OrderLifecycleManager orderLifecycleManager; + @Mock + private IotDeviceControlApi iotDeviceControlApi; + @Mock + private StringRedisTemplate stringRedisTemplate; + @Mock + private ValueOperations valueOperations; + @Mock + private VoiceBroadcastService voiceBroadcastService; + @Mock + private EventLogRecorder eventLogRecorder; + + @Spy + private ObjectMapper objectMapper = new ObjectMapper(); + + // 模拟数据库 + private Map orderDB = new HashMap<>(); + private Map cleanExtDB = new HashMap<>(); + + @BeforeEach + void setUp() { + // 配置 Redis Mock + lenient().when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations); + lenient().when(valueOperations.setIfAbsent(anyString(), anyString(), anyLong(), any(TimeUnit.class))) + .thenReturn(true); + + // 配置 ID 生成器 + lenient().when(orderIdGenerator.generate()).thenAnswer(i -> System.currentTimeMillis()); + lenient().when(orderCodeGenerator.generate(anyString())).thenAnswer(i -> "WO" + System.currentTimeMillis()); + + // 配置 Mapper 模拟 (基础 CRUD) + lenient().when(opsOrderMapper.insert(any(OpsOrderDO.class))).thenAnswer(i -> { + OpsOrderDO order = i.getArgument(0); + if (order.getId() == null) { + order.setId(System.currentTimeMillis()); + } + 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))).thenAnswer(i -> { + OpsOrderDO update = i.getArgument(0); + OpsOrderDO existing = orderDB.get(update.getId()); + if (existing != null) { + if (update.getStatus() != null) existing.setStatus(update.getStatus()); + if (update.getAssigneeId() != null) existing.setAssigneeId(update.getAssigneeId()); + if (update.getAssigneeDeviceId() != null) existing.setAssigneeDeviceId(update.getAssigneeDeviceId()); + if (update.getAssigneeDeviceKey() != null) existing.setAssigneeDeviceKey(update.getAssigneeDeviceKey()); + } + return 1; + }); + + lenient().when(cleanExtMapper.insert(any(OpsOrderCleanExtDO.class))).thenAnswer(i -> { + OpsOrderCleanExtDO ext = i.getArgument(0); + cleanExtDB.put(ext.getOpsOrderId(), ext); + return 1; + }); + + // 注入 CleanOrderEventListener + injectField(cleanOrderService, "cleanOrderEventListener", cleanOrderEventListener); + + // Stub IotDeviceControlApi for resetTrafficCounter + lenient().when(iotDeviceControlApi.resetTrafficCounter(any())) + .thenReturn(CommonResult.success(true)); + } + + // ========================================== + // TR-01 ~ TR-03: 工单自动触发与创建测试 + // ========================================== + + @Test + void testTR01_AutoCreateFromTraffic() throws Exception { + injectField(createEventHandler, "cleanOrderService", cleanOrderService); + injectField(cleanOrderService, "opsOrderMapper", opsOrderMapper); + injectField(cleanOrderService, "cleanExtMapper", cleanExtMapper); + injectField(cleanOrderService, "orderIdGenerator", orderIdGenerator); + injectField(cleanOrderService, "orderCodeGenerator", orderCodeGenerator); + injectField(cleanOrderService, "orderEventPublisher", orderEventPublisher); + + // 准备数据 + String eventJson = "{" + + "\"eventId\":\"evt-001\"," + + "\"areaId\":101," + + "\"triggerSource\":\"IOT_TRAFFIC\"," + + "\"triggerDeviceId\":8001," + + "\"triggerDeviceKey\":\"traffic-cam-01\"," + + "\"triggerData\":{\"actualCount\":150,\"baseValue\":1000}" + + "}"; + + // 执行 + createEventHandler.onMessage(eventJson); + + // 验证 + // 1. 验证工单创建 + ArgumentCaptor orderCaptor = ArgumentCaptor.forClass(OpsOrderDO.class); + verify(opsOrderMapper).insert((OpsOrderDO) orderCaptor.capture()); + OpsOrderDO order = orderCaptor.getValue(); + assertEquals(101L, order.getAreaId()); + assertEquals("TRAFFIC", order.getSourceType()); + assertEquals("IOT_TRAFFIC", order.getTriggerSource()); + + // 2. 验证扩展表创建 + verify(cleanExtMapper).insert(any(OpsOrderCleanExtDO.class)); + + // 3. 验证计数器重置调用 + verify(iotDeviceControlApi).resetTrafficCounter(any()); + } + + @Test + void testTR02_Idempotency() throws Exception { + injectField(createEventHandler, "cleanOrderService", cleanOrderService); + + // 模拟 Redis 第一次返回 true,第二次返回 false + when(valueOperations.setIfAbsent(anyString(), eq("1"), anyLong(), any(TimeUnit.class))) + .thenReturn(true) + .thenReturn(false); + + String eventJson = "{\"eventId\":\"evt-duplicate\",\"areaId\":101,\"triggerSource\":\"IOT_TRAFFIC\"}"; + + // 第一次调用 + createEventHandler.onMessage(eventJson); + // 第二次调用 + createEventHandler.onMessage(eventJson); + + // 验证只创建了一次工单 + verify(opsOrderMapper, times(1)).insert(any(OpsOrderDO.class)); + } + + // ========================================== + // DP-01 ~ DP-03: 智能调度测试 (模拟) + // ========================================== + + @Test + void testDP03_P0UrgentInterrupt() { + injectField(cleanOrderService, "dispatchEngine", dispatchEngine); + injectField(cleanOrderService, "opsOrderMapper", opsOrderMapper); + + // 准备一个 P0 紧急工单 + Long orderId = 999L; + Long cleanerId = 2001L; + + OpsOrderDO order = OpsOrderDO.builder() + .id(orderId) + .priority(PriorityEnum.P0.getPriority()) + .build(); + orderDB.put(orderId, order); + + // 模拟调度引擎返回成功 + when(dispatchEngine.urgentInterrupt(eq(orderId), eq(cleanerId))) + .thenReturn(DispatchResult.success("P0插队成功", cleanerId)); + + // 执行派单 + cleanOrderService.enqueueAndDispatch(orderId, cleanerId, true); + + // 验证 + verify(dispatchEngine).urgentInterrupt(eq(orderId), eq(cleanerId)); + } + + // ========================================== + // AV-01 ~ AV-03: 到岗校验测试 + // ========================================== + + @Test + void testAV01_BeaconAutoArrive() throws Exception { + injectField(arriveEventHandler, "orderLifecycleManager", orderLifecycleManager); + + // 准备工单:状态 DISPATCHED + Long orderId = 1001L; + OpsOrderDO order = OpsOrderDO.builder() + .id(orderId) + .status(WorkOrderStatusEnum.DISPATCHED.getStatus()) + .assigneeId(2001L) + .build(); + orderDB.put(orderId, order); + + String eventJson = "{" + + "\"eventId\":\"evt-arrive-01\"," + + "\"orderId\":1001," + + "\"deviceId\":5001," + + "\"deviceKey\":\"badge-01\"," + + "\"areaId\":101," + + "\"triggerSource\":\"IOT_BEACON\"," + + "\"triggerData\":{\"beaconMac\":\"F0:C8:60:1D:10:BB\"}" + + "}"; + + // 执行 + arriveEventHandler.onMessage(eventJson); + + // 验证 + // 1. 验证设备ID更新 + ArgumentCaptor orderCaptor = ArgumentCaptor.forClass(OpsOrderDO.class); + verify(opsOrderMapper).updateById((OpsOrderDO) orderCaptor.capture()); + OpsOrderDO updatedOrder = orderCaptor.getValue(); + assertEquals(orderId, updatedOrder.getId()); + assertEquals(5001L, updatedOrder.getAssigneeDeviceId()); + + // 2. 验证调用了生命周期管理器的 transition + ArgumentCaptor reqCaptor = ArgumentCaptor.forClass(OrderTransitionRequest.class); + verify(orderLifecycleManager).transition(reqCaptor.capture()); + OrderTransitionRequest req = reqCaptor.getValue(); + assertEquals(orderId, req.getOrderId()); + assertEquals(WorkOrderStatusEnum.ARRIVED, req.getTargetStatus()); + assertTrue(req.getReason().contains("自动到岗确认")); + + // 3. 验证 Redis 缓存更新 + verify(valueOperations).set(contains("ops:clean:device:order:5001"), anyString(), anyLong(), any(TimeUnit.class)); + } + + @Test + void testAV02_StatusInterceptor() throws Exception { + // 准备工单:状态 PENDING (不允许到岗) + Long orderId = 1002L; + OpsOrderDO order = OpsOrderDO.builder() + .id(orderId) + .status(WorkOrderStatusEnum.PENDING.getStatus()) + .build(); + orderDB.put(orderId, order); + + String eventJson = "{\"eventId\":\"evt-arrive-02\",\"orderId\":1002,\"deviceId\":5001}"; + + // 执行 + arriveEventHandler.onMessage(eventJson); + + // 验证:不应该调用 transition + verify(orderLifecycleManager, never()).transition(any()); + } + + // ========================================== + // CP-01 ~ CP-03: 离岗与结单测试 + // ========================================== + + @Test + void testCP01_SignalLossAutoComplete() throws Exception { + injectField(completeEventHandler, "orderLifecycleManager", orderLifecycleManager); + injectField(completeEventHandler, "cleanOrderService", cleanOrderService); + injectField(cleanOrderService, "dispatchEngine", dispatchEngine); + + // 准备工单:状态 ARRIVED + Long orderId = 1003L; + OpsOrderDO order = OpsOrderDO.builder() + .id(orderId) + .status(WorkOrderStatusEnum.ARRIVED.getStatus()) + .assigneeId(2001L) + .build(); + orderDB.put(orderId, order); + + String eventJson = "{" + + "\"eventId\":\"evt-complete-01\"," + + "\"orderId\":1003," + + "\"deviceId\":5001," + + "\"triggerSource\":\"IOT_SIGNAL_LOSS\"," + + "\"triggerData\":{\"durationMs\":1800000}" + + "}"; + + // 模拟 autoDispatchNext 调用成功 + when(dispatchEngine.autoDispatchNext(eq(orderId), eq(2001L))) + .thenReturn(DispatchResult.success("Success", 2001L)); + + // 执行 + completeEventHandler.onMessage(eventJson); + + // 验证 + // 1. 验证调用了 completeOrder + verify(orderLifecycleManager).completeOrder(eq(orderId), eq(null), contains("信号丢失超时")); + + // 2. 验证清理了 Redis 缓存 + verify(stringRedisTemplate).delete(contains("ops:clean:device:order:5001")); + + // 3. 验证触发了自动调度下一单 + verify(dispatchEngine).autoDispatchNext(eq(orderId), eq(2001L)); + } + + // 简单的反射注入辅助方法 + private void injectField(Object target, String fieldName, Object value) { + try { + java.lang.reflect.Field field = getField(target.getClass(), fieldName); + field.setAccessible(true); + field.set(target, value); + } catch (Exception e) { + throw new RuntimeException("Failed to inject field: " + fieldName, e); + } + } + + private java.lang.reflect.Field getField(Class clazz, String fieldName) throws NoSuchFieldException { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + if (clazz.getSuperclass() != null) { + return getField(clazz.getSuperclass(), fieldName); + } + throw e; + } + } +}