package com.baiyue.lastleg.controller; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.core.api.PageResult; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.common.event.EventEnvelope; import com.baiyue.common.event.EventTypes; import com.baiyue.lastleg.dto.LastlegOrderRequests; import com.baiyue.lastleg.dto.LastlegOrderSummary; import com.baiyue.lastleg.security.InternalServiceTokenGuard; import com.baiyue.lastleg.service.LastlegOrderService; import com.baiyue.lastleg.service.LastlegOrderCommandService; import com.baiyue.lastleg.dto.LastlegOrderCommandDtos; import com.baiyue.lastleg.service.LoadingCompletedConsumer; import com.baiyue.lastleg.shipping.service.ContainerShippingNodeConsumer; import org.junit.jupiter.api.Test; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.beans.BeanUtils; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.server.ResponseStatusException; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.never; import static org.mockito.Mockito.when; class LastlegOrderControllerTest { // Controller contract tests intentionally instantiate the controller without a Spring context here. private final LastlegOrderService service = mock(LastlegOrderService.class); private final LastlegOrderCommandService commandService = mock(LastlegOrderCommandService.class); private final LastlegOrderController controller = controller(); private final LastlegOrderRequests.Query query = new LastlegOrderRequests.Query( 1L, 20L, null, null, null, null, null, null, null, null, null, null, null, null, null, null); @Test void orderQueryHasOneResolvableConstructorForRequestParameterBinding() { assertEquals(16, BeanUtils.getResolvableConstructor(LastlegOrderRequests.Query.class).getParameterCount()); } @Test void platformEndpointRejectsMerchantIdentity() { assertThrows(ResponseStatusException.class, () -> controller.list(user(100L, "MERCHANT"), "200", query)); } @Test void platformOperatorCanQueryAllCustomerTenantsWithoutCustomerId() { controller.list(user(0L, "PLATFORM"), null, query); verify(service).page(isNull(), eq(query)); } @Test void platformOperatorCanQueryExplicitCustomerTenant() { controller.list(user(0L, "PLATFORM"), "200", query); verify(service).page(eq("200"), eq(query)); } @Test void holdCommandPassesVersionedIdempotentRequestToSingleOrderService() { var request = new LastlegOrderCommandDtos.HoldRequest(true, true, "海关查验", 2); controller.hold(user(0L, "PLATFORM"), "100", 11L, "hold-11", request); verify(commandService).hold("100", 11L, "hold-11", request, "1", "tester"); } @Test void manualNodeQueryUsesOrderScopeRatherThanContainerScope() { controller.manualNodes(user(0L, "PLATFORM"), "100", 11L); verify(commandService).manualNodes("100", 11L); verify(service, never()).containerProgressDetail(org.mockito.ArgumentMatchers.anyString()); } @Test void platformOperatorCanQueryCrossCustomerContainerSummaries() { var summaryQuery = new LastlegOrderRequests.ContainerProgressQuery(1, 20, "CONT-", null, null); controller.containerSummaries(user(0L, "PLATFORM"), summaryQuery); verify(service).containerProgressPage(summaryQuery); } @Test void containerEndpointsUseCanonicalLastMileOrderAuthorities() throws Exception { var list = LastlegOrderController.class.getMethod("containerSummaries", AuthenticatedUser.class, LastlegOrderRequests.ContainerProgressQuery.class); var detail = LastlegOrderController.class.getMethod("containerSummaryDetail", AuthenticatedUser.class, String.class); var change = LastlegOrderController.class.getMethod("changeReceivingWarehouse", AuthenticatedUser.class, String.class, String.class, LastlegOrderRequests.ChangeReceivingWarehouse.class); assertEquals("hasAuthority('platform:overseas-warehouse:last-mile-order:list')", list.getAnnotation(PreAuthorize.class).value()); assertEquals("hasAuthority('platform:overseas-warehouse:last-mile-order:list')", detail.getAnnotation(PreAuthorize.class).value()); assertEquals("hasAuthority('platform:overseas-warehouse:last-mile-order:edit')", change.getAnnotation(PreAuthorize.class).value()); } @Test void containerSummariesRejectMerchantIdentity() { var summaryQuery = new LastlegOrderRequests.ContainerProgressQuery(1, 20, null, null, null); assertThrows(ResponseStatusException.class, () -> controller.containerSummaries(user(100L, "MERCHANT"), summaryQuery)); verify(service, never()).containerProgressPage(summaryQuery); } @Test void platformOperatorCanQueryUnpagedContainerDetailByStableId() { controller.containerSummaryDetail(user(0L, "PLATFORM"), "BOOKING-1"); verify(service).containerProgressDetail("BOOKING-1"); } @Test void exportBuildsCsvBeforeReturningTheResponse() { byte[] csv = "csv".getBytes(java.nio.charset.StandardCharsets.UTF_8); when(service.exportCsv("200", query)).thenReturn(csv); var response = controller.export(user(0L, "PLATFORM"), "200", query); assertArrayEquals(csv, response.getBody()); verify(service).exportCsv("200", query); } @Test void platformOperatorCanExportAllCustomersWithoutFilters() { byte[] csv = "all customers".getBytes(java.nio.charset.StandardCharsets.UTF_8); LastlegOrderRequests.Query emptyQuery = new LastlegOrderRequests.Query( null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); when(service.exportCsv(null, emptyQuery)).thenReturn(csv); var response = controller.export(user(0L, "PLATFORM"), null, emptyQuery); assertEquals(Long.valueOf(1), emptyQuery.page()); assertEquals(Long.valueOf(20), emptyQuery.size()); assertArrayEquals(csv, response.getBody()); verify(service).exportCsv(isNull(), eq(emptyQuery)); } @Test void iamSecurityEventsAreRejectedBeforeLoadingCompletedConsumer() { EventEnvelope event = mock(EventEnvelope.class); when(event.eventType()).thenReturn(EventTypes.IAM_USER_STATUS_CHANGED); LoadingCompletedConsumer loading = (LoadingCompletedConsumer) ReflectionTestUtils.getField(controller, "consumer"); assertThrows(ResponseStatusException.class, () -> controller.receive("internal-token", event)); verify(loading, never()).consume(event); } private static AuthenticatedUser user(long customerId, String appCode) { return new AuthenticatedUser(1L, customerId, customerId, 1L, "tester", appCode, "WEB", Set.of()); } private LastlegOrderController controller() { LastlegOrderController controller = new LastlegOrderController(); ReflectionTestUtils.setField(controller, "consumer", mock(LoadingCompletedConsumer.class)); ReflectionTestUtils.setField(controller, "shippingNodeConsumer", mock(ContainerShippingNodeConsumer.class)); ReflectionTestUtils.setField(controller, "orderService", service); ReflectionTestUtils.setField(controller, "commandService", commandService); ReflectionTestUtils.setField(controller, "tokenGuard", mock(InternalServiceTokenGuard.class)); return controller; } }