package com.battery.business.web.device; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.lang.reflect.Method; import org.junit.jupiter.api.Test; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.exception.ServiceException; import com.battery.business.service.DeviceAdapterErrorCodes; import com.battery.business.service.DeviceIngestErrorCodes; import com.battery.business.service.IDeviceIngestService; import com.battery.business.service.IDeviceIngestService.IngestResult; /** * 接收端点的应答契约:拒绝一律用非 2xx 状态码——取数程序只在收到 2xx 后才推进自己的读取位置, * 所以"响应 200 但体内是错误码"是不能接受的:那会让程序把没落库的数据当成已送达。 */ class DeviceIngestControllerTest { private final IDeviceIngestService ingest = mock(IDeviceIngestService.class); private final MockMvc mvc = MockMvcBuilders.standaloneSetup(new DeviceIngestController(ingest)).build(); @Test void acceptedBatchAnswersOkSoTheProgramMayAdvanceItsWatermark() throws Exception { when(ingest.ingest(any(), any(), any())).thenReturn(new IngestResult(2, 0, 0)); mvc.perform(MockMvcRequestBuilders.post("/device-ingest/j22") .header(DeviceIngestController.TOKEN_HEADER, "token").content("{}")) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.accepted").value(2)); } @Test void rejectionsAnswerWithNonOkStatusesSoTheProgramKeepsItsPosition() throws Exception { assertStatus(DeviceIngestErrorCodes.TOKEN_INVALID, 401); assertStatus(DeviceIngestErrorCodes.SOURCE_DISABLED, 403); assertStatus(DeviceAdapterErrorCodes.UNSUPPORTED_TYPE, 403); assertStatus(DeviceIngestErrorCodes.PAYLOAD_INVALID, 400); assertStatus(DeviceAdapterErrorCodes.NOT_FOUND, 404); assertStatus(59999, 500); } /** 端点匿名放行(取数程序没有登录态),凭据由请求头里的来源令牌自己校验。 */ @Test void endpointIsAnonymousAndAuthenticatesWithTheSourceTokenHeader() throws Exception { Method receive = DeviceIngestController.class.getMethod("receive", String.class, String.class, String.class); assertTrue(DeviceIngestController.class.isAnnotationPresent(Anonymous.class)); assertEquals("X-Device-Ingest-Token", DeviceIngestController.TOKEN_HEADER); assertTrue(receive.getParameters()[1].toString().contains("token")); } private void assertStatus(int code, int expected) throws Exception { // 用 doThrow/when 形式重新打桩:直接调 mock 方法会被上一次的桩立即抛异常。 org.mockito.Mockito.doThrow(new ServiceException("拒绝", code)).when(ingest).ingest(any(), any(), any()); mvc.perform(MockMvcRequestBuilders.post("/device-ingest/j22").content("{}")) .andExpect(MockMvcResultMatchers.status().is(expected)); } }