|
|
@@ -0,0 +1,192 @@
|
|
|
+package com.zksy.web.controller.cockpit;
|
|
|
+
|
|
|
+import com.zksy.cockpit.foundation.CockpitReadException;
|
|
|
+import com.zksy.cockpit.overview.CockpitOverviewWarningSettingsAdapter;
|
|
|
+import com.zksy.cockpit.overview.CockpitOverviewWarningSettingsService;
|
|
|
+import com.zksy.cockpit.overview.CockpitOverviewWarningSettingsSnapshot;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.springframework.security.access.prepost.PreAuthorize;
|
|
|
+import org.springframework.web.bind.annotation.GetMapping;
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
+import org.springframework.web.bind.annotation.PutMapping;
|
|
|
+import org.springframework.web.bind.annotation.PatchMapping;
|
|
|
+import org.springframework.web.bind.annotation.DeleteMapping;
|
|
|
+import org.springframework.test.web.servlet.MockMvc;
|
|
|
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|
|
+
|
|
|
+import java.time.Clock;
|
|
|
+import java.time.Instant;
|
|
|
+import java.time.ZoneOffset;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.lang.reflect.Method;
|
|
|
+
|
|
|
+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 CockpitOverviewWarningSettingsControllerTest {
|
|
|
+
|
|
|
+ private CockpitOverviewWarningSettingsAdapter adapter;
|
|
|
+ private MockMvc mockMvc;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ adapter = mock(CockpitOverviewWarningSettingsAdapter.class);
|
|
|
+ CockpitOverviewWarningSettingsService service = new CockpitOverviewWarningSettingsService(
|
|
|
+ adapter,
|
|
|
+ Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC));
|
|
|
+ mockMvc = MockMvcBuilders
|
|
|
+ .standaloneSetup(new CockpitOverviewWarningSettingsController(service))
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void warningSettingsUsesEnvelopeCamelCaseAndDefaultWarningMode() throws Exception {
|
|
|
+ when(adapter.load()).thenReturn(fixture());
|
|
|
+
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/overview/warning-settings")
|
|
|
+ .header("X-Request-Id", "warning-settings-001"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.msg").value("操作成功"))
|
|
|
+ .andExpect(jsonPath("$.data.requestId").value("warning-settings-001"))
|
|
|
+ .andExpect(jsonPath("$.data.apiVersion").value("v1"))
|
|
|
+ .andExpect(jsonPath("$.data.readOnly").value(true))
|
|
|
+ .andExpect(jsonPath("$.data.displayName").value("预警设置"))
|
|
|
+ .andExpect(jsonPath("$.data.selectedMode").value("WARNING"))
|
|
|
+ .andExpect(jsonPath("$.data.timezone").value("Asia/Shanghai"))
|
|
|
+ .andExpect(jsonPath("$.data.range.type").value("CURRENT_BASELINE"))
|
|
|
+ .andExpect(jsonPath("$.data.freshness.status").value("CURRENT"))
|
|
|
+ .andExpect(jsonPath("$.data.freshness.reason").value("READ_ONLY_AGGREGATE"))
|
|
|
+ .andExpect(jsonPath("$.data.updatedAt").value("2026-09-02T09:59:50+08:00"))
|
|
|
+ .andExpect(jsonPath("$.data.warning.total").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.warning.handled").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.warning.pending").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.warning.handlingEfficiency.value").value("0.3333"))
|
|
|
+ .andExpect(jsonPath("$.data.warning.handlingEfficiency.state").value("AVAILABLE"))
|
|
|
+ .andExpect(jsonPath("$.data.warning.handlingEfficiency.numerator").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.warning.handlingEfficiency.denominator").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.warning.trend[0].handlingEfficiency.value").value("0.5000"))
|
|
|
+ .andExpect(jsonPath("$.data.warning.trend[0].handlingEfficiency.numerator").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.warning.trend[0].handlingEfficiency.denominator").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.warning.workOrderStates['1']").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.warning.workOrderStates['5']").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.warning.workOrderStates['6']").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.warning.unlinkedEventCount").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.alarm.total").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.alarm.handled").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.alarm.pending").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.alarm.cancelledCount").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.alarm.handlingEfficiency.value").value("0.0000"))
|
|
|
+ .andExpect(jsonPath("$.data.alarm.handlingEfficiency.numerator").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.alarm.handlingEfficiency.denominator").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.eventFamily").value("WARNING"))
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.label").value("预警"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void alarmModeSelectsAlarmMapFilterWithoutMergingFamilies() throws Exception {
|
|
|
+ when(adapter.load()).thenReturn(fixture());
|
|
|
+
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/overview/warning-settings")
|
|
|
+ .queryParam("mode", "ALARM")
|
|
|
+ .header("X-Request-Id", "warning-settings-alarm"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.selectedMode").value("ALARM"))
|
|
|
+ .andExpect(jsonPath("$.data.warning.total").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.alarm.total").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.alarm.workOrderStates['4']").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.eventFamily").value("ALARM"))
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.label").value("报警"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void invalidModeReturnsTypedBadRequestBeforeReadAdapterIsCalled() throws Exception {
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/overview/warning-settings")
|
|
|
+ .queryParam("mode", "THRESHOLD"))
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
+ .andExpect(jsonPath("$.code").value(400))
|
|
|
+ .andExpect(jsonPath("$.data.category").value("INVALID_REQUEST"))
|
|
|
+ .andExpect(jsonPath("$.data.message").value("mode must be WARNING or ALARM"));
|
|
|
+
|
|
|
+ verifyNoInteractions(adapter);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void mutatingRequestIsRejectedBeforeReadAdapterIsCalled() throws Exception {
|
|
|
+ mockMvc.perform(post("/api/cockpit/v1/overview/warning-settings"))
|
|
|
+ .andExpect(status().isMethodNotAllowed());
|
|
|
+
|
|
|
+ verifyNoInteractions(adapter);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void controllerExposesAuthenticatedReadOnlyMethodsOnly() {
|
|
|
+ assertTrue(Arrays.stream(CockpitOverviewWarningSettingsController.class.getDeclaredMethods())
|
|
|
+ .filter(method -> method.isAnnotationPresent(GetMapping.class)
|
|
|
+ || method.isAnnotationPresent(PostMapping.class)
|
|
|
+ || method.isAnnotationPresent(PutMapping.class)
|
|
|
+ || method.isAnnotationPresent(PatchMapping.class)
|
|
|
+ || method.isAnnotationPresent(DeleteMapping.class))
|
|
|
+ .allMatch(method -> method.isAnnotationPresent(GetMapping.class)
|
|
|
+ && !method.isAnnotationPresent(PostMapping.class)
|
|
|
+ && !method.isAnnotationPresent(PutMapping.class)
|
|
|
+ && !method.isAnnotationPresent(PatchMapping.class)
|
|
|
+ && !method.isAnnotationPresent(DeleteMapping.class)));
|
|
|
+
|
|
|
+ assertTrue(Arrays.stream(CockpitOverviewWarningSettingsController.class.getDeclaredMethods())
|
|
|
+ .anyMatch(method -> method.isAnnotationPresent(PreAuthorize.class)
|
|
|
+ && "isAuthenticated()".equals(method.getAnnotation(
|
|
|
+ PreAuthorize.class).value())));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void assertTrue(boolean condition) {
|
|
|
+ org.junit.jupiter.api.Assertions.assertTrue(condition);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void serverFailureReturnsTypedServerError() throws Exception {
|
|
|
+ when(adapter.load()).thenThrow(new CockpitReadException("upstream unavailable"));
|
|
|
+
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/overview/warning-settings")
|
|
|
+ .header("X-Request-Id", "warning-settings-500"))
|
|
|
+ .andExpect(status().isInternalServerError())
|
|
|
+ .andExpect(jsonPath("$.code").value(500))
|
|
|
+ .andExpect(jsonPath("$.data.category").value("SERVER_ERROR"))
|
|
|
+ .andExpect(jsonPath("$.data.message").value("驾驶舱预警设置概览数据暂不可用"))
|
|
|
+ .andExpect(jsonPath("$.data.requestId").value("warning-settings-500"))
|
|
|
+ .andExpect(jsonPath("$.data.serverTime").value("2026-09-02T10:00:00+08:00"));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CockpitOverviewWarningSettingsSnapshot fixture() {
|
|
|
+ return new CockpitOverviewWarningSettingsSnapshot(
|
|
|
+ "2026-09-02T09:59:50+08:00",
|
|
|
+ Arrays.asList(
|
|
|
+ new CockpitOverviewWarningSettingsSnapshot.SourceEvent(
|
|
|
+ "WARNING", "WRN-001", "DEV-001", "WO-001", 1),
|
|
|
+ new CockpitOverviewWarningSettingsSnapshot.SourceEvent(
|
|
|
+ "WARNING", "WRN-002", "DEV-002", "WO-002", 5),
|
|
|
+ new CockpitOverviewWarningSettingsSnapshot.SourceEvent(
|
|
|
+ "WARNING", "WRN-003", "DEV-003", "WO-003", 6),
|
|
|
+ new CockpitOverviewWarningSettingsSnapshot.SourceEvent(
|
|
|
+ "ALARM", "ALM-001", "DEV-004", "WO-004", 4),
|
|
|
+ new CockpitOverviewWarningSettingsSnapshot.SourceEvent(
|
|
|
+ "ALARM", "ALM-002", "DEV-005", "WO-005", 4)),
|
|
|
+ Arrays.asList(
|
|
|
+ new CockpitOverviewWarningSettingsSnapshot.SourceTrendPoint(
|
|
|
+ "WARNING", "2026-09-01", 2, 1),
|
|
|
+ new CockpitOverviewWarningSettingsSnapshot.SourceTrendPoint(
|
|
|
+ "WARNING", "2026-09-02", 1, 0),
|
|
|
+ new CockpitOverviewWarningSettingsSnapshot.SourceTrendPoint(
|
|
|
+ "ALARM", "2026-09-01", 1, 0),
|
|
|
+ new CockpitOverviewWarningSettingsSnapshot.SourceTrendPoint(
|
|
|
+ "ALARM", "2026-09-02", 1, 0)));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|