|
@@ -0,0 +1,246 @@
|
|
|
|
|
+package com.ruoyi.app.order;
|
|
|
|
|
+
|
|
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
|
|
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
|
|
|
|
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
|
|
+import com.ruoyi.app.order.dto.SelfDeliveryOperateInput;
|
|
|
|
|
+import com.ruoyi.app.pay.LinePayOrderGuard;
|
|
|
|
|
+import com.ruoyi.app.push.BusinessPushService;
|
|
|
|
|
+import com.ruoyi.system.utils.AuthContext;
|
|
|
|
|
+import com.ruoyi.app.service.UserService;
|
|
|
|
|
+import com.ruoyi.app.utils.PayPush;
|
|
|
|
|
+import com.ruoyi.app.utils.event.PushEventService;
|
|
|
|
|
+import com.ruoyi.common.core.domain.model.LoginUser;
|
|
|
|
|
+import com.ruoyi.common.core.domain.entity.SysUser;
|
|
|
|
|
+import com.ruoyi.common.utils.MessageUtils;
|
|
|
|
|
+import com.ruoyi.common.utils.SecurityUtils;
|
|
|
|
|
+import com.ruoyi.framework.manager.AsyncManager;
|
|
|
|
|
+import com.ruoyi.system.domain.InfoUser;
|
|
|
|
|
+import com.ruoyi.system.domain.PosOrder;
|
|
|
|
|
+import com.ruoyi.system.service.IInfoUserService;
|
|
|
|
|
+import com.ruoyi.system.service.IPosOrderService;
|
|
|
|
|
+import com.ruoyi.system.utils.JwtUtil;
|
|
|
|
|
+import com.ruoyi.system.utils.OrderLogHelper;
|
|
|
|
|
+import org.apache.ibatis.builder.MapperBuilderAssistant;
|
|
|
|
|
+import org.junit.jupiter.api.*;
|
|
|
|
|
+import org.junit.jupiter.params.ParameterizedTest;
|
|
|
|
|
+import org.junit.jupiter.params.provider.ValueSource;
|
|
|
|
|
+import org.mockito.MockedStatic;
|
|
|
|
|
+import org.redisson.api.RLock;
|
|
|
|
|
+import org.redisson.api.RedissonClient;
|
|
|
|
|
+import org.springframework.test.util.ReflectionTestUtils;
|
|
|
|
|
+import org.springframework.transaction.support.TransactionSynchronizationManager;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
+
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.*;
|
|
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
|
|
+
|
|
|
|
|
+/** Capture real controller payloads at the push boundary; asynchronous work runs deterministically. */
|
|
|
|
|
+class UserOrderNotificationSceneTest {
|
|
|
|
|
+ private final IPosOrderService orders = mock(IPosOrderService.class);
|
|
|
|
|
+ private final IInfoUserService users = mock(IInfoUserService.class);
|
|
|
|
|
+ private final OrderLifecycleService lifecycle = mock(OrderLifecycleService.class);
|
|
|
|
|
+ private final MerchantNotificationRouter merchants = mock(MerchantNotificationRouter.class);
|
|
|
|
|
+ private final BusinessPushService channel = mock(BusinessPushService.class);
|
|
|
|
|
+ private final List<JSONObject> userBodies = new ArrayList<>();
|
|
|
|
|
+ private final List<JSONObject> riderBodies = new ArrayList<>();
|
|
|
|
|
+ private final List<MockedStatic<?>> statics = new ArrayList<>();
|
|
|
|
|
+ private final PosOrder order = new PosOrder();
|
|
|
|
|
+ private final InfoUser user = new InfoUser();
|
|
|
|
|
+ private RedissonClient redis;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ void setUp() throws Exception {
|
|
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), PosOrder.class);
|
|
|
|
|
+ order.setId(10L); order.setDdId("ORDER-1"); order.setUserId(7L); order.setState(2L);
|
|
|
|
|
+ order.setDeliveryStatus(0L); order.setMdId(20L); order.setShId(101L);
|
|
|
|
|
+ order.setType(0L); order.setPayStatus(1L); order.setSelfDelivery(1);
|
|
|
|
|
+ user.setUserId(7L); user.setUserType("0");
|
|
|
|
|
+ when(orders.getById(10L)).thenReturn(order);
|
|
|
|
|
+ when(orders.getOne(any())).thenReturn(order);
|
|
|
|
|
+ when(orders.saveOrUpdate(any(PosOrder.class))).thenReturn(true);
|
|
|
|
|
+ when(orders.update(any(PosOrder.class), any())).thenReturn(true);
|
|
|
|
|
+ when(orders.list(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class))).thenReturn(List.of());
|
|
|
|
|
+ when(users.getById(7L)).thenReturn(user);
|
|
|
|
|
+ when(channel.addressKey(any())).thenReturn("address");
|
|
|
|
|
+ RLock lock = mock(RLock.class);
|
|
|
|
|
+ when(lock.tryLock(anyLong(), anyLong(), eq(TimeUnit.SECONDS))).thenReturn(true);
|
|
|
|
|
+ redis = mock(RedissonClient.class); when(redis.getLock(anyString())).thenReturn(lock);
|
|
|
|
|
+ MockedStatic<com.ruoyi.common.utils.spring.SpringUtils> spring = mockStatic(com.ruoyi.common.utils.spring.SpringUtils.class);
|
|
|
|
|
+ statics.add(spring);
|
|
|
|
|
+ spring.when(() -> com.ruoyi.common.utils.spring.SpringUtils.getBean("scheduledExecutorService"))
|
|
|
|
|
+ .thenReturn(mock(java.util.concurrent.ScheduledExecutorService.class));
|
|
|
|
|
+ AsyncManager manager = mock(AsyncManager.class);
|
|
|
|
|
+ doAnswer(call -> { ((TimerTask) call.getArgument(0)).run(); return null; }).when(manager).execute(any(TimerTask.class));
|
|
|
|
|
+ MockedStatic<AsyncManager> async = mockStatic(AsyncManager.class);
|
|
|
|
|
+ async.when(AsyncManager::me).thenReturn(manager); statics.add(async);
|
|
|
|
|
+ MockedStatic<PayPush> push = mockStatic(PayPush.class); statics.add(push);
|
|
|
|
|
+ push.when(() -> PayPush.userPushHandleLocal(any(), any(), any(), nullable(String.class),
|
|
|
|
|
+ anyString(), anyString(), anyString(), anyString(), anyString()))
|
|
|
|
|
+ .thenAnswer(call -> { userBodies.add(JSON.parseObject(call.getArgument(6, String.class))); return null; });
|
|
|
|
|
+ push.when(() -> PayPush.qsPushHandleLocal(any(), any(), any(), nullable(String.class),
|
|
|
|
|
+ anyString(), anyString(), anyString(), anyString(), anyString()))
|
|
|
|
|
+ .thenAnswer(call -> { riderBodies.add(JSON.parseObject(call.getArgument(6, String.class))); return null; });
|
|
|
|
|
+ MockedStatic<MessageUtils> messages = mockStatic(MessageUtils.class); statics.add(messages);
|
|
|
|
|
+ messages.when(() -> MessageUtils.message(anyString())).thenAnswer(call -> call.getArgument(0));
|
|
|
|
|
+ MockedStatic<SecurityUtils> security = mockStatic(SecurityUtils.class); statics.add(security);
|
|
|
|
|
+ LoginUser login = new LoginUser(); login.setUser(new SysUser());
|
|
|
|
|
+ security.when(SecurityUtils::getLoginUser).thenReturn(login);
|
|
|
|
|
+ MockedStatic<AuthContext> auth = mockStatic(AuthContext.class); statics.add(auth);
|
|
|
|
|
+ auth.when(AuthContext::requireUserId).thenReturn(101L);
|
|
|
|
|
+ InfoUser owner = new InfoUser(); owner.setUserId(101L); owner.setUserType("3");
|
|
|
|
|
+ when(users.getOne(any())).thenReturn(owner);
|
|
|
|
|
+ TransactionSynchronizationManager.initSynchronization();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @AfterEach
|
|
|
|
|
+ void tearDown() {
|
|
|
|
|
+ TransactionSynchronizationManager.clearSynchronization();
|
|
|
|
|
+ Collections.reverse(statics); statics.forEach(MockedStatic::close);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private <T> T wire(T controller) {
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "posOrderService", orders);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "infoUserService", users);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "businessPushService", channel);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "pushEventService", mock(PushEventService.class));
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "redissonClient", redis);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "orderLogHelper", mock(OrderLogHelper.class));
|
|
|
|
|
+ return controller;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void commit() {
|
|
|
|
|
+ TransactionSynchronizationManager.getSynchronizations().forEach(sync -> sync.afterCommit());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @ParameterizedTest
|
|
|
|
|
+ @ValueSource(longs = {1, 2, 3})
|
|
|
|
|
+ void modernRiderSuccessPreservesStateAndSelectsScene(long status) {
|
|
|
|
|
+ PosOrderQsOprateController controller = wire(new PosOrderQsOprateController());
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "orderLifecycleService", lifecycle);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "merchantNotificationRouter", merchants);
|
|
|
|
|
+ PosOrder update = new PosOrder(); update.setId(10L); update.setDeliveryStatus(status);
|
|
|
|
|
+ ReflectionTestUtils.invokeMethod(controller, "setOrderQsState", update, "88", new PayPush(), "rider");
|
|
|
|
|
+ assertTrue(userBodies.isEmpty()); commit();
|
|
|
|
|
+ assertEquals(1, userBodies.size());
|
|
|
|
|
+ assertEquals(List.of("RIDER_ACCEPTED", "ORDER_DELIVERING", "ORDER_DELIVERED").get((int) status - 1),
|
|
|
|
|
+ userBodies.get(0).getString("eventScene"));
|
|
|
|
|
+ assertEquals("2", userBodies.get(0).getString("state"));
|
|
|
|
|
+ if (status == 1) {
|
|
|
|
|
+ var body = org.mockito.ArgumentCaptor.forClass(String.class);
|
|
|
|
|
+ verify(merchants).sendStoreNotification(eq(20L), eq(101L), anyString(), anyString(), body.capture(), eq("ORDER-1"));
|
|
|
|
|
+ assertFalse(JSON.parseObject(body.getValue()).containsKey("eventScene"));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @ParameterizedTest
|
|
|
|
|
+ @ValueSource(longs = {3, 4, 12})
|
|
|
|
|
+ void legacyRiderSuccessPreservesLegacyStateAndSelectsScene(long state) {
|
|
|
|
|
+ PosOrderController controller = wire(new PosOrderController());
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "userService", mock(UserService.class));
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "merchantNotificationRouter", merchants);
|
|
|
|
|
+ PosOrder update = new PosOrder(); update.setId(10L); update.setState(state);
|
|
|
|
|
+ ReflectionTestUtils.invokeMethod(controller, "setOrderQsState", update, "88", new PayPush());
|
|
|
|
|
+ assertTrue(userBodies.isEmpty()); commit();
|
|
|
|
|
+ assertEquals(1, userBodies.size());
|
|
|
|
|
+ assertEquals(state == 3 ? "RIDER_ACCEPTED" : state == 4 ? "ORDER_DELIVERING" : "ORDER_DELIVERED",
|
|
|
|
|
+ userBodies.get(0).getString("eventScene"));
|
|
|
|
|
+ assertEquals(String.valueOf(state), userBodies.get(0).getString("state"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void noAddressStillReachesUserInboxBoundaryAndRollbackDoesNotNotify() {
|
|
|
|
|
+ when(channel.addressKey(any())).thenReturn(null);
|
|
|
|
|
+ PosOrderQsOprateController controller = wire(new PosOrderQsOprateController());
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "orderLifecycleService", lifecycle);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "merchantNotificationRouter", merchants);
|
|
|
|
|
+ PosOrder update = new PosOrder(); update.setId(10L); update.setDeliveryStatus(2L);
|
|
|
|
|
+ ReflectionTestUtils.invokeMethod(controller, "setOrderQsState", update, "88", new PayPush(), "rider");
|
|
|
|
|
+ TransactionSynchronizationManager.getSynchronizations().forEach(sync -> sync.afterCompletion(1));
|
|
|
|
|
+ assertTrue(userBodies.isEmpty());
|
|
|
|
|
+ // A separate successful transaction must not use device availability as an inbox prerequisite.
|
|
|
|
|
+ TransactionSynchronizationManager.clearSynchronization(); TransactionSynchronizationManager.initSynchronization();
|
|
|
|
|
+ ReflectionTestUtils.invokeMethod(controller, "setOrderQsState", update, "88", new PayPush(), "rider");
|
|
|
|
|
+ commit(); assertEquals(1, userBodies.size());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void rejectedNotificationSchedulingDoesNotEscapeCommittedRiderOperation() {
|
|
|
|
|
+ PosOrderQsOprateController controller = wire(new PosOrderQsOprateController());
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "orderLifecycleService", lifecycle);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "merchantNotificationRouter", merchants);
|
|
|
|
|
+ AsyncManager manager = AsyncManager.me();
|
|
|
|
|
+ doThrow(new java.util.concurrent.RejectedExecutionException("closed executor"))
|
|
|
|
|
+ .when(manager).execute(any(TimerTask.class));
|
|
|
|
|
+ PosOrder update = new PosOrder(); update.setId(10L); update.setDeliveryStatus(2L);
|
|
|
|
|
+ ReflectionTestUtils.invokeMethod(controller, "setOrderQsState", update, "88", new PayPush(), "rider");
|
|
|
|
|
+ assertDoesNotThrow(this::commit);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void failedLifecycleDoesNotRegisterSuccessNotification() {
|
|
|
|
|
+ PosOrderQsOprateController controller = wire(new PosOrderQsOprateController());
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "orderLifecycleService", lifecycle);
|
|
|
|
|
+ doThrow(new IllegalStateException("failed update")).when(lifecycle).acceptDeliveryByRider(10L, 88L);
|
|
|
|
|
+ PosOrder update = new PosOrder(); update.setId(10L); update.setDeliveryStatus(1L);
|
|
|
|
|
+ assertThrows(IllegalStateException.class, () -> ReflectionTestUtils.invokeMethod(controller,
|
|
|
|
|
+ "setOrderQsState", update, "88", new PayPush(), "rider"));
|
|
|
|
|
+ commit(); assertTrue(userBodies.isEmpty());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void selfDeliveryDirectCompletionOnlyNotifiesDeliveredAfterCommit() {
|
|
|
|
|
+ PosOrderShOprateController controller = wire(new PosOrderShOprateController());
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "linePayOrderGuard", mock(LinePayOrderGuard.class));
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "orderService", mock(OrderService.class));
|
|
|
|
|
+ SelfDeliveryOperateInput input = new SelfDeliveryOperateInput(); input.setId(10L);
|
|
|
|
|
+ controller.selfDeliveryComplete(JwtUtil.token("101", "owner"), input);
|
|
|
|
|
+ assertTrue(userBodies.isEmpty()); commit();
|
|
|
|
|
+ assertEquals(1, userBodies.size());
|
|
|
|
|
+ assertEquals("ORDER_DELIVERED", userBodies.get(0).getString("eventScene"));
|
|
|
|
|
+ assertTrue(riderBodies.isEmpty());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void merchantAcceptanceAndSelfDeliveryStartHaveDistinctScenes() {
|
|
|
|
|
+ PosOrderShOprateController controller = wire(new PosOrderShOprateController());
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "linePayOrderGuard", mock(LinePayOrderGuard.class));
|
|
|
|
|
+ order.setState(0L);
|
|
|
|
|
+ controller.acceptOrder(JwtUtil.token("101", "owner"), 10L);
|
|
|
|
|
+ assertTrue(userBodies.isEmpty()); commit();
|
|
|
|
|
+ assertEquals("MERCHANT_ACCEPTED", userBodies.get(0).getString("eventScene"));
|
|
|
|
|
+ TransactionSynchronizationManager.clearSynchronization(); TransactionSynchronizationManager.initSynchronization();
|
|
|
|
|
+ userBodies.clear(); order.setState(2L);
|
|
|
|
|
+ SelfDeliveryOperateInput input = new SelfDeliveryOperateInput(); input.setId(10L);
|
|
|
|
|
+ controller.selfDeliveryStart(JwtUtil.token("101", "owner"), input);
|
|
|
|
|
+ assertTrue(userBodies.isEmpty()); commit();
|
|
|
|
|
+ assertEquals("ORDER_DELIVERING", userBodies.get(0).getString("eventScene"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void failedMerchantPersistenceDoesNotNotifyAcceptedOrReady() {
|
|
|
|
|
+ PosOrderShOprateController controller = wire(new PosOrderShOprateController());
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "linePayOrderGuard", mock(LinePayOrderGuard.class));
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "orderInvoiceService", mock(OrderInvoiceService.class));
|
|
|
|
|
+ when(orders.saveOrUpdate(any(PosOrder.class))).thenReturn(false);
|
|
|
|
|
+ when(users.list(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class))).thenReturn(List.of(user));
|
|
|
|
|
+ order.setState(0L); controller.acceptOrder(JwtUtil.token("101", "owner"), 10L);
|
|
|
|
|
+ order.setState(1L); controller.dispatchOrder(JwtUtil.token("101", "owner"), 10L);
|
|
|
|
|
+ commit(); assertTrue(userBodies.isEmpty());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void readyNotificationOnlyAddsSceneToUserAndWaitsForCommit() {
|
|
|
|
|
+ PosOrderShOprateController controller = wire(new PosOrderShOprateController());
|
|
|
|
|
+ InfoUser rider = new InfoUser(); rider.setUserId(88L); rider.setUserType("2");
|
|
|
|
|
+ when(users.list(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class))).thenReturn(List.of(user, rider)); order.setQsId(88L);
|
|
|
|
|
+ controller.chuCan(order, order);
|
|
|
|
|
+ assertTrue(userBodies.isEmpty()); assertTrue(riderBodies.isEmpty()); commit();
|
|
|
|
|
+ assertEquals("MERCHANT_READY", userBodies.get(0).getString("eventScene"));
|
|
|
|
|
+ assertEquals(1, riderBodies.size()); assertFalse(riderBodies.get(0).containsKey("eventScene"));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|