|
|
@@ -0,0 +1,251 @@
|
|
|
+package com.zksy.web.controller.cockpit;
|
|
|
+
|
|
|
+import com.zksy.cockpit.event.CockpitAlarmWarningRelation;
|
|
|
+import com.zksy.cockpit.event.CockpitEventAlarm;
|
|
|
+import com.zksy.cockpit.event.CockpitEventReadAdapter;
|
|
|
+import com.zksy.cockpit.event.CockpitEventReadService;
|
|
|
+import com.zksy.cockpit.event.CockpitEventSnapshot;
|
|
|
+import com.zksy.cockpit.event.CockpitEventWarning;
|
|
|
+import com.zksy.cockpit.event.CockpitEventLevelColor;
|
|
|
+import com.zksy.cockpit.foundation.CockpitDeviceNotFoundException;
|
|
|
+import com.zksy.cockpit.foundation.CockpitReadException;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.springframework.test.web.servlet.MockMvc;
|
|
|
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|
|
+
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.time.Clock;
|
|
|
+import java.time.Instant;
|
|
|
+import java.time.ZoneOffset;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.verifyNoInteractions;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
|
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
|
+
|
|
|
+class CockpitEventReadControllerTest {
|
|
|
+ private static final String DEVICE_CODE = "TC-PS-WS-202609020012";
|
|
|
+
|
|
|
+ private CockpitEventReadAdapter adapter;
|
|
|
+ private MockMvc mockMvc;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ adapter = mock(CockpitEventReadAdapter.class);
|
|
|
+ CockpitEventReadService service = new CockpitEventReadService(
|
|
|
+ adapter,
|
|
|
+ Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC));
|
|
|
+ mockMvc = MockMvcBuilders
|
|
|
+ .standaloneSetup(new CockpitEventReadController(service))
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void currentEventsReturnSeparateFamiliesAndAuthoritativeRelations() throws Exception {
|
|
|
+ when(adapter.loadCurrent(DEVICE_CODE)).thenReturn(currentFixture());
|
|
|
+
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/events/current")
|
|
|
+ .header("X-Request-Id", "event-current-001"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.data.requestId").value("event-current-001"))
|
|
|
+ .andExpect(jsonPath("$.data.apiVersion").value("v1"))
|
|
|
+ .andExpect(jsonPath("$.data.readOnly").value(true))
|
|
|
+ .andExpect(jsonPath("$.data.deviceCode").value(DEVICE_CODE))
|
|
|
+ .andExpect(jsonPath("$.data.state").value("READY"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings", org.hamcrest.Matchers.hasSize(2)))
|
|
|
+ .andExpect(jsonPath("$.data.warnings[0].warningNo").value("WRN-20260902-001"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings[0].level").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.warnings[0].levelColor").value("RED"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings[0].status").value("PROCESSING"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings[0].deviceCode").value(DEVICE_CODE))
|
|
|
+ .andExpect(jsonPath("$.data.warnings[1].level").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.warnings[1].levelColor").value("YELLOW"))
|
|
|
+ .andExpect(jsonPath("$.data.alarms", org.hamcrest.Matchers.hasSize(1)))
|
|
|
+ .andExpect(jsonPath("$.data.alarms[0].alarmId").value("ALM-20260902-001"))
|
|
|
+ .andExpect(jsonPath("$.data.alarms[0].level").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.alarms[0].levelColor").value("BLUE"))
|
|
|
+ .andExpect(jsonPath("$.data.alarms[0].status").value("UNHANDLED"))
|
|
|
+ .andExpect(jsonPath("$.data.alarms[0].deviceCode").value(DEVICE_CODE))
|
|
|
+ .andExpect(jsonPath("$.data.alarmWarningRelations", org.hamcrest.Matchers.hasSize(1)))
|
|
|
+ .andExpect(jsonPath("$.data.alarmWarningRelations[0].warningNo").value("WRN-20260902-001"))
|
|
|
+ .andExpect(jsonPath("$.data.alarmWarningRelations[0].alarmId").value("ALM-20260902-001"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void eventLevelColorDictionaryCoversOneRedToFourGreen() throws Exception {
|
|
|
+ when(adapter.loadCurrent(DEVICE_CODE)).thenReturn(levelFixture());
|
|
|
+
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/events/current"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.warnings[0].levelColor").value("RED"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings[1].levelColor").value("YELLOW"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings[2].levelColor").value("BLUE"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings[3].levelColor").value("GREEN"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void historyEventsReturnPagedContractAndPreserveFamilies() throws Exception {
|
|
|
+ when(adapter.loadHistory(
|
|
|
+ DEVICE_CODE,
|
|
|
+ java.time.OffsetDateTime.parse("2026-09-01T10:00:00+08:00"),
|
|
|
+ java.time.OffsetDateTime.parse("2026-09-02T10:00:00+08:00"),
|
|
|
+ 1,
|
|
|
+ 100)).thenReturn(historyFixture());
|
|
|
+
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/events/history")
|
|
|
+ .param("startTime", "2026-09-01T10:00:00+08:00")
|
|
|
+ .param("endTime", "2026-09-02T10:00:00+08:00")
|
|
|
+ .param("page", "1")
|
|
|
+ .param("pageSize", "100")
|
|
|
+ .header("X-Request-Id", "event-history-001"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.requestId").value("event-history-001"))
|
|
|
+ .andExpect(jsonPath("$.data.startTime").value("2026-09-01T10:00:00+08:00"))
|
|
|
+ .andExpect(jsonPath("$.data.endTime").value("2026-09-02T10:00:00+08:00"))
|
|
|
+ .andExpect(jsonPath("$.data.timezone").value("Asia/Shanghai"))
|
|
|
+ .andExpect(jsonPath("$.data.page").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.pageSize").value(100))
|
|
|
+ .andExpect(jsonPath("$.data.total").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.warnings", org.hamcrest.Matchers.hasSize(2)))
|
|
|
+ .andExpect(jsonPath("$.data.alarms", org.hamcrest.Matchers.hasSize(1)))
|
|
|
+ .andExpect(jsonPath("$.data.alarmWarningRelations", org.hamcrest.Matchers.hasSize(1)));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void historyRejectsInvertedRangeAndInvalidPage() throws Exception {
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/events/history")
|
|
|
+ .param("startTime", "2026-09-02T10:00:00+08:00")
|
|
|
+ .param("endTime", "2026-09-01T10:00:00+08:00"))
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
+ .andExpect(jsonPath("$.data.category").value("INVALID_REQUEST"));
|
|
|
+
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/events/history")
|
|
|
+ .param("startTime", "2026-09-01T10:00:00+08:00")
|
|
|
+ .param("endTime", "2026-09-02T10:00:00+08:00")
|
|
|
+ .param("page", "0"))
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
+ .andExpect(jsonPath("$.data.category").value("INVALID_REQUEST"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void currentEventsReturnSuccessfulEmptyState() throws Exception {
|
|
|
+ when(adapter.loadCurrent(DEVICE_CODE)).thenReturn(emptyFixture());
|
|
|
+
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/events/current"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.state").value("EMPTY"))
|
|
|
+ .andExpect(jsonPath("$.data.stateReason").value("NO_EVENTS"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings", org.hamcrest.Matchers.hasSize(0)))
|
|
|
+ .andExpect(jsonPath("$.data.alarms", org.hamcrest.Matchers.hasSize(0)))
|
|
|
+ .andExpect(jsonPath("$.data.alarmWarningRelations", org.hamcrest.Matchers.hasSize(0)));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void missingDeviceAndAdapterFailureUseTypedReadOnlyErrors() throws Exception {
|
|
|
+ when(adapter.loadCurrent("MISSING")).thenThrow(new CockpitDeviceNotFoundException("device not found"));
|
|
|
+ when(adapter.loadCurrent("BROKEN")).thenThrow(new CockpitReadException("read failed"));
|
|
|
+
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/equipments/MISSING/events/current"))
|
|
|
+ .andExpect(status().isNotFound())
|
|
|
+ .andExpect(jsonPath("$.data.category").value("NOT_FOUND"));
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/equipments/BROKEN/events/current"))
|
|
|
+ .andExpect(status().isInternalServerError())
|
|
|
+ .andExpect(jsonPath("$.data.category").value("SERVER_ERROR"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void controllerExposesReadOnlyMappedMethodsOnly() throws Exception {
|
|
|
+ Method[] methods = CockpitEventReadController.class.getDeclaredMethods();
|
|
|
+ for (Method method : methods) {
|
|
|
+ if (method.isAnnotationPresent(org.springframework.web.bind.annotation.GetMapping.class)) {
|
|
|
+ assertTrue(!method.isAnnotationPresent(org.springframework.web.bind.annotation.PostMapping.class)
|
|
|
+ && !method.isAnnotationPresent(org.springframework.web.bind.annotation.PutMapping.class)
|
|
|
+ && !method.isAnnotationPresent(org.springframework.web.bind.annotation.PatchMapping.class)
|
|
|
+ && !method.isAnnotationPresent(org.springframework.web.bind.annotation.DeleteMapping.class));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ mockMvc.perform(post("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/events/current"))
|
|
|
+ .andExpect(status().isMethodNotAllowed());
|
|
|
+ verifyNoInteractions(adapter);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void assertTrue(boolean condition) {
|
|
|
+ org.junit.jupiter.api.Assertions.assertTrue(condition);
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitEventSnapshot currentFixture() {
|
|
|
+ return new CockpitEventSnapshot(
|
|
|
+ DEVICE_CODE,
|
|
|
+ "ws_sewage_environment",
|
|
|
+ "污水环境监测设备",
|
|
|
+ Arrays.asList(
|
|
|
+ new CockpitEventWarning(
|
|
|
+ "WRN-20260902-001", "污水COD预警", "WS_ENV", 1, "RED",
|
|
|
+ "PROCESSING", "2026-09-02T09:59:30+08:00",
|
|
|
+ new BigDecimal("18.2"), "mg/L", DEVICE_CODE, "污水环境监测设备", "太常片区"),
|
|
|
+ new CockpitEventWarning(
|
|
|
+ "WRN-20260902-002", "污水pH预警", "WS_ENV", 2, "YELLOW",
|
|
|
+ "PENDING", "2026-09-02T09:58:30+08:00",
|
|
|
+ new BigDecimal("8.1"), "", DEVICE_CODE, "污水环境监测设备", "太常片区")),
|
|
|
+ Collections.singletonList(new CockpitEventAlarm(
|
|
|
+ "ALM-20260902-001", "WS_ENV_HIGH", "污水COD报警", "WS_ENV", 3, "BLUE",
|
|
|
+ "UNHANDLED", "2026-09-02T10:00:00+08:00",
|
|
|
+ new BigDecimal("42.0"), new BigDecimal("40.0"), new BigDecimal("50.0"), "mg/L",
|
|
|
+ DEVICE_CODE, "污水环境监测设备", "太常片区")),
|
|
|
+ Collections.singletonList(new CockpitAlarmWarningRelation(
|
|
|
+ "REL-20260902-001", "WRN-20260902-001", "ALM-20260902-001",
|
|
|
+ "2026-09-02T10:00:00+08:00")),
|
|
|
+ null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitEventSnapshot levelFixture() {
|
|
|
+ return new CockpitEventSnapshot(
|
|
|
+ DEVICE_CODE,
|
|
|
+ "ws_sewage_environment",
|
|
|
+ "污水环境监测设备",
|
|
|
+ Arrays.asList(
|
|
|
+ warning("WRN-1", 1), warning("WRN-2", 2),
|
|
|
+ warning("WRN-3", 3), warning("WRN-4", 4)),
|
|
|
+ Collections.emptyList(),
|
|
|
+ Collections.emptyList(),
|
|
|
+ null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitEventSnapshot historyFixture() {
|
|
|
+ return new CockpitEventSnapshot(
|
|
|
+ DEVICE_CODE,
|
|
|
+ "ws_sewage_environment",
|
|
|
+ "污水环境监测设备",
|
|
|
+ Arrays.asList(warning("WRN-20260901-001", 2), warning("WRN-20260901-002", 4)),
|
|
|
+ Collections.singletonList(new CockpitEventAlarm(
|
|
|
+ "ALM-20260901-001", "WS_ENV_HIGH", "污水COD报警", "WS_ENV", 1, "RED",
|
|
|
+ "HANDLED", "2026-09-01T15:30:00+08:00",
|
|
|
+ new BigDecimal("45.0"), new BigDecimal("40.0"), new BigDecimal("50.0"), "mg/L",
|
|
|
+ DEVICE_CODE, "污水环境监测设备", "太常片区")),
|
|
|
+ Collections.singletonList(new CockpitAlarmWarningRelation(
|
|
|
+ "REL-20260901-001", "WRN-20260901-001", "ALM-20260901-001",
|
|
|
+ "2026-09-01T15:30:00+08:00")),
|
|
|
+ 3);
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitEventSnapshot emptyFixture() {
|
|
|
+ return new CockpitEventSnapshot(
|
|
|
+ DEVICE_CODE, "gas_pressure", "燃气压力计",
|
|
|
+ Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitEventWarning warning(String warningNo, Integer level) {
|
|
|
+ return new CockpitEventWarning(
|
|
|
+ warningNo, "等级映射预警", "WS_ENV", level, CockpitEventLevelColor.resolve(level), "PENDING",
|
|
|
+ "2026-09-02T09:59:30+08:00", null, "", DEVICE_CODE, "污水环境监测设备", "太常片区");
|
|
|
+ }
|
|
|
+}
|