package com.baiyue.lastleg.merchant; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.lastleg.controller.MerchantLastlegOrderController; import com.baiyue.lastleg.inboundplan.controller.MerchantInboundPlanController; import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests; import com.baiyue.lastleg.inboundplan.service.InboundPlanService; import com.baiyue.lastleg.discard.controller.MerchantCargoDiscardController; import com.baiyue.lastleg.looseboard.controller.MerchantLocalLooseboardController; import com.baiyue.lastleg.looseboard.dto.LocalLooseboardRequests; import com.baiyue.lastleg.looseboard.service.LocalLooseboardService; import com.baiyue.lastleg.procurement.controller.MerchantOverseasProcurementController; import com.baiyue.lastleg.relabel.controller.MerchantRelabelApplicationController; import com.baiyue.lastleg.returnreceipt.controller.MerchantReturnOrderController; import com.baiyue.lastleg.security.OverseasPermissions; import com.baiyue.lastleg.transfer.controller.MerchantWarehouseTransferController; import com.baiyue.lastleg.trucking.controller.MerchantUsTruckOrderController; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.server.ResponseStatusException; import java.util.Arrays; import java.util.Map; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) class MerchantOverseasCustomerApiContractTest { @Mock LocalLooseboardService looseboardService; @Mock InboundPlanService inboundPlanService; private MerchantLocalLooseboardController looseboard; private MerchantInboundPlanController inbound; @BeforeEach void setUp() { looseboard = new MerchantLocalLooseboardController(); inbound = new MerchantInboundPlanController(); ReflectionTestUtils.setField(looseboard, "service", looseboardService); ReflectionTestUtils.setField(inbound, "service", inboundPlanService); } @Test void merchantControllersUseFrozenPathsAndOperationPermissions() { assertPath(MerchantOverseasProcurementController.class, "/api/merchant/v1/last-leg/overseas-procurements"); assertPermissions(MerchantOverseasProcurementController.class, Map.of( "create", OverseasPermissions.MERCHANT_PROCUREMENT_CREATE, "page", OverseasPermissions.MERCHANT_PROCUREMENT_LIST, "detail", OverseasPermissions.MERCHANT_PROCUREMENT_LIST)); assertPath(MerchantUsTruckOrderController.class, "/api/merchant/v1/last-leg/us-truck-orders"); assertPermissions(MerchantUsTruckOrderController.class, Map.of( "create", OverseasPermissions.MERCHANT_TRUCKING_CREATE, "page", OverseasPermissions.MERCHANT_TRUCKING_LIST, "detail", OverseasPermissions.MERCHANT_TRUCKING_LIST, "update", OverseasPermissions.MERCHANT_TRUCKING_EDIT)); assertPath(MerchantLocalLooseboardController.class, "/api/merchant/v1/last-leg/local-looseboard-orders"); assertPermissions(MerchantLocalLooseboardController.class, Map.of( "create", OverseasPermissions.MERCHANT_LOOSEBOARD_CREATE, "page", OverseasPermissions.MERCHANT_LOOSEBOARD_LIST, "detail", OverseasPermissions.MERCHANT_LOOSEBOARD_LIST, "update", OverseasPermissions.MERCHANT_LOOSEBOARD_EDIT)); assertController(MerchantInboundPlanController.class, "/api/merchant/v1/last-leg/inbound-plans", OverseasPermissions.MERCHANT_INBOUND_LIST); assertController(MerchantLastlegOrderController.class, "/api/merchant/v1/last-leg/orders", OverseasPermissions.MERCHANT_LAST_MILE_ORDER_LIST); assertPermissions(MerchantWarehouseTransferController.class, Map.of( "create", OverseasPermissions.MERCHANT_TRANSFER_CREATE, "page", OverseasPermissions.MERCHANT_TRANSFER_LIST, "detail", OverseasPermissions.MERCHANT_TRANSFER_LIST, "confirmMeasurement", OverseasPermissions.MERCHANT_TRANSFER_CONFIRM)); assertPermissions(MerchantCargoDiscardController.class, Map.of( "candidates", OverseasPermissions.MERCHANT_DISCARD_LIST, "create", OverseasPermissions.MERCHANT_DISCARD_CREATE, "page", OverseasPermissions.MERCHANT_DISCARD_LIST, "detail", OverseasPermissions.MERCHANT_DISCARD_LIST, "cancel", OverseasPermissions.MERCHANT_DISCARD_CANCEL)); assertPermissions(MerchantReturnOrderController.class, Map.of( "create", OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE, "page", OverseasPermissions.MERCHANT_RETURN_ORDER_LIST, "detail", OverseasPermissions.MERCHANT_RETURN_ORDER_LIST, "downloadPhoto", OverseasPermissions.MERCHANT_RETURN_ORDER_LIST, "relabelCandidates", OverseasPermissions.MERCHANT_RETURN_ORDER_RELABEL, "createRelabel", OverseasPermissions.MERCHANT_RETURN_ORDER_RELABEL)); assertPermissions(MerchantRelabelApplicationController.class, Map.of( "candidates", OverseasPermissions.MERCHANT_RELABEL_LIST, "historicalOrders", OverseasPermissions.MERCHANT_RELABEL_LIST, "create", OverseasPermissions.MERCHANT_RELABEL_CREATE, "page", OverseasPermissions.MERCHANT_RELABEL_LIST, "detail", OverseasPermissions.MERCHANT_RELABEL_LIST, "uploadShippingLabel", OverseasPermissions.MERCHANT_RELABEL_FILES, "uploadFile", OverseasPermissions.MERCHANT_RELABEL_FILES, "downloadShippingLabel", OverseasPermissions.MERCHANT_RELABEL_LIST, "downloadCompletionPhoto", OverseasPermissions.MERCHANT_RELABEL_LIST, "confirmCompletion", OverseasPermissions.MERCHANT_RELABEL_CONFIRM)); } @Test void looseboardAndInboundQueriesUseAuthenticatedCustomerScope() { AuthenticatedUser merchant = user(101L, "MERCHANT"); LocalLooseboardRequests.MerchantQuery looseboardQuery = new LocalLooseboardRequests.MerchantQuery(1, 20, null, null, null, null, null); InboundPlanRequests.Query inboundQuery = new InboundPlanRequests.Query(1, 20, null, null, null, null, null, null, null, null, null, null, null, null); looseboard.page(merchant, looseboardQuery); inbound.page(merchant, inboundQuery); verify(looseboardService).page("101", looseboardQuery); verify(inboundPlanService).page("101", inboundQuery); } @Test void merchantQueryDtosDoNotAcceptCustomerScope() { for (Class type : Set.of(LocalLooseboardRequests.MerchantQuery.class, com.baiyue.lastleg.procurement.dto.OverseasProcurementRequests.MerchantQuery.class, com.baiyue.lastleg.trucking.dto.UsTruckOrderRequests.MerchantQuery.class, InboundPlanRequests.Query.class)) { assertThat(Arrays.stream(type.getRecordComponents()).map(component -> component.getName())) .doesNotContain("customerId", "warehouseId", "operatorId"); } } @Test void merchantEndpointsRejectPlatformIdentityBeforeCallingServices() { AuthenticatedUser platform = user(0L, "PLATFORM"); assertThatThrownBy(() -> looseboard.detail(platform, "LB-1")) .isInstanceOf(ResponseStatusException.class).hasMessageContaining("403"); assertThatThrownBy(() -> inbound.detail(platform, "IP-1")) .isInstanceOf(ResponseStatusException.class).hasMessageContaining("403"); } private static void assertController(Class type, String path, String permission) { assertPath(type, path); assertThat(type.getAnnotation(PreAuthorize.class).value()) .isEqualTo("hasAuthority('" + permission + "')"); } private static void assertPath(Class type, String path) { assertThat(type.getAnnotation(RequestMapping.class).value()).containsExactly(path); } private static void assertPermissions(Class type, Map permissions) { for (var entry : permissions.entrySet()) { var method = Arrays.stream(type.getDeclaredMethods()) .filter(candidate -> candidate.getName().equals(entry.getKey())) .findFirst().orElseThrow(); assertThat(method.getAnnotation(PreAuthorize.class)) .as(type.getSimpleName() + "." + entry.getKey()).isNotNull(); assertThat(method.getAnnotation(PreAuthorize.class).value()) .isEqualTo("hasAuthority('" + entry.getValue() + "')"); } } private static AuthenticatedUser user(long customerId, String appCode) { return new AuthenticatedUser(7L, customerId, customerId, 8L, "operator", appCode, "WEB", Set.of()); } }