package com.ruoyi.app.flashdelivery.service; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.ruoyi.app.push.ApnsPushSender; import com.ruoyi.app.push.BusinessPushService; import com.ruoyi.app.push.FcmPushSender; import com.ruoyi.app.utils.PayPush; import com.ruoyi.app.utils.event.PushEvent; import com.ruoyi.app.utils.event.PushEventListener; import com.ruoyi.common.utils.LocaleUtils; import com.ruoyi.common.utils.spring.SpringUtils; import com.ruoyi.framework.manager.AsyncManager; import com.ruoyi.system.domain.PushMessage; import com.ruoyi.system.domain.flash.FlashDeliveryOrderStop; import com.ruoyi.system.service.IInfoUserService; import com.ruoyi.system.service.IPushMessageService; import com.ruoyi.app.utils.event.PushEventService; import com.ruoyi.app.order.dto.OrderPushBodyDto; import com.ruoyi.app.push.BusinessPushProperties; import com.ruoyi.app.push.NativeRiderBatchPushService; import com.ruoyi.app.push.UserNotificationScene; import com.ruoyi.system.domain.InfoUser; import com.ruoyi.system.domain.RiderPosition; import com.ruoyi.common.core.domain.entity.SysDictData; import com.ruoyi.system.domain.flash.FlashDeliveryOrder; import com.ruoyi.system.mapper.InfoUserMapper; import com.ruoyi.system.mapper.RiderPositionMapper; import com.ruoyi.common.utils.DictUtils; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.MessageSource; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.math.BigDecimal; import java.util.List; import java.util.ArrayList; import java.util.Locale; import java.util.Set; import java.util.TimerTask; import java.util.concurrent.ScheduledExecutorService; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mockStatic; class FlashDeliveryNotificationServiceTest { @Test void successfulUserNotificationsCarryScenesWithoutDeviceAddressAfterCommit() { InfoUserMapper users = mock(InfoUserMapper.class); InfoUser sender = new InfoUser(); sender.setUserId(7L); when(users.selectById(7L)).thenReturn(sender); FlashDeliveryNotificationService service = new FlashDeliveryNotificationService(users, mock(PushEventService.class)); java.util.List bodies = new java.util.ArrayList<>(); try (var spring = org.mockito.Mockito.mockStatic(com.ruoyi.common.utils.spring.SpringUtils.class)) { spring.when(() -> com.ruoyi.common.utils.spring.SpringUtils.getBean("scheduledExecutorService")) .thenReturn(mock(java.util.concurrent.ScheduledExecutorService.class)); com.ruoyi.framework.manager.AsyncManager manager = mock(com.ruoyi.framework.manager.AsyncManager.class); org.mockito.Mockito.doAnswer(call -> { ((java.util.TimerTask) call.getArgument(0)).run(); return null; }) .when(manager).execute(any(java.util.TimerTask.class)); try (var async = org.mockito.Mockito.mockStatic(com.ruoyi.framework.manager.AsyncManager.class); var push = org.mockito.Mockito.mockStatic(com.ruoyi.app.utils.PayPush.class)) { async.when(com.ruoyi.framework.manager.AsyncManager::me).thenReturn(manager); push.when(() -> com.ruoyi.app.utils.PayPush.userPushHandleLocal(any(), any(), eq(7L), org.mockito.ArgumentMatchers.isNull(), anyString(), anyString(), anyString(), anyString(), anyString())) .thenAnswer(call -> { bodies.add(com.alibaba.fastjson.JSON.parseObject(call.getArgument(6, String.class))); return null; }); org.springframework.transaction.support.TransactionSynchronizationManager.initSynchronization(); try { service.notifyAccepted(order()); service.notifyPickedUp(order()); service.notifyDelivered(order()); org.junit.jupiter.api.Assertions.assertTrue(bodies.isEmpty()); org.springframework.transaction.support.TransactionSynchronizationManager.getSynchronizations() .forEach(sync -> sync.afterCommit()); org.junit.jupiter.api.Assertions.assertEquals(List.of("RIDER_ACCEPTED", "ORDER_DELIVERING", "ORDER_DELIVERED"), bodies.stream().map(body -> body.getString("eventScene")).toList()); bodies.forEach(body -> org.junit.jupiter.api.Assertions.assertEquals(3, body.getIntValue("pushType"))); } finally { org.springframework.transaction.support.TransactionSynchronizationManager.clearSynchronization(); } } } } @Test void rollbackSkipsLookupAndPreparationFailureDoesNotEscapeBusinessOperation() { InfoUserMapper users = mock(InfoUserMapper.class); FlashDeliveryNotificationService service = new FlashDeliveryNotificationService(users, mock(PushEventService.class)); org.springframework.transaction.support.TransactionSynchronizationManager.initSynchronization(); try { service.notifyAccepted(order()); org.springframework.transaction.support.TransactionSynchronizationManager.getSynchronizations() .forEach(sync -> sync.afterCompletion(1)); verify(users, never()).selectById(any()); } finally { org.springframework.transaction.support.TransactionSynchronizationManager.clearSynchronization(); } when(users.selectById(7L)).thenThrow(new IllegalStateException("push preparation failed")); org.junit.jupiter.api.Assertions.assertDoesNotThrow(() -> service.notifyAccepted(order())); } @Test void availableOrderRoutesNewFlashOrderToNearbyEligibleRiders() { InfoUserMapper userMapper = mock(InfoUserMapper.class); RiderPositionMapper positions = mock(RiderPositionMapper.class); FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService( userMapper, mock(PushEventService.class), positions)); RiderPosition rider = new RiderPosition(); rider.setRiderId(9L); rider.setCid("rider-cid"); FlashDeliveryOrder order = order(); order.setStatus("WAITING_ACCEPTANCE"); order.setIsDisplay(true); order.setVehicleType(1); order.setDeliveryType("NORMAL"); order.setPickupLongitude(new BigDecimal("121.5000")); order.setPickupLatitude(new BigDecimal("25.0300")); SysDictData distance = new SysDictData(); distance.setDictValue("5"); when(positions.getFlashRiderNotificationList(order.getPickupLongitude(), order.getPickupLatitude(), 1, "NORMAL", 5, 20)).thenReturn(List.of(rider)); doNothing().when(service).sendAvailableOrderToRider(rider, order); try (MockedStatic dictionary = org.mockito.Mockito.mockStatic(DictUtils.class)) { dictionary.when(() -> DictUtils.getDictCache("sys_qs_newtask_distance")) .thenReturn(List.of(distance)); service.notifyAvailable(order); } verify(service).sendAvailableOrderToRider(rider, order); } @Test void nativeAvailableFlashOrderUsesRiderBatchAndUniKeepsPerRiderPath() { InfoUserMapper userMapper = mock(InfoUserMapper.class); RiderPositionMapper positions = mock(RiderPositionMapper.class); BusinessPushProperties properties = new BusinessPushProperties(); NativeRiderBatchPushService batches = mock(NativeRiderBatchPushService.class); FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService( userMapper, mock(PushEventService.class), positions, properties, batches)); FlashDeliveryOrder order = order(); order.setStatus("WAITING_ACCEPTANCE"); order.setIsDisplay(true); order.setVehicleType(1); order.setDeliveryType("NORMAL"); order.setPickupLongitude(new BigDecimal("121.5000")); order.setPickupLatitude(new BigDecimal("25.0300")); RiderPosition rider = new RiderPosition(); rider.setRiderId(9L); List riders = List.of(rider); SysDictData distance = new SysDictData(); distance.setDictValue("5"); when(positions.getFlashRiderNotificationList(order.getPickupLongitude(), order.getPickupLatitude(), 1, "NORMAL", 5, 20)).thenReturn(riders); doNothing().when(service).sendAvailableOrderToRider(rider, order); try (MockedStatic dictionary = org.mockito.Mockito.mockStatic(DictUtils.class)) { dictionary.when(() -> DictUtils.getDictCache("sys_qs_newtask_distance")) .thenReturn(List.of(distance)); properties.setMode(BusinessPushProperties.Mode.NATIVE); service.notifyAvailable(order); verify(batches, timeout(3000)).sendNewOrder(org.mockito.ArgumentMatchers.eq(riders), org.mockito.ArgumentMatchers.eq("no.message.push.message"), org.mockito.ArgumentMatchers.eq("no.message.push.new.flash.order"), org.mockito.ArgumentMatchers.eq(OrderPushBodyDto.getJson(order.getOrderNo(), "WAITING_ACCEPTANCE", 0, 3)), org.mockito.ArgumentMatchers.eq(order.getOrderNo()), org.mockito.ArgumentMatchers.any()); verify(service, never()).sendAvailableOrderToRider(rider, order); properties.setMode(BusinessPushProperties.Mode.UNI); service.notifyAvailable(order); verify(service).sendAvailableOrderToRider(rider, order); } } @Test void acceptedNotificationRoutesToSender() { InfoUserMapper userMapper = mock(InfoUserMapper.class); FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService( userMapper, mock(PushEventService.class))); doNothing().when(service).sendToUser(any(), any(), anyString(), anyString(), any()); InfoUser sender = new InfoUser(); sender.setUserId(7L); FlashDeliveryOrder order = order(); when(userMapper.selectById(7L)).thenReturn(sender); service.notifyAccepted(order); verify(service).sendToUser(same(sender), same(order), eq("ACCEPTED"), eq("no.message.push.delivery.personnel.receiving.order"), eq(UserNotificationScene.RIDER_ACCEPTED)); } @Test void missingSenderSkipsNotification() { InfoUserMapper userMapper = mock(InfoUserMapper.class); FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService( userMapper, mock(PushEventService.class))); when(userMapper.selectById(7L)).thenReturn(null); service.notifyAccepted(order()); verify(service, never()).sendToUser(any(), any(), anyString(), anyString(), any()); } @Test void pickedUpNotificationRoutesToSender() { InfoUserMapper userMapper = mock(InfoUserMapper.class); FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService( userMapper, mock(PushEventService.class))); doNothing().when(service).sendToUser(any(), any(), anyString(), anyString(), any()); InfoUser sender = new InfoUser(); sender.setUserId(7L); FlashDeliveryOrder order = order(); when(userMapper.selectById(7L)).thenReturn(sender); service.notifyPickedUp(order); verify(service).sendToUser(same(sender), same(order), eq("PICKED_UP"), eq("no.message.push.delivery.personnel.qspsz.order"), eq(UserNotificationScene.ORDER_DELIVERING)); } @Test void deliveredNotificationRoutesToSender() { InfoUserMapper userMapper = mock(InfoUserMapper.class); FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService( userMapper, mock(PushEventService.class))); doNothing().when(service).sendToUser(any(), any(), anyString(), anyString(), any()); InfoUser sender = new InfoUser(); sender.setUserId(7L); FlashDeliveryOrder order = order(); when(userMapper.selectById(7L)).thenReturn(sender); service.notifyDelivered(order); verify(service).sendToUser(same(sender), same(order), eq("DELIVERED"), eq("no.message.push.delivery.personnel.qsysd.order"), eq(UserNotificationScene.ORDER_DELIVERED)); } @Test void cancelledNotificationRoutesToAssignedRider() { InfoUserMapper userMapper = mock(InfoUserMapper.class); FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService( userMapper, mock(PushEventService.class))); doNothing().when(service).sendToRider(any(), any(), anyString(), anyString()); InfoUser rider = new InfoUser(); rider.setUserId(9L); FlashDeliveryOrder order = order(); order.setRiderId(9L); when(userMapper.selectById(9L)).thenReturn(rider); service.notifyCancelled(order); verify(service).sendToRider(same(rider), same(order), eq("CANCELLED"), eq("no.message.push.order.cancelled")); } @Test void cancelledNotificationSkipsWhenNoRiderAssigned() { InfoUserMapper userMapper = mock(InfoUserMapper.class); FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService( userMapper, mock(PushEventService.class))); service.notifyCancelled(order()); verify(service, never()).sendToRider(any(), any(), anyString(), anyString()); verify(service, never()).sendToUser(any(), any(), anyString(), anyString(), any()); } @Test void pickupStopProgressIsQueuedOnlyAfterCommitAndSentOnlyToOrderSender() { try (StopNotificationFixture fixture = new StopNotificationFixture()) { FlashDeliveryOrder order = order(); order.setStatus("ACCEPTED"); order.setReceiverUserId(88L); order.setDeliveryPinCode("order-secret-pin"); FlashDeliveryOrderStop stop = stop("PICKUP", 2); TransactionSynchronizationManager.initSynchronization(); try { assertDoesNotThrow(() -> fixture.service.notifyPickupStop(order, stop, 1, 2)); assertTrue(fixture.tasks.isEmpty()); assertTrue(fixture.messages.isEmpty()); assertTrue(fixture.lookedUpUsers.isEmpty()); TransactionSynchronizationManager.getSynchronizations().forEach(TransactionSynchronization::afterCommit); assertEquals(1, fixture.tasks.size()); assertTrue(fixture.messages.isEmpty()); assertEquals(List.of(7L), fixture.lookedUpUsers); fixture.runTasks(); } finally { TransactionSynchronizationManager.clearSynchronization(); } assertEquals(1, fixture.messages.size()); PushMessage message = fixture.messages.get(0); assertEquals(7L, message.getUserId()); assertEquals("FL-20260813-021", message.getDdId()); assertEquals(3, message.getPushType()); JSONObject body = JSON.parseObject(message.getBody()); assertEquals(Set.of("ddId", "state", "type", "pushType", "eventType", "stopId", "stopType", "stopOrder", "completedStopCount", "totalStopCount"), body.keySet()); assertEquals("FL-20260813-021", body.getString("ddId")); assertEquals("ACCEPTED", body.getString("state")); assertEquals(-1, body.getIntValue("type")); assertEquals(3, body.getIntValue("pushType")); assertEquals("FLASH_PICKUP_STOP_COMPLETED", body.getString("eventType")); assertEquals(41L, body.getLong("stopId")); assertEquals("PICKUP", body.getString("stopType")); assertEquals(2, body.getIntValue("stopOrder")); assertEquals(1, body.getIntValue("completedStopCount")); assertEquals(2, body.getIntValue("totalStopCount")); assertFalse(message.getBody().contains("secret")); assertEquals(1, fixture.sentContents.size()); assertTrue(fixture.sentContents.get(0).contains("stop 3")); assertTrue(fixture.sentContents.get(0).contains("1/2")); } } @Test void deliveryStopProgressKeepsRealPartialAndFinalOrderStatesWithoutWholeOrderScene() { try (StopNotificationFixture fixture = new StopNotificationFixture()) { FlashDeliveryOrder partial = order(); partial.setStatus("PICKED_UP"); assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(partial, stop("DELIVERY", 3), 1, 2)); FlashDeliveryOrder finished = order(); finished.setStatus("DELIVERED"); assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(finished, stop("DELIVERY", 4), 2, 2)); fixture.runTasks(); assertEquals(2, fixture.messages.size()); List bodies = fixture.messages.stream().map(message -> JSON.parseObject(message.getBody())).toList(); assertEquals(List.of("PICKED_UP", "DELIVERED"), bodies.stream().map(body -> body.getString("state")).toList()); assertEquals(List.of(1, 2), bodies.stream().map(body -> body.getIntValue("completedStopCount")).toList()); for (JSONObject body : bodies) { assertEquals("FLASH_DELIVERY_STOP_COMPLETED", body.getString("eventType")); assertEquals("DELIVERY", body.getString("stopType")); assertEquals(2, body.getIntValue("totalStopCount")); assertFalse(body.containsKey("eventScene")); } assertEquals(List.of(7L, 7L), fixture.messages.stream().map(PushMessage::getUserId).toList()); } } @Test void rollbackDoesNotQueueOrPersistEitherStopProgressNotification() { try (StopNotificationFixture fixture = new StopNotificationFixture()) { TransactionSynchronizationManager.initSynchronization(); try { assertDoesNotThrow(() -> fixture.service.notifyPickupStop(order(), stop("PICKUP", 0), 1, 2)); assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(order(), stop("DELIVERY", 2), 1, 2)); TransactionSynchronizationManager.getSynchronizations() .forEach(sync -> sync.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK)); fixture.runTasks(); assertTrue(fixture.lookedUpUsers.isEmpty()); assertTrue(fixture.tasks.isEmpty()); assertTrue(fixture.messages.isEmpty()); assertTrue(fixture.sentContents.isEmpty()); } finally { TransactionSynchronizationManager.clearSynchronization(); } } } @Test void missingDeviceAddressStillPersistsStopProgressThroughRealPayPushEventPath() { try (StopNotificationFixture fixture = new StopNotificationFixture()) { fixture.sender.setCid(null); for (BusinessPushProperties.Mode mode : BusinessPushProperties.Mode.values()) { fixture.properties.setMode(mode); assertDoesNotThrow(() -> fixture.service.notifyPickupStop(order(), stop("PICKUP", 0), 1, 2)); assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(order(), stop("DELIVERY", 2), 1, 2)); fixture.runTasks(); } assertEquals(4, fixture.messages.size()); assertTrue(fixture.messages.stream().allMatch(message -> Long.valueOf(7L).equals(message.getUserId()) && Integer.valueOf(3).equals(message.getPushType()) && "FL-20260813-021".equals(message.getDdId()))); assertTrue(fixture.sentContents.isEmpty()); } } @Test void stopProgressTextUsesSenderLocaleAndRouteNumberWithCompletedAndTotalCounts() { List locales = List.of(Locale.ROOT, Locale.TAIWAN, Locale.SIMPLIFIED_CHINESE, Locale.US, Locale.forLanguageTag("th-TH")); List pickupTexts = List.of("第 3 站已完成取貨,取貨進度 1/2", "第 3 站已完成取貨,取貨進度 1/2", "第 3 站已完成取货,取货进度 1/2", "Pickup completed at stop 3. Pickup progress: 1/2", "รับพัสดุที่จุด 3 แล้ว ความคืบหน้าการรับ: 1/2"); List deliveryTexts = List.of("第 5 站已送達,送達進度 2/2", "第 5 站已送達,送達進度 2/2", "第 5 站已送达,送达进度 2/2", "Delivery completed at stop 5. Delivery progress: 2/2", "ส่งพัสดุถึงจุด 5 แล้ว ความคืบหน้าการส่ง: 2/2"); for (int i = 0; i < locales.size(); i++) { try (StopNotificationFixture fixture = new StopNotificationFixture()) { fixture.locale.when(() -> LocaleUtils.getUserLocale(7L)).thenReturn(locales.get(i)); assertDoesNotThrow(() -> fixture.service.notifyPickupStop(order(), stop("PICKUP", 2), 1, 2)); assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(order(), stop("DELIVERY", 4), 2, 2)); fixture.runTasks(); assertEquals(2, fixture.messages.size()); assertTrue(fixture.messages.get(0).getContent().contains(pickupTexts.get(i))); assertTrue(fixture.messages.get(1).getContent().contains(deliveryTexts.get(i))); assertTrue(fixture.messages.stream().allMatch(message -> !message.getContent().contains("{0}") && message.getContent().contains("FL-20260813-021"))); } } } @Test void stopNotificationLookupAndSchedulingFailuresDoNotEscapeBusinessOperation() { try (StopNotificationFixture fixture = new StopNotificationFixture()) { when(fixture.users.selectById(7L)).thenThrow(new IllegalStateException("lookup failed")); assertDoesNotThrow(() -> fixture.service.notifyPickupStop(order(), stop("PICKUP", 0), 1, 2)); assertTrue(fixture.tasks.isEmpty()); org.mockito.Mockito.doReturn(fixture.sender).when(fixture.users).selectById(7L); doThrow(new IllegalStateException("executor unavailable")).when(fixture.manager).execute(any(TimerTask.class)); assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(order(), stop("DELIVERY", 2), 1, 2)); assertTrue(fixture.messages.isEmpty()); } } @Test void stopNotificationPersistenceFailureIsContainedAndNextProgressStillSends() { try (StopNotificationFixture fixture = new StopNotificationFixture()) { fixture.persistenceFailure = true; assertDoesNotThrow(() -> fixture.service.notifyPickupStop(order(), stop("PICKUP", 0), 1, 2)); assertDoesNotThrow(fixture::runTasks); assertTrue(fixture.messages.isEmpty()); fixture.persistenceFailure = false; assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(order(), stop("DELIVERY", 2), 1, 2)); assertDoesNotThrow(fixture::runTasks); assertEquals(1, fixture.messages.size()); assertEquals("FLASH_DELIVERY_STOP_COMPLETED", JSON.parseObject(fixture.messages.get(0).getBody()).getString("eventType")); } } @Test void externalStopPushFailureDoesNotLosePersistedProgressOrEscapeBusinessOperation() { try (StopNotificationFixture fixture = new StopNotificationFixture()) { doThrow(new IllegalStateException("network failure")).when(fixture.uni) .apppush(anyString(), anyString(), anyString(), anyString()); assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(order(), stop("DELIVERY", 2), 1, 2)); assertDoesNotThrow(fixture::runTasks); assertEquals(1, fixture.messages.size()); assertEquals(7L, fixture.messages.get(0).getUserId()); } } private static FlashDeliveryOrderStop stop(String type, int routeOrder) { FlashDeliveryOrderStop stop = new FlashDeliveryOrderStop(); stop.setId(41L); stop.setOrderId(10L); stop.setStopType(type); stop.setStopOrder(routeOrder); stop.setReceiverUserId(99L); stop.setName("secret-contact"); stop.setPhone("secret-phone"); stop.setAddress("secret-address"); stop.setDeliveryPinCode("secret-pin"); return stop; } private static final class StopNotificationFixture implements AutoCloseable { final InfoUserMapper users = mock(InfoUserMapper.class); final InfoUser sender = new InfoUser(); final PayPush uni = mock(PayPush.class); final BusinessPushProperties properties = new BusinessPushProperties(); final List lookedUpUsers = new ArrayList<>(); final List tasks = new ArrayList<>(); final List messages = new ArrayList<>(); final List sentContents = new ArrayList<>(); final MockedStatic spring; final MockedStatic async; final MockedStatic locale; final AsyncManager manager; final FlashDeliveryNotificationService service; boolean persistenceFailure; StopNotificationFixture() { sender.setUserId(7L); sender.setCid("sender-cid"); when(users.selectById(7L)).thenAnswer(call -> { lookedUpUsers.add(7L); return sender; }); IInfoUserService currentUsers = mock(IInfoUserService.class); when(currentUsers.getById(7L)).thenReturn(sender); BusinessPushService router = new BusinessPushService(properties, currentUsers, mock(FcmPushSender.class), mock(ApnsPushSender.class), uni, mock(com.ruoyi.app.push.NativePushSoundResolver.class)); IPushMessageService repository = mock(IPushMessageService.class); doAnswer(call -> { if (persistenceFailure) throw new IllegalStateException("message storage unavailable"); messages.add(call.getArgument(0)); return 1; }).when(repository).insertPushMessage(any(PushMessage.class)); PushEventListener listener = new PushEventListener(); ReflectionTestUtils.setField(listener, "pushMessageService", repository); PushEventService events = new PushEventService(); ApplicationEventPublisher publisher = event -> listener.onApplicationEvent((PushEvent) event); ReflectionTestUtils.setField(events, "eventPublisher", publisher); ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasename("i18n/messages"); messageSource.setDefaultEncoding("UTF-8"); messageSource.setFallbackToSystemLocale(false); spring = mockStatic(SpringUtils.class); spring.when(() -> SpringUtils.getBean("scheduledExecutorService")).thenReturn(mock(ScheduledExecutorService.class)); spring.when(() -> SpringUtils.getBean(MessageSource.class)).thenReturn(messageSource); spring.when(() -> SpringUtils.getBean(BusinessPushService.class)).thenReturn(router); manager = mock(AsyncManager.class); doAnswer(call -> { tasks.add(call.getArgument(0)); return null; }).when(manager).execute(any(TimerTask.class)); async = mockStatic(AsyncManager.class); async.when(AsyncManager::me).thenReturn(manager); locale = mockStatic(LocaleUtils.class); locale.when(() -> LocaleUtils.getUserLocale(7L)).thenReturn(Locale.US); doAnswer(call -> { sentContents.add(call.getArgument(2)); return null; }).when(uni) .apppush(eq("sender-cid"), anyString(), anyString(), anyString()); service = new FlashDeliveryNotificationService(users, events); } void runTasks() { List pending = new ArrayList<>(tasks); tasks.clear(); pending.forEach(TimerTask::run); } @Override public void close() { locale.close(); async.close(); spring.close(); } } private static FlashDeliveryOrder order() { FlashDeliveryOrder order = new FlashDeliveryOrder(); order.setId(10L); order.setOrderNo("FL-20260813-021"); order.setUserId(7L); return order; } }