feat: complete phase 5 overseas fulfillment contracts

This commit is contained in:
2026-09-01 14:24:59 +08:00
parent ffed9bf898
commit 97168b19d8
46 changed files with 2446 additions and 409 deletions
@@ -0,0 +1,139 @@
package com.baiyue.lastleg.delivery.controller;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.delivery.dto.DeliveryRequests;
import com.baiyue.lastleg.delivery.service.DeliveryTransferService;
import com.baiyue.lastleg.security.OverseasPermissions;
import jakarta.validation.Validation;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.server.ResponseStatusException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
class MerchantDeliveryControllerTest {
private final DeliveryTransferService service = mock(DeliveryTransferService.class);
private final MerchantDeliveryController controller = controller();
private final DeliveryRequests.Query query = new DeliveryRequests.Query(
1, 20, null, null, null, null, null, null, null, null, null, null);
@Test
void fixedRoutesUseIndependentListAndCreatePermissionsWithoutCustomerParameter() throws Exception {
RequestMapping mapping = MerchantDeliveryController.class.getAnnotation(RequestMapping.class);
assertEquals(List.of("/api/merchant/v1/last-leg/delivery-orders"), List.of(mapping.value()));
Method page = MerchantDeliveryController.class.getMethod("page", AuthenticatedUser.class,
DeliveryRequests.Query.class);
Method create = MerchantDeliveryController.class.getMethod("create", AuthenticatedUser.class,
String.class, DeliveryRequests.Transfer.class);
Method detail = MerchantDeliveryController.class.getMethod("detail", AuthenticatedUser.class, String.class);
assertPermission(page, OverseasPermissions.MERCHANT_DELIVERY_LIST);
assertPermission(detail, OverseasPermissions.MERCHANT_DELIVERY_LIST);
assertPermission(create, OverseasPermissions.MERCHANT_DELIVERY_CREATE);
assertNotEquals(OverseasPermissions.MERCHANT_DELIVERY_LIST,
OverseasPermissions.MERCHANT_DELIVERY_CREATE);
assertTrue(Arrays.stream(page.getParameters()).noneMatch(this::isCustomerParameter));
assertTrue(Arrays.stream(detail.getParameters()).noneMatch(this::isCustomerParameter));
assertTrue(Arrays.stream(create.getParameters()).noneMatch(this::isCustomerParameter));
assertTrue(Arrays.stream(DeliveryRequests.Transfer.class.getRecordComponents())
.noneMatch(component -> "customerId".equals(component.getName())));
}
@Test
void allOperationsUseOnlyAuthenticatedCustomerScope() {
AuthenticatedUser merchant = user(100L, "MERCHANT");
DeliveryRequests.Transfer request = transferRequest();
controller.page(merchant, query);
controller.detail(merchant, "DEL-100");
controller.create(merchant, "DSP-100", request);
verify(service).deliveryPage("100", query);
verify(service).deliveryDetail("100", "DEL-100");
verify(service).merchantTransfer("100", "DSP-100", "1", request);
}
@Test
void merchantEndpointsRejectPlatformIdentity() {
AuthenticatedUser platform = user(0L, "PLATFORM");
assertThrows(ResponseStatusException.class, () -> controller.page(platform, query));
assertThrows(ResponseStatusException.class, () -> controller.detail(platform, "DEL-100"));
assertThrows(ResponseStatusException.class, () -> controller.create(platform, "DSP-100", transferRequest()));
}
@Test
void invalidCreatePayloadIsRejectedByBeanValidationContract() {
DeliveryRequests.Transfer invalid = new DeliveryRequests.Transfer(" ", null, " ", null, null,
" ", " ", null, null, null, null, null, null, null, null, null);
try (var factory = Validation.buildDefaultValidatorFactory()) {
assertTrue(factory.getValidator().validate(invalid).size() >= 6);
}
}
@Test
void openApiPublishesOnlyTokenScopedMerchantDeliveryProtocol() throws Exception {
String yaml = Files.readString(Path.of("src/main/resources/openapi/overseas-business-v1.yaml"),
StandardCharsets.UTF_8);
String contract = slice(yaml, " /api/merchant/v1/last-leg/delivery-orders:",
" /api/merchant/v1/last-leg/overseas-procurements:");
assertTrue(contract.contains("merchant:warehouse:delivery:list"));
assertTrue(contract.contains("merchant:warehouse:delivery:create"));
assertFalse(contract.contains("name: customerId"));
}
private boolean isCustomerParameter(java.lang.reflect.Parameter parameter) {
RequestParam annotation = parameter.getAnnotation(RequestParam.class);
return annotation != null && ("customerId".equals(annotation.name())
|| "customerId".equals(annotation.value()));
}
private static void assertPermission(Method method, String permission) {
PreAuthorize annotation = method.getAnnotation(PreAuthorize.class);
assertEquals("hasAuthority('" + permission + "')", annotation.value());
}
private static String slice(String source, String start, String end) {
int from = source.indexOf(start);
int to = source.indexOf(end, from + start.length());
assertTrue(from >= 0 && to > from);
return source.substring(from, to);
}
private static AuthenticatedUser user(long tenantId, String appCode) {
return new AuthenticatedUser(1L, tenantId, tenantId, 9L, "merchant", appCode, "WEB", Set.of());
}
private MerchantDeliveryController controller() {
MerchantDeliveryController value = new MerchantDeliveryController();
ReflectionTestUtils.setField(value, "service", service);
return value;
}
private static DeliveryRequests.Transfer transferRequest() {
return new DeliveryRequests.Transfer("merchant-key", LocalDateTime.now(), "SELF_DELIVERY", null, null,
"ONE_PICKUP_ONE_DROP", "PALLET", null, null, null, null, null, null, null, null,
List.of(new DeliveryRequests.FeeItem("DELIVERY_FEE", "USD", BigDecimal.ZERO, null)));
}
}