yeg
2026-09-28 ccf7fa9210887633944bd4d78f90463b82807e6e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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));
    }
}