优化
This commit is contained in:
+93
@@ -0,0 +1,93 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.firstleg.dto.ContainerFeeDetailDtos;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeDetailEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ContainerFeeOpenApiContractTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void coversEveryControllerOperationPermissionAndResponse() throws Exception {
|
||||
Map<String, Object> api = read();
|
||||
Map<String, Map<String, Map<String, Object>>> paths = (Map<String, Map<String, Map<String, Object>>>) api.get("paths");
|
||||
String base = PlatformContainerFeeDetailController.class.getAnnotation(RequestMapping.class).value()[0];
|
||||
int operationCount = 0;
|
||||
for (var method : PlatformContainerFeeDetailController.class.getDeclaredMethods()) {
|
||||
var get = method.getAnnotation(GetMapping.class);
|
||||
var post = method.getAnnotation(PostMapping.class);
|
||||
if (get == null && post == null) continue;
|
||||
String[] suffix = get != null ? get.value() : post.value();
|
||||
String path = base + (suffix.length == 0 ? "" : suffix[0]);
|
||||
assertThat(paths).containsKey(path);
|
||||
var operation = paths.get(path).get(get != null ? "get" : "post");
|
||||
assertThat(operation).isNotNull();
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('" + operation.get("x-permission") + "')");
|
||||
var responses = (Map<String, Object>) operation.get("responses");
|
||||
assertThat((Map<String, Object>) responses.get("200")).containsKey("content");
|
||||
operationCount++;
|
||||
}
|
||||
assertThat(paths.values().stream().mapToInt(Map::size).sum()).isEqualTo(operationCount);
|
||||
validateReferences(api, api);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void requestAndResponseSchemasMatchJavaFields() throws Exception {
|
||||
var components = (Map<String, Object>) read().get("components");
|
||||
var schemas = (Map<String, Map<String, Object>>) components.get("schemas");
|
||||
for (Class<?> dto : List.of(ContainerFeeDetailDtos.SaveRequest.class,
|
||||
ContainerFeeDetailDtos.AdjustRequest.class, ContainerFeeDetailDtos.Allocation.class,
|
||||
ContainerFeeDetailDtos.AllocationGroupRequest.class, ContainerFeeDetailDtos.AllocationAmount.class,
|
||||
ContainerFeeDetailDtos.AllocationGroupAdjustRequest.class, ContainerFeeDetailDtos.VersionRequest.class,
|
||||
ContainerFeeDetailDtos.RejectRequest.class)) {
|
||||
Set<String> fields = Arrays.stream(dto.getRecordComponents()).map(value -> value.getName()).collect(Collectors.toSet());
|
||||
var properties = (Map<String, Object>) schemas.get(dto.getSimpleName()).get("properties");
|
||||
assertThat(properties.keySet()).containsExactlyInAnyOrderElementsOf(fields);
|
||||
}
|
||||
Set<String> responseFields = Arrays.stream(ContainerFeeDetailEntity.class.getDeclaredFields())
|
||||
.filter(field -> !Modifier.isStatic(field.getModifiers())).map(field -> field.getName()).collect(Collectors.toSet());
|
||||
var properties = (Map<String, Object>) schemas.get("FeeDetail").get("properties");
|
||||
assertThat(properties.keySet()).containsExactlyInAnyOrderElementsOf(responseFields);
|
||||
Set<String> envelopeFields = Arrays.stream(com.baiyue.common.core.api.ApiResponse.class.getRecordComponents())
|
||||
.map(component -> component.getName()).collect(Collectors.toSet());
|
||||
for (String name : List.of("FeeResponse", "FeeListResponse", "EmptyResponse")) {
|
||||
var envelope = (Map<String, Object>) schemas.get(name).get("properties");
|
||||
assertThat(envelope.keySet()).containsExactlyInAnyOrderElementsOf(envelopeFields);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> read() throws Exception {
|
||||
return new Yaml().load(Files.readString(Path.of("src/main/resources/openapi/container-fees-v1.yaml")));
|
||||
}
|
||||
|
||||
private static void validateReferences(Object value, Map<String, Object> root) {
|
||||
if (value instanceof Map<?, ?> map) {
|
||||
if (map.get("$ref") instanceof String ref) {
|
||||
assertThat(ref).startsWith("#/");
|
||||
Object target = root;
|
||||
for (String segment : ref.substring(2).split("/")) target = ((Map<?, ?>) target).get(segment);
|
||||
assertThat(target).as(ref).isNotNull();
|
||||
}
|
||||
map.values().forEach(child -> validateReferences(child, root));
|
||||
} else if (value instanceof List<?> list) {
|
||||
list.forEach(child -> validateReferences(child, root));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,6 +265,48 @@ class ContainerFeeDetailServiceTest {
|
||||
.doesNotContainNull().allMatch(captor.getAllValues().get(0).getAllocationGroupId()::equals);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsAllocationWhenPersistedRoundedRowsWouldExceedTotal() {
|
||||
ownedPlan();
|
||||
when(feeItems.findItem(11L)).thenReturn(item());
|
||||
var fee = new ContainerFeeDetailDtos.SaveRequest(11L, "RECEIVABLE",
|
||||
LocalDate.of(2026, 8, 20), new BigDecimal("0.01"), "CNY", null, null);
|
||||
var request = new AllocationGroupRequest(fee, List.of(
|
||||
new Allocation(10001L, 501L, "FL-501", null, null, new BigDecimal("0.005")),
|
||||
new Allocation(10002L, 502L, "FL-502", null, null, new BigDecimal("0.005"))));
|
||||
|
||||
assertThatThrownBy(() -> service.createAllocationGroup(41L, 9L, request))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("分摊合计必须等于费用总额");
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsAllocationThatRoundsToZero() {
|
||||
ownedPlan();
|
||||
when(feeItems.findItem(11L)).thenReturn(item());
|
||||
var fee = new ContainerFeeDetailDtos.SaveRequest(11L, "RECEIVABLE",
|
||||
LocalDate.of(2026, 8, 20), new BigDecimal("0.00"), "CNY", null, null);
|
||||
var request = new AllocationGroupRequest(fee, List.of(
|
||||
new Allocation(10001L, 501L, "FL-501", null, null, new BigDecimal("0.001"))));
|
||||
|
||||
assertThatThrownBy(() -> service.createAllocationGroup(41L, 9L, request))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("取整后必须大于0");
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsGroupAdjustmentThatRoundsAnAllocationToZero() {
|
||||
ownedPlan();
|
||||
when(repository.listGroup(41L, 81L)).thenReturn(List.of(approvedFee(1)));
|
||||
var request = new ContainerFeeDetailDtos.AllocationGroupAdjustRequest(1,
|
||||
LocalDate.of(2026, 8, 20), new BigDecimal("0.001"), List.of(
|
||||
new ContainerFeeDetailDtos.AllocationAmount(51L, new BigDecimal("0.001"))));
|
||||
|
||||
assertThatThrownBy(() -> service.adjustAllocationGroup(41L, 81L, 9L, request))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("取整后必须大于0");
|
||||
verify(repository, never()).adjust(anyLong(), anyLong(), anyLong(), anyInt(), any(), any(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsAllocationWhenInboundIdAndCodeAreOnlyPartiallyProvided() {
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity();
|
||||
|
||||
+13
@@ -9,6 +9,19 @@ import java.nio.file.Path;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TerminalDrayageOpenApiContractTest {
|
||||
@Test
|
||||
void actualTimeRequestBindsRemarkFromPublicJson() throws Exception {
|
||||
var mapper = new com.fasterxml.jackson.databind.ObjectMapper().findAndRegisterModules();
|
||||
var request = mapper.readValue("""
|
||||
{"expectedVersion":3,"actualAt":"2026-09-04T09:00:00","remark":"已还柜"}
|
||||
""", com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.ActualTimeRequest.class);
|
||||
assertThat(request.remark()).isEqualTo("已还柜");
|
||||
assertThat(request.expectedVersion()).isEqualTo(3);
|
||||
String yaml = Files.readString(Path.of("src/main/resources/openapi/terminal-drayage-v1.yaml"));
|
||||
String schema = yaml.substring(yaml.indexOf(" ActualTimeRequest:"), yaml.indexOf(" Warning:"));
|
||||
assertThat(schema).contains("remark:", "expectedVersion:", "actualAt:").doesNotContain("reason:");
|
||||
}
|
||||
|
||||
@Test
|
||||
void controllerUsesFrozenOverseasPermissionsWithoutGenericContainerFallback() throws Exception {
|
||||
assertPermission("views", "platform:firstleg:terminal-drayage:list");
|
||||
|
||||
+20
@@ -177,6 +177,23 @@ class TerminalDrayageServiceTest {
|
||||
assertThat(result.allowedActions()).doesNotContain("RECORD_EMPTY_RETURN");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unloadedContainerOffersEmptyReturnOnlyWithDedicatedPermission() {
|
||||
Context context = context("PICKED_UP", LocalDateTime.of(2026, 9, 3, 0, 0));
|
||||
TerminalDrayageEntity row = row(3);
|
||||
row.setActualPickupAt(LocalDateTime.of(2026, 9, 2, 10, 0));
|
||||
when(mapper.context(11L)).thenReturn(context);
|
||||
when(mapper.find(11L)).thenReturn(row);
|
||||
|
||||
var raw = service.detail(11L);
|
||||
assertThat(raw.status()).isEqualTo("UNLOADED");
|
||||
assertThat(service.authorize(raw,
|
||||
Set.of("platform:firstleg:terminal-drayage:empty-return:confirm")).allowedActions())
|
||||
.containsExactly("RECORD_EMPTY_RETURN");
|
||||
assertThat(service.authorize(raw,
|
||||
Set.of("platform:firstleg:terminal-drayage:list")).allowedActions()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void bulkViewOmitsContainerWhenActualFactPortHasNoTrackingRecord() {
|
||||
when(mapper.contexts(List.of(11L))).thenReturn(List.of(
|
||||
@@ -276,6 +293,9 @@ class TerminalDrayageServiceTest {
|
||||
ArgumentCaptor<ManualNodeRequest> node = ArgumentCaptor.forClass(ManualNodeRequest.class);
|
||||
verify(carrierSchedules).addManualNode(anyLong(), anyLong(), anyString(), anyString(), node.capture());
|
||||
assertThat(node.getValue().standardNode()).isEqualTo("EMPTY_RETURNED");
|
||||
assertThat(node.getValue().reason()).isEqualTo("已还柜");
|
||||
assertThat(node.getValue().actualAt()).isEqualTo(LocalDateTime.of(2026, 9, 4, 9, 0));
|
||||
assertThat(result.version()).isEqualTo(4);
|
||||
}
|
||||
|
||||
private Context context(String node, LocalDateTime unloadedAt) {
|
||||
|
||||
Reference in New Issue
Block a user