优化
This commit is contained in:
+65
-40
@@ -6,7 +6,6 @@ import com.baiyue.lastleg.delivery.service.DeliveryTransferService;
|
||||
import com.baiyue.lastleg.delivery.service.DeliveryDocumentService;
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryDocumentDtos;
|
||||
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;
|
||||
@@ -15,7 +14,6 @@ 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;
|
||||
@@ -26,7 +24,6 @@ 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;
|
||||
@@ -41,14 +38,12 @@ class MerchantDeliveryControllerTest {
|
||||
1, 20, null, null, null, null, null, null, null, null, null, null, null, null);
|
||||
|
||||
@Test
|
||||
void fixedRoutesUseIndependentListAndCreatePermissionsWithoutCustomerParameter() throws Exception {
|
||||
void fixedRoutesKeepOnlyTokenScopedReads() 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);
|
||||
Method receipts = MerchantDeliveryController.class.getMethod("receipts", AuthenticatedUser.class, String.class);
|
||||
Method download = MerchantDeliveryController.class.getMethod("receiptDownload", AuthenticatedUser.class, String.class, long.class);
|
||||
@@ -57,30 +52,23 @@ class MerchantDeliveryControllerTest {
|
||||
assertPermission(detail, OverseasPermissions.MERCHANT_DELIVERY_LIST);
|
||||
assertPermission(receipts, OverseasPermissions.MERCHANT_DELIVERY_LIST);
|
||||
assertPermission(download, 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(receipts.getParameters()).noneMatch(this::isCustomerParameter));
|
||||
assertTrue(Arrays.stream(download.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())));
|
||||
assertTrue(Arrays.stream(MerchantDeliveryController.class.getDeclaredMethods())
|
||||
.noneMatch(method -> method.isAnnotationPresent(org.springframework.web.bind.annotation.PostMapping.class)));
|
||||
}
|
||||
|
||||
@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
|
||||
@@ -89,7 +77,6 @@ class MerchantDeliveryControllerTest {
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> controller.page(platform, query));
|
||||
assertThrows(ResponseStatusException.class, () -> controller.detail(platform, "DEL-100"));
|
||||
assertThrows(ResponseStatusException.class, () -> controller.create(platform, "DSP-100", transferRequest()));
|
||||
assertThrows(ResponseStatusException.class, () -> controller.receipts(platform, "DEL-100"));
|
||||
assertThrows(ResponseStatusException.class, () -> controller.receiptDownload(platform, "DEL-100", 13));
|
||||
}
|
||||
@@ -122,29 +109,35 @@ class MerchantDeliveryControllerTest {
|
||||
org.mockito.Mockito.verifyNoInteractions(documents);
|
||||
}
|
||||
|
||||
/** 客户即使保留旧权限或原创建载荷,也不能通过已移除的写路由安排整车。 */
|
||||
@Test
|
||||
void invalidCreatePayloadIsRejectedByBeanValidationContract() {
|
||||
DeliveryRequests.Transfer invalid = new DeliveryRequests.Transfer(" ", null, " ", null, null,
|
||||
" ", " ", null, null, null, null, null, null, null, null, null, null, null, null);
|
||||
try (var factory = Validation.buildDefaultValidatorFactory()) {
|
||||
assertTrue(factory.getValidator().validate(invalid).size() >= 6);
|
||||
}
|
||||
void removedCreateRouteReturnsMethodNotAllowedWithoutCallingBusinessService() throws Exception {
|
||||
var mvc = org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup(controller).build();
|
||||
mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
||||
.post("/api/merchant/v1/last-leg/delivery-orders")
|
||||
.param("dispatchNo", "DP-1").contentType("application/json")
|
||||
.content("{\"idempotencyKey\":\"old-client\",\"fees\":[]}"))
|
||||
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isMethodNotAllowed());
|
||||
org.mockito.Mockito.verifyNoInteractions(service, documents);
|
||||
}
|
||||
|
||||
/** 精确核对客户派送路径,其他业务的合法写操作不得混入本合同。 */
|
||||
@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:");
|
||||
String contract = pathBlock(yaml, "/api/merchant/v1/last-leg/delivery-orders")
|
||||
+ pathBlock(yaml, "/api/merchant/v1/last-leg/delivery-orders/{deliveryNo}");
|
||||
|
||||
assertTrue(contract.contains("merchant:warehouse:delivery:list"));
|
||||
assertTrue(contract.contains("merchant:warehouse:delivery:create"));
|
||||
assertFalse(contract.contains("merchant:warehouse:delivery:create"));
|
||||
assertFalse(contract.contains("\n post:"));
|
||||
assertFalse(contract.contains("name: customerId"));
|
||||
String receiptContract = Files.readString(Path.of("src/main/resources/openapi/delivery-documents-v1.yaml"));
|
||||
String merchantPaths = slice(receiptContract,
|
||||
" /api/merchant/v1/last-leg/delivery-orders/{deliveryNo}/documents/receipt:",
|
||||
" /api/platform/v1/last-leg/delivery-orders/{deliveryNo}/documents:");
|
||||
String merchantPaths = pathBlock(receiptContract,
|
||||
"/api/merchant/v1/last-leg/delivery-orders/{deliveryNo}/documents/receipt")
|
||||
+ pathBlock(receiptContract,
|
||||
"/api/merchant/v1/last-leg/delivery-orders/{deliveryNo}/documents/receipt/files/{fileId}/download");
|
||||
assertTrue(merchantPaths.contains("merchant:warehouse:delivery:list"));
|
||||
assertTrue(merchantPaths.contains("/documents/receipt/files/{fileId}/download:"));
|
||||
assertFalse(merchantPaths.contains("#/components/parameters/CustomerId"));
|
||||
@@ -152,6 +145,41 @@ class MerchantDeliveryControllerTest {
|
||||
assertFalse(merchantPaths.contains("\n put:"));
|
||||
}
|
||||
|
||||
/** 相邻仓储业务保留POST时仍只提取派送路径,派送本身的POST不能被提取器漏掉。 */
|
||||
@Test
|
||||
void exactPathBlocksExcludeUnrelatedWritesAndKeepDeliveryWritesDetectable() {
|
||||
String yaml = """
|
||||
paths:
|
||||
/api/merchant/v1/last-leg/delivery-orders:
|
||||
get:
|
||||
summary: 客户派送列表
|
||||
/api/merchant/v1/last-leg/warehouse-transfers:
|
||||
post:
|
||||
summary: 合法仓储转运创建
|
||||
/api/merchant/v1/last-leg/delivery-orders/{deliveryNo}:
|
||||
get:
|
||||
summary: 客户派送详情
|
||||
/api/merchant/v1/last-leg/cargo-discards:
|
||||
post:
|
||||
summary: 合法弃货创建
|
||||
components:
|
||||
schemas:
|
||||
post: {}
|
||||
""";
|
||||
String contract = pathBlock(yaml, "/api/merchant/v1/last-leg/delivery-orders")
|
||||
+ pathBlock(yaml, "/api/merchant/v1/last-leg/delivery-orders/{deliveryNo}");
|
||||
assertTrue(contract.contains("客户派送列表"));
|
||||
assertTrue(contract.contains("客户派送详情"));
|
||||
assertFalse(contract.contains("warehouse-transfers"));
|
||||
assertFalse(contract.contains("cargo-discards"));
|
||||
assertFalse(contract.contains("components:"));
|
||||
assertFalse(contract.contains("\n post:"));
|
||||
String illegalDeliveryWrite = yaml.replace(" get:\n summary: 客户派送列表",
|
||||
" post:\n summary: 客户派送列表");
|
||||
assertTrue(pathBlock(illegalDeliveryWrite, "/api/merchant/v1/last-leg/delivery-orders")
|
||||
.contains("\n post:"));
|
||||
}
|
||||
|
||||
private boolean isCustomerParameter(java.lang.reflect.Parameter parameter) {
|
||||
RequestParam annotation = parameter.getAnnotation(RequestParam.class);
|
||||
return annotation != null && ("customerId".equals(annotation.name())
|
||||
@@ -163,11 +191,15 @@ class MerchantDeliveryControllerTest {
|
||||
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 String pathBlock(String source, String path) {
|
||||
var header = java.util.regex.Pattern.compile("(?m)^ "
|
||||
+ java.util.regex.Pattern.quote(path) + ":[ \\t]*\\r?$").matcher(source);
|
||||
assertTrue(header.find(), "缺少OpenAPI路径:" + path);
|
||||
var next = java.util.regex.Pattern.compile("(?m)^(?: /[^\\r\\n]*:|[A-Za-z][A-Za-z0-9_-]*:)[ \\t]*\\r?$")
|
||||
.matcher(source);
|
||||
int to = next.find(header.end()) ? next.start() : source.length();
|
||||
return source.substring(header.start(), to);
|
||||
}
|
||||
|
||||
private static AuthenticatedUser user(long tenantId, String appCode) {
|
||||
@@ -181,11 +213,4 @@ class MerchantDeliveryControllerTest {
|
||||
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", "派送费", "RECEIVABLE", "CNY",
|
||||
new BigDecimal("10.00"), null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null)), null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user