package com.zksy.web.controller.cockpit; import com.zksy.base.service.GasPipePointService; import com.zksy.base.service.ManholeBaseService; import com.zksy.base.service.PipeNetworkBaseService; import com.zksy.cockpit.foundation.CockpitReadException; import com.zksy.cockpit.geometry.CockpitFacilityGeometryResponse; import com.zksy.cockpit.geometry.CockpitFacilityGeometryResponse.Feature; import com.zksy.cockpit.geometry.CockpitFacilityGeometryResponse.Properties; import com.zksy.cockpit.geometry.CockpitFacilityGeometryService; import com.zksy.cockpit.foundation.CockpitCoordinateValidator; 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.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.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.*; 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.*; /** Standalone MVC tests lock JSON/routing, not the deployed authentication chain. */ class CockpitFacilityGeometryControllerTest { private static final String URL = "/api/cockpit/v1/infrastructure/geometry"; private CockpitFacilityGeometryService service; private MockMvc mvc; @BeforeEach void setUp() { service = mock(CockpitFacilityGeometryService.class); mvc = MockMvcBuilders.standaloneSetup(new CockpitFacilityGeometryController(service, Clock.fixed(Instant.parse("2026-10-12T02:00:00Z"), ZoneOffset.UTC))).build(); } @Test void returnsGeoJsonInsideTheExistingAjaxEnvelopeWithoutRequiringAnAuthorizationHeader() throws Exception { PipeNetworkBaseService networks = mock(PipeNetworkBaseService.class); GasPipePointService points = mock(GasPipePointService.class); ManholeBaseService manholes = mock(ManholeBaseService.class); when(networks.list()).thenReturn(Collections.emptyList()); when(points.list()).thenReturn(Collections.emptyList()); when(manholes.list()).thenReturn(Collections.emptyList()); CockpitFacilityGeometryResponse response = new CockpitFacilityGeometryService(networks, points, manholes, Clock.fixed(Instant.parse("2026-10-12T02:00:00Z"), ZoneOffset.UTC)).load(); when(service.load()).thenReturn(response); mvc.perform(get(URL)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.type").value("FeatureCollection")) .andExpect(jsonPath("$.data.features", hasSize(0))) .andExpect(jsonPath("$.data.coverage", hasSize(9))) .andExpect(jsonPath("$.data.coverage[4].state").value("UNAVAILABLE")); } @Test void invalidGeometryIsExplicitJsonNullWithRawCoordinatesAndSameValidationShape() throws Exception { Properties properties = new Properties("gas_pipe_point", "1", "G1", "阀门", "GAS", "GAS_PIPE_POINT", null, "阀门", null, Collections.singletonList(Arrays.asList(null, new BigDecimal("28.4"))), CockpitCoordinateValidator.validate(null, new BigDecimal("28.4"), true), Collections.singletonList(CockpitCoordinateValidator.validate(null, new BigDecimal("28.4"), true))); when(service.load()).thenReturn(new CockpitFacilityGeometryResponse("2026-10-12T10:00:00+08:00", Collections.singletonList(new Feature("gas_pipe_point:1", null, properties)), Collections.emptyList())); mvc.perform(get(URL)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.features[0].type").value("Feature")) .andExpect(jsonPath("$.data.features[0].geometry").value(nullValue())) .andExpect(jsonPath("$.data.features[0].properties.networkId").value(nullValue())) .andExpect(jsonPath("$.data.features[0].properties.pointType").value("阀门")) .andExpect(jsonPath("$.data.features[0].properties.sourceCoordinates[0][0]").value(nullValue())) .andExpect(jsonPath("$.data.features[0].properties.coordinateValidation.state").value("INVALID")) .andExpect(jsonPath("$.data.features[0].properties.coordinateValidation.reason").value("MISSING_LONGITUDE")) .andExpect(jsonPath("$.data.features[0].properties.coordinateValidation.renderable").value(false)) .andExpect(jsonPath("$.data.features[0].properties.coordinateValidation.crs").value("WGS84")); } @Test void sourceFailureReturns500AndStableCategoryRatherThanSuccess() throws Exception { when(service.load()).thenThrow(new CockpitReadException("private database details")); mvc.perform(get(URL).header("X-Request-Id", "geometry-failure")) .andExpect(status().isInternalServerError()) .andExpect(jsonPath("$.code").value(500)) .andExpect(jsonPath("$.data.category").value("SERVER_ERROR")) .andExpect(jsonPath("$.data.message").value("facility geometry source unavailable")) .andExpect(jsonPath("$.data.requestId").value("geometry-failure")); } @Test void serializesActualServicePointAndLineCoordinatesInLongitudeLatitudeOrder() throws Exception { PipeNetworkBaseService networks = mock(PipeNetworkBaseService.class); GasPipePointService points = mock(GasPipePointService.class); ManholeBaseService manholes = mock(ManholeBaseService.class); com.zksy.base.domain.PipeNetworkBase line = new com.zksy.base.domain.PipeNetworkBase(); line.setNetworkId("gas-line"); line.setNetworkType("0"); line.setStartLongitude(new BigDecimal("110.4")); line.setStartLatitude(new BigDecimal("28.4")); line.setEndLongitude(new BigDecimal("110.6")); line.setEndLatitude(new BigDecimal("28.6")); com.zksy.base.domain.GasPipePoint point = new com.zksy.base.domain.GasPipePoint(); point.setPointId(1L); point.setPointType("三通"); point.setNetworkId("gas-line"); point.setLongitude(new BigDecimal("110.5")); point.setLatitude(new BigDecimal("28.5")); when(networks.list()).thenReturn(Collections.singletonList(line)); when(points.list()).thenReturn(Collections.singletonList(point)); when(manholes.list()).thenReturn(Collections.emptyList()); CockpitFacilityGeometryService actual = new CockpitFacilityGeometryService(networks, points, manholes, Clock.fixed(Instant.parse("2026-10-12T02:00:00Z"), ZoneOffset.UTC)); mvc = MockMvcBuilders.standaloneSetup(new CockpitFacilityGeometryController(actual, Clock.fixed(Instant.parse("2026-10-12T02:00:00Z"), ZoneOffset.UTC))).build(); mvc.perform(get(URL)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.features", hasSize(2))) .andExpect(jsonPath("$.data.features[0].geometry.type").value("LineString")) .andExpect(jsonPath("$.data.features[0].geometry.coordinates", hasSize(2))) .andExpect(jsonPath("$.data.features[0].geometry.coordinates[0][0]").value(110.4)) .andExpect(jsonPath("$.data.features[0].geometry.coordinates[0][1]").value(28.4)) .andExpect(jsonPath("$.data.features[0].geometry.coordinates[1][0]").value(110.6)) .andExpect(jsonPath("$.data.features[0].geometry.coordinates[1][1]").value(28.6)) .andExpect(jsonPath("$.data.features[1].geometry.type").value("Point")) .andExpect(jsonPath("$.data.features[1].geometry.coordinates", hasSize(2))) .andExpect(jsonPath("$.data.features[1].geometry.coordinates[0]").value(110.5)) .andExpect(jsonPath("$.data.features[1].geometry.coordinates[1]").value(28.5)) .andExpect(jsonPath("$.data.features[1].properties.pointType").value("三通")) .andExpect(jsonPath("$.data.features[1].properties.networkId").value("gas-line")) .andExpect(jsonPath("$.data.features[1].properties.coordinateValidation.state").value("UNVERIFIED")) .andExpect(jsonPath("$.data.features[1].properties.coordinateValidation.renderable").value(false)); } @Test void transactionProxyFailureIsAlsoMappedToSanitized500() throws Exception { when(service.load()).thenThrow(new org.springframework.transaction.CannotCreateTransactionException( "private connection detail")); mvc.perform(get(URL).header("X-Request-Id", "transaction-failure")) .andExpect(status().isInternalServerError()) .andExpect(jsonPath("$.code").value(500)) .andExpect(jsonPath("$.data.category").value("SERVER_ERROR")) .andExpect(jsonPath("$.data.message").value("facility geometry source unavailable")) .andExpect(jsonPath("$.data.requestId").value("transaction-failure")); } @Test void postIsNotAWriteEntryPoint() throws Exception { mvc.perform(post(URL)).andExpect(status().isMethodNotAllowed()); verifyNoInteractions(service); } }