|
@@ -0,0 +1,445 @@
|
|
|
|
|
+package com.zksy.web.controller.cockpit;
|
|
|
|
|
+
|
|
|
|
|
+import com.zksy.cockpit.event.CockpitCurrentEventsAdapter;
|
|
|
|
|
+import com.zksy.cockpit.event.CockpitCurrentEventsService;
|
|
|
|
|
+import com.zksy.cockpit.event.CockpitCurrentEventsSnapshot;
|
|
|
|
|
+import com.zksy.cockpit.foundation.CockpitReadException;
|
|
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+import org.springframework.security.access.prepost.PreAuthorize;
|
|
|
|
|
+import org.springframework.test.web.servlet.MockMvc;
|
|
|
|
|
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|
|
|
|
+import org.springframework.web.bind.annotation.DeleteMapping;
|
|
|
|
|
+import org.springframework.web.bind.annotation.GetMapping;
|
|
|
|
|
+import org.springframework.web.bind.annotation.PatchMapping;
|
|
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
|
|
+import org.springframework.web.bind.annotation.PutMapping;
|
|
|
|
|
+
|
|
|
|
|
+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 java.util.List;
|
|
|
|
|
+
|
|
|
|
|
+import static org.hamcrest.Matchers.hasSize;
|
|
|
|
|
+import static org.hamcrest.Matchers.nullValue;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
|
+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 CockpitCurrentEventsControllerTest {
|
|
|
|
|
+
|
|
|
|
|
+ private static final String ENDPOINT = "/api/cockpit/v1/events/current";
|
|
|
|
|
+
|
|
|
|
|
+ private CockpitCurrentEventsAdapter adapter;
|
|
|
|
|
+ private MockMvc mockMvc;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ void setUp() {
|
|
|
|
|
+ adapter = mock(CockpitCurrentEventsAdapter.class);
|
|
|
|
|
+ CockpitCurrentEventsService service = new CockpitCurrentEventsService(
|
|
|
|
|
+ adapter,
|
|
|
|
|
+ Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC));
|
|
|
|
|
+ mockMvc = MockMvcBuilders
|
|
|
|
|
+ .standaloneSetup(new CockpitCurrentEventsController(service))
|
|
|
|
|
+ .build();
|
|
|
|
|
+ when(adapter.load()).thenReturn(fixture());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void warningModeExposesEnvelopePagedEventsColorsAndWorkOrderSummaries() throws Exception {
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
|
|
+ .param("mode", "WARNING")
|
|
|
|
|
+ .header("X-Request-Id", "current-warnings-001"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.requestId").value("current-warnings-001"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.apiVersion").value("v1"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.readOnly").value(true))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.displayName").value("当前预警"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.timezone").value("Asia/Shanghai"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.mode").value("WARNING"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.filters.mode").value("WARNING"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.filters.level").value(nullValue()))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.filters.industry").value(nullValue()))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.filters.status").value(nullValue()))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.pagination.page").value(1))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.pagination.pageSize").value(100))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.pagination.total").value(5))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.pagination.totalPages").value(1))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.pagination.hasMore").value(false))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.state").value("SUCCESS"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data", hasSize(5)))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("W-001"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].level").value(1))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].levelColor").value("RED"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].deviceCode").value("D-GAS-1"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].industry").value("GAS"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].workOrderSummary.workOrderCode").value("WO-1"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].workOrderSummary.stateCode").value(4))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].workOrderSummary.stateName").value("处理中"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[1].eventNo").value("W-002"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[1].levelColor").value("YELLOW"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[1].workOrderSummary").value(nullValue()))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[2].eventNo").value("W-003"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[2].levelColor").value("BLUE"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[3].eventNo").value("W-004"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[3].levelColor").value("GREEN"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[4].eventNo").value("W-005"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[4].levelColor").value("UNKNOWN"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[4].workOrderSummary.stateName").value("已结案"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.workOrders.state").value("SUCCESS"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.workOrders.reason").value("SUCCESS"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.mode").value("WARNING"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(5)));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void alarmModeReturnsAlarmsOnlyAndKeepsFamilySeparate() throws Exception {
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("mode", "ALARM"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.displayName").value("当前报警"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.mode").value("ALARM"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.pagination.total").value(2))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data", hasSize(2)))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("A-001"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].levelColor").value("RED"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].workOrderSummary.stateName").value("已取消"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[1].eventNo").value("A-002"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[1].workOrderSummary").value(nullValue()))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.workOrders.state").value("SUCCESS"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(2)));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void levelColorsFollowOneRedTwoYellowThreeBlueFourGreen() throws Exception {
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("mode", "WARNING"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].levelColor").value("RED"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[1].levelColor").value("YELLOW"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[2].levelColor").value("BLUE"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[3].levelColor").value("GREEN"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[4].levelColor").value("UNKNOWN"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void unknownOrNonFormalEventTypesAreDroppedFromTheList() throws Exception {
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("mode", "WARNING"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.pagination.total").value(5))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("W-001"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(5)));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void emptyWorkOrderLinksProduceEmptySectionAndNullSummaries() throws Exception {
|
|
|
|
|
+ when(adapter.load()).thenReturn(fixtureWithNoWorkOrders());
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("mode", "WARNING"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.state").value("SUCCESS"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].workOrderSummary").value(nullValue()))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.workOrders.state").value("EMPTY"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.workOrders.reason").value("NO_WORK_ORDERS"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void workOrderSectionFailureRendersIndependentlyWhileEventsStayVisible() throws Exception {
|
|
|
|
|
+ when(adapter.load()).thenReturn(fixtureWithFailedWorkOrders());
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("mode", "WARNING"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.state").value("SUCCESS"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data", hasSize(5)))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].workOrderSummary").value(nullValue()))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.workOrders.state").value("FAILED"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.workOrders.reason").value("WORK_ORDER_SECTION_UNAVAILABLE"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.workOrders.message").value("工单状态分区暂不可用"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void emptyListReturnsSuccessfulEmptyState() throws Exception {
|
|
|
|
|
+ when(adapter.load()).thenReturn(CockpitCurrentEventsSnapshot.empty("2026-09-02T09:59:50+08:00"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("mode", "WARNING"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.pagination.total").value(0))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.state").value("EMPTY"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.reason").value("NO_EVENTS"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data").value(nullValue()))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.workOrders.state").value("EMPTY"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(0)));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void paginationSlicesEventsAndTotalsMatch() throws Exception {
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
|
|
+ .param("mode", "WARNING")
|
|
|
|
|
+ .param("page", "1")
|
|
|
|
|
+ .param("pageSize", "2"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.pagination.total").value(5))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.pagination.totalPages").value(3))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.pagination.hasMore").value(true))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data", hasSize(2)))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("W-001"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[1].eventNo").value("W-002"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
|
|
+ .param("mode", "WARNING")
|
|
|
|
|
+ .param("page", "3")
|
|
|
|
|
+ .param("pageSize", "2"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data", hasSize(1)))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("W-005"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.pagination.hasMore").value(false));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void levelIndustryAndStatusFiltersNarrowListAndMapFilter() throws Exception {
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
|
|
+ .param("mode", "WARNING")
|
|
|
|
|
+ .param("level", "2"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.pagination.total").value(1))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("W-002"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.level").value(2));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
|
|
+ .param("mode", "WARNING")
|
|
|
|
|
+ .param("industry", "WATER_SUPPLY"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.pagination.total").value(2))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.industry").value("WATER_SUPPLY"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
|
|
+ .param("mode", "WARNING")
|
|
|
|
|
+ .param("status", "ACTIVE"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.pagination.total").value(3))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("W-001"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.sections.events.data[2].eventNo").value("W-005"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.status").value("ACTIVE"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void missingOrInvalidModeReturnsTypedBadRequestBeforeReadAdapterIsCalled() throws Exception {
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT))
|
|
|
|
|
+ .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"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("mode", "WHATEVER"))
|
|
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.message").value("mode must be WARNING or ALARM"));
|
|
|
|
|
+
|
|
|
|
|
+ verifyNoInteractions(adapter);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void invalidLevelIndustryOrPaginationReturnsTypedBadRequestBeforeReadAdapterIsCalled() throws Exception {
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("mode", "WARNING").param("level", "abc"))
|
|
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.message").value("level must be an integer between 1 and 4"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("mode", "WARNING").param("level", "5"))
|
|
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.message").value("level must be an integer between 1 and 4"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("mode", "WARNING").param("industry", "MANHOLE"))
|
|
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.message").value(
|
|
|
|
|
+ "industry must be GAS, WATER_SUPPLY or DRAINAGE"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
|
|
+ .param("mode", "WARNING")
|
|
|
|
|
+ .param("page", "0")
|
|
|
|
|
+ .param("pageSize", "1001"))
|
|
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.message").value("当前预警分页参数不合法"));
|
|
|
|
|
+
|
|
|
|
|
+ verifyNoInteractions(adapter);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void serverFailureReturnsTypedServerError() throws Exception {
|
|
|
|
|
+ when(adapter.load()).thenThrow(new CockpitReadException("upstream unavailable"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
|
|
+ .param("mode", "WARNING")
|
|
|
|
|
+ .header("X-Request-Id", "current-warnings-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("current-warnings-500"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void mutatingRequestIsRejectedBeforeReadAdapterIsCalled() throws Exception {
|
|
|
|
|
+ mockMvc.perform(post(ENDPOINT))
|
|
|
|
|
+ .andExpect(status().isMethodNotAllowed());
|
|
|
|
|
+
|
|
|
|
|
+ verifyNoInteractions(adapter);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void controllerOnlyExposesAuthenticatedReadOnlyOperations() {
|
|
|
|
|
+ assertTrue(Arrays.stream(CockpitCurrentEventsController.class.getDeclaredMethods())
|
|
|
|
|
+ .filter(this::hasRequestMappingAnnotation)
|
|
|
|
|
+ .allMatch(CockpitCurrentEventsControllerTest::isReadOnlyMappedMethod));
|
|
|
|
|
+
|
|
|
|
|
+ assertTrue(Arrays.stream(CockpitCurrentEventsController.class.getDeclaredMethods())
|
|
|
|
|
+ .anyMatch(method -> method.isAnnotationPresent(PreAuthorize.class)
|
|
|
|
|
+ && "isAuthenticated()".equals(
|
|
|
|
|
+ method.getAnnotation(PreAuthorize.class).value())));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean hasRequestMappingAnnotation(Method method) {
|
|
|
|
|
+ return method.isAnnotationPresent(org.springframework.web.bind.annotation.RequestMapping.class)
|
|
|
|
|
+ || method.isAnnotationPresent(GetMapping.class)
|
|
|
|
|
+ || method.isAnnotationPresent(PostMapping.class)
|
|
|
|
|
+ || method.isAnnotationPresent(PutMapping.class)
|
|
|
|
|
+ || method.isAnnotationPresent(PatchMapping.class)
|
|
|
|
|
+ || method.isAnnotationPresent(DeleteMapping.class);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static boolean isReadOnlyMappedMethod(Method method) {
|
|
|
|
|
+ return method.isAnnotationPresent(GetMapping.class)
|
|
|
|
|
+ && !method.isAnnotationPresent(PostMapping.class)
|
|
|
|
|
+ && !method.isAnnotationPresent(PutMapping.class)
|
|
|
|
|
+ && !method.isAnnotationPresent(PatchMapping.class)
|
|
|
|
|
+ && !method.isAnnotationPresent(DeleteMapping.class);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static CockpitCurrentEventsSnapshot fixture() {
|
|
|
|
|
+ return new CockpitCurrentEventsSnapshot(
|
|
|
|
|
+ "2026-09-02T09:59:50+08:00",
|
|
|
|
|
+ types(),
|
|
|
|
|
+ Arrays.asList(
|
|
|
|
|
+ warning("W-001", "燃气浓度预警", "GAS_CONCENTRATION", 1, "ACTIVE",
|
|
|
|
|
+ "2026-09-02T09:50:00+08:00", "2026-09-02T09:52:00+08:00",
|
|
|
|
|
+ "12.5", "mg/m³", "D-GAS-1", "燃气浓度计-001", "城区A-燃气1号点",
|
|
|
|
|
+ "GAS_SENSOR", "110.130", "28.440"),
|
|
|
|
|
+ warning("W-002", "燃气压力预警", "GAS_PRESSURE", 2, "ACTIVE",
|
|
|
|
|
+ "2026-09-02T09:45:00+08:00", "2026-09-02T09:46:00+08:00",
|
|
|
|
|
+ "0.32", "MPa", "D-GAS-2", "燃气浓度计-002", "城区A-燃气2号点",
|
|
|
|
|
+ "GAS_SENSOR", "110.131", "28.441"),
|
|
|
|
|
+ warning("W-003", "供水压力预警", "WATER_PRESSURE", 3, "PENDING",
|
|
|
|
|
+ "2026-09-02T09:40:00+08:00", "2026-09-02T09:41:00+08:00",
|
|
|
|
|
+ "0.18", "MPa", "D-WS-1", "供水压力计-001", "城区B-供水1号点",
|
|
|
|
|
+ "WATER_SUPPLY_SENSOR", "110.150", "28.460"),
|
|
|
|
|
+ warning("W-004", "污水浓度预警", "SEWAGE_CONCENTRATION", 4, "RELEASED",
|
|
|
|
|
+ "2026-09-02T09:35:00+08:00", "2026-09-02T09:36:00+08:00",
|
|
|
|
|
+ "18.2", "mg/L", "D-DR-1", "污水环境监测仪-001", "城区C-污水1号点",
|
|
|
|
|
+ "SEWAGE_ENV", "110.170", "28.480"),
|
|
|
|
|
+ warning("W-005", "供水流量预警", "WATER_FLOW", 9, "ACTIVE",
|
|
|
|
|
+ "2026-09-02T09:30:00+08:00", "2026-09-02T09:31:00+08:00",
|
|
|
|
|
+ "66.6", "L/s", "D-WS-2", "供水压力计-002", "城区B-供水2号点",
|
|
|
|
|
+ "WATER_SUPPLY_SENSOR", "110.151", "28.461"),
|
|
|
|
|
+ warning("W-006", "未知类型预警", "LEGACY_EVENT", 1, "ACTIVE",
|
|
|
|
|
+ "2026-09-02T09:55:00+08:00", null,
|
|
|
|
|
+ "1", "x", "D-LEGACY", "旧设备", "旧位置",
|
|
|
|
|
+ "LEGACY", "110.100", "28.400"),
|
|
|
|
|
+ warning("W-007", "智能井盖预警", "MANHOLE_EVENT", 1, "ACTIVE",
|
|
|
|
|
+ "2026-09-02T09:55:00+08:00", null,
|
|
|
|
|
+ "1", "x", "D-MH", "智能井盖", "井盖位置",
|
|
|
|
|
+ "SMART_MANHOLE", "110.101", "28.401")),
|
|
|
|
|
+ Arrays.asList(
|
|
|
|
|
+ alarm("A-001", "燃气浓度报警", "GAS_CONCENTRATION", 1, "HANDLED",
|
|
|
|
|
+ "2026-09-02T09:55:00+08:00", "2026-09-02T09:56:00+08:00",
|
|
|
|
|
+ "22.6", "mg/m³", "D-GAS-3", "燃气浓度计-003", "城区A-燃气3号点",
|
|
|
|
|
+ "GAS_SENSOR", "110.132", "28.442"),
|
|
|
|
|
+ alarm("A-002", "污水浓度报警", "SEWAGE_CONCENTRATION", 2, "PROCESSING",
|
|
|
|
|
+ "2026-09-02T09:32:00+08:00", "2026-09-02T09:33:00+08:00",
|
|
|
|
|
+ "42.1", "mg/L", "D-DR-1", "污水环境监测仪-001", "城区C-污水1号点",
|
|
|
|
|
+ "SEWAGE_ENV", "110.170", "28.480")),
|
|
|
|
|
+ Arrays.asList(
|
|
|
|
|
+ workOrder("W-001", "WO-1", 2, "燃气浓度处置", 4),
|
|
|
|
|
+ workOrder("W-003", "WO-2", 3, "供水压力处置", 1),
|
|
|
|
|
+ workOrder("W-005", "WO-3", 2, "供水流量处置", 6),
|
|
|
|
|
+ workOrder("A-001", "WO-4", 1, "燃气浓度处置", 7)));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static List<CockpitCurrentEventsSnapshot.SourceType> types() {
|
|
|
|
|
+ return Arrays.asList(
|
|
|
|
|
+ new CockpitCurrentEventsSnapshot.SourceType(
|
|
|
|
|
+ "GAS_SENSOR", "燃气浓度计", "GAS", "SMART_DEVICE"),
|
|
|
|
|
+ new CockpitCurrentEventsSnapshot.SourceType(
|
|
|
|
|
+ "WATER_SUPPLY_SENSOR", "供水压力计", "WATER_SUPPLY", "SMART_DEVICE"),
|
|
|
|
|
+ new CockpitCurrentEventsSnapshot.SourceType(
|
|
|
|
|
+ "SEWAGE_ENV", "污水环境监测", "DRAINAGE", "SMART_DEVICE"),
|
|
|
|
|
+ new CockpitCurrentEventsSnapshot.SourceType(
|
|
|
|
|
+ "WATER_PLANT", "水厂", "WATER_SUPPLY", "FACILITY"),
|
|
|
|
|
+ new CockpitCurrentEventsSnapshot.SourceType(
|
|
|
|
|
+ "SMART_MANHOLE", "智能井盖", null, "SMART_DEVICE"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static CockpitCurrentEventsSnapshot fixtureWithNoWorkOrders() {
|
|
|
|
|
+ return new CockpitCurrentEventsSnapshot(
|
|
|
|
|
+ "2026-09-02T09:59:50+08:00",
|
|
|
|
|
+ types(),
|
|
|
|
|
+ Collections.singletonList(
|
|
|
|
|
+ warning("W-001", "燃气浓度预警", "GAS_CONCENTRATION", 1, "ACTIVE",
|
|
|
|
|
+ "2026-09-02T09:50:00+08:00", "2026-09-02T09:52:00+08:00",
|
|
|
|
|
+ "12.5", "mg/m³", "D-GAS-1", "燃气浓度计-001", "城区A-燃气1号点",
|
|
|
|
|
+ "GAS_SENSOR", "110.130", "28.440")),
|
|
|
|
|
+ Collections.singletonList(
|
|
|
|
|
+ alarm("A-001", "燃气浓度报警", "GAS_CONCENTRATION", 1, "HANDLED",
|
|
|
|
|
+ "2026-09-02T09:55:00+08:00", "2026-09-02T09:56:00+08:00",
|
|
|
|
|
+ "22.6", "mg/m³", "D-GAS-3", "燃气浓度计-003", "城区A-燃气3号点",
|
|
|
|
|
+ "GAS_SENSOR", "110.132", "28.442")),
|
|
|
|
|
+ Collections.emptyList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static CockpitCurrentEventsSnapshot fixtureWithFailedWorkOrders() {
|
|
|
|
|
+ CockpitCurrentEventsSnapshot base = fixture();
|
|
|
|
|
+ return new CockpitCurrentEventsSnapshot(
|
|
|
|
|
+ base.getUpdatedAt(),
|
|
|
|
|
+ base.getTypes(),
|
|
|
|
|
+ base.getWarnings(),
|
|
|
|
|
+ base.getAlarms(),
|
|
|
|
|
+ null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static CockpitCurrentEventsSnapshot.SourceWarning warning(
|
|
|
|
|
+ String no, String name, String type, Integer level, String status,
|
|
|
|
|
+ String eventTime, String updatedTime, String value, String unit,
|
|
|
|
|
+ String deviceCode, String deviceName, String location, String typeCode,
|
|
|
|
|
+ String longitude, String latitude) {
|
|
|
|
|
+ return new CockpitCurrentEventsSnapshot.SourceWarning(
|
|
|
|
|
+ no, name, type, level, status, eventTime, updatedTime,
|
|
|
|
|
+ new BigDecimal(value), unit, deviceCode, deviceName, location, typeCode,
|
|
|
|
|
+ new BigDecimal(longitude), new BigDecimal(latitude));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static CockpitCurrentEventsSnapshot.SourceAlarm alarm(
|
|
|
|
|
+ String id, String name, String type, Integer level, String status,
|
|
|
|
|
+ String eventTime, String updatedTime, String value, String unit,
|
|
|
|
|
+ String deviceCode, String deviceName, String location, String typeCode,
|
|
|
|
|
+ String longitude, String latitude) {
|
|
|
|
|
+ return new CockpitCurrentEventsSnapshot.SourceAlarm(
|
|
|
|
|
+ id, name, type, level, status, eventTime, updatedTime,
|
|
|
|
|
+ new BigDecimal(value), unit, deviceCode, deviceName, location, typeCode,
|
|
|
|
|
+ new BigDecimal(longitude), new BigDecimal(latitude));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static CockpitCurrentEventsSnapshot.SourceWorkOrderLink workOrder(
|
|
|
|
|
+ String eventNo, String workOrderCode, Integer orderLevel, String orderDesc, Integer stateCode) {
|
|
|
|
|
+ return new CockpitCurrentEventsSnapshot.SourceWorkOrderLink(
|
|
|
|
|
+ eventNo, workOrderCode, orderLevel, orderDesc, stateCode,
|
|
|
|
|
+ "2026-09-02T09:00:00+08:00",
|
|
|
|
|
+ "2026-09-02T09:30:00+08:00",
|
|
|
|
|
+ "2026-09-02T18:00:00+08:00",
|
|
|
|
|
+ null);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|