package com.ruoyi.system.service.impl; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import java.io.IOException; import java.time.Duration; import java.time.Instant; import java.util.List; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.springframework.web.context.request.async.AsyncRequestNotUsableException; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import com.ruoyi.device.bts.BtsDevelopmentSimulator; import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore; import com.ruoyi.system.mapper.TTestMapper; import com.ruoyi.system.service.ITTestService; import com.ruoyi.system.service.ITestLiveService.LiveState; /** 实时流客户端断连时应结束连接而不触发全局异常处理。 */ class TTestLiveServiceImplTest { @Test void initialWriteFailureShouldCompleteStreamWithoutErrorDispatch() { RecordingEmitter emitter = new RecordingEmitter(0); TTestLiveServiceImpl service = new TestableLiveService(emitter); try { service.stream(4L); } finally { service.shutdown(); } assertTrue(emitter.completed); assertFalse(emitter.completedWithError); } @Test void scheduledWriteFailureShouldCompleteStreamWithoutErrorDispatch() { RecordingEmitter emitter = new RecordingEmitter(6); TTestLiveServiceImpl service = new TestableLiveService(emitter); try { service.stream(4L); Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> emitter.completed); } finally { service.shutdown(); } assertFalse(emitter.completedWithError); } private static final class TestableLiveService extends TTestLiveServiceImpl { private final RecordingEmitter emitter; private TestableLiveService(RecordingEmitter emitter) { super(mock(ITTestService.class), mock(DeviceDiscoveryCandidateStore.class), mock(BtsDevelopmentSimulator.class), mock(TTestMapper.class)); this.emitter = emitter; } @Override protected SseEmitter createEmitter() { return emitter; } @Override public LiveState current(Long testId) { return new LiveState(testId, "RUNNING", "RECEIVING", Instant.now(), List.of(), List.of()); } } private static final class RecordingEmitter extends SseEmitter { private final int successfulWrites; private int writes; private volatile boolean completed; private volatile boolean completedWithError; private RecordingEmitter(int successfulWrites) { this.successfulWrites = successfulWrites; } @Override public void send(SseEventBuilder builder) throws IOException { if (writes++ >= successfulWrites) throw new AsyncRequestNotUsableException("client disconnected", new IOException("connection closed")); } @Override public synchronized void complete() { completed = true; } @Override public synchronized void completeWithError(Throwable error) { completedWithError = true; } } }