This commit is contained in:
2026-08-13 11:57:33 +08:00
parent 5a48992d78
commit 16dc03a2e2
102 changed files with 3656 additions and 120 deletions
@@ -17,11 +17,27 @@ class ContainerControllerContractTest {
@DisplayName("查询、新增和编辑按钮权限彼此独立")
void separatesButtonPermissions() {
assertPermission("page", "platform:firstleg:container:list");
assertPermission("detail", "platform:firstleg:container:view");
assertPermission("detail", "platform:firstleg:container:list");
assertPermission("create", "platform:firstleg:container:create");
assertPermission("updateDraft", "platform:firstleg:container:edit");
assertPermission("updateTransport", "platform:firstleg:container:edit");
assertPermission("uploadClearance", "platform:firstleg:customs:manage");
assertPermission("overview", "platform:firstleg:container:list");
assertPermission("markException", "platform:firstleg:container:edit");
assertPermission("cancelException", "platform:firstleg:container:edit");
assertPermission("logisticsDocument", "platform:firstleg:container:list");
assertPermission("uploadLogisticsDocument", "platform:firstleg:container:edit");
assertPermission("downloadDocument", "platform:firstleg:container:list");
}
@Test void overviewOpenApiFreezesContainerIdBasedPdaContract() throws Exception {
String openapi = new ClassPathResource("openapi/phase3-container-overview-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(openapi).contains("/api/platform/v1/containers/{id}/overview:",
"/documents/{documentType}/{fileId}/download:", "ExceptionRequest",
"enum: [DELIVERY_ORDER, PICKUP_ORDER]", "Idempotency-Key", "photoCounts:",
"downloadPath:", "version: {type: integer, minimum: 0}")
.doesNotContain("?containerNo", "containerNo in: query");
}
@Test
@@ -53,4 +69,5 @@ class ContainerControllerContractTest {
assertThat(method.getAnnotation(PreAuthorize.class).value())
.isEqualTo("hasAuthority('" + authority + "')");
}
}
@@ -29,13 +29,13 @@ class LoadingListControllerContractTest {
@Test
void permissionsMatch() throws Exception {
authority("preloadPage", "platform:firstleg:preload:list", AuthenticatedUser.class, PreloadQuery.class);
authority("preloadDetail", "platform:firstleg:preload:view", AuthenticatedUser.class, long.class, long.class);
authority("preloadDetail", "platform:firstleg:preload:list", AuthenticatedUser.class, long.class, long.class);
authority("updateTransport", "platform:firstleg:preload:edit", AuthenticatedUser.class,
long.class, long.class, String.class, TransportUpdateRequest.class);
authority("confirmedPage", "platform:firstleg:loading-list:list", AuthenticatedUser.class,
ConfirmedQuery.class);
authority("confirmedDetail", "platform:firstleg:loading-list:view", AuthenticatedUser.class, long.class, long.class);
authority("declaration", "platform:firstleg:loading-list:view", AuthenticatedUser.class, long.class, long.class);
authority("confirmedDetail", "platform:firstleg:loading-list:list", AuthenticatedUser.class, long.class, long.class);
authority("declaration", "platform:firstleg:loading-list:list", AuthenticatedUser.class, long.class, long.class);
authority("attachDeclaration", "platform:firstleg:customs:manage", AuthenticatedUser.class,
long.class, long.class, String.class, DocumentAttachRequest.class);
authority("uploadDeclaration", "platform:firstleg:customs:manage", AuthenticatedUser.class,
@@ -60,7 +60,7 @@ class LoadingListControllerContractTest {
.contains("expectedVersion")
.contains("createdFrom")
.contains("createdTo")
.contains("platform:firstleg:loading-list:view")
.contains("platform:firstleg:loading-list:list")
.contains("PreloadPageEnvelope")
.contains("ConfirmedPageEnvelope")
.contains("PreloadSummary")
@@ -21,8 +21,8 @@ class TruckAppointmentControllerContractTest {
@Test void openApiDocumentsLifecycleIdempotencyAndActualTimes() throws Exception {String s=new ClassPathResource("openapi/truck-appointments-v1.yaml").getContentAsString(StandardCharsets.UTF_8);assertThat(s).contains("/api/platform/v1/truck-appointments/{id}/{action}:","/api/platform/v1/truck-appointments/{id}/reschedule:","name: Idempotency-Key","required: true","actual event timestamps");}
@Test void methodsHavePlatformPermissionsAndAuthenticatedScope() {
Map<String,String> expected=Map.ofEntries(
Map.entry("page","platform:firstleg:truck-appointment:list"), Map.entry("detail","platform:firstleg:truck-appointment:view"),
Map.entry("history","platform:firstleg:truck-appointment:view"), Map.entry("attachments","platform:firstleg:truck-appointment:view"),
Map.entry("page","platform:firstleg:truck-appointment:list"), Map.entry("detail","platform:firstleg:truck-appointment:list"),
Map.entry("history","platform:firstleg:truck-appointment:list"), Map.entry("attachments","platform:firstleg:truck-appointment:list"),
Map.entry("attach","platform:firstleg:truck-appointment:edit"), Map.entry("upload","platform:firstleg:truck-appointment:edit"),
Map.entry("deleteAttachment","platform:firstleg:truck-appointment:edit"), Map.entry("create","platform:firstleg:truck-appointment:create"),
Map.entry("edit","platform:firstleg:truck-appointment:edit"), Map.entry("action","platform:firstleg:truck-appointment:operate"),
@@ -18,7 +18,7 @@ class PlatformOceanBookingControllerContractTest {
.containsExactly("/api/platform/v1/ocean-bookings");
Map<String, String> permissions = Map.ofEntries(
Map.entry("page", "platform:firstleg:ocean-booking:list"),
Map.entry("detail", "platform:firstleg:ocean-booking:view"),
Map.entry("detail", "platform:firstleg:ocean-booking:list"),
Map.entry("create", "platform:firstleg:ocean-booking:create"),
Map.entry("updateDraft", "platform:firstleg:ocean-booking:edit"),
Map.entry("submit", "platform:firstleg:ocean-booking:submit"),
@@ -0,0 +1,25 @@
package com.baiyue.firstleg.repository;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class ContainerOverviewSqlContractTest {
@Test void developmentAndUnifiedSchemasContainImmutableIdempotentExceptionHistory() throws Exception {
String development = new ClassPathResource("db/phase3-smart-loading-schema.sql")
.getContentAsString(StandardCharsets.UTF_8);
String migration = new ClassPathResource("db/phase3-container-overview.sql")
.getContentAsString(StandardCharsets.UTF_8);
String unified = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"));
for (String sql : java.util.List.of(development, migration, unified)) {
assertThat(sql).contains("fl_container_exception_history", "operation_id", "request_hash",
"uk_container_exception_operation", "trg_container_exception_history_before_update",
"trg_container_exception_history_before_delete");
}
}
}
@@ -0,0 +1,68 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.ContainerOverviewDtos.ExceptionRequest;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.mapper.ContainerOverviewMapper;
import com.baiyue.firstleg.repository.ContainerRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
class ContainerOverviewServiceTest {
@Mock ContainerRepository containers;
@Mock ContainerOverviewMapper mapper;
@InjectMocks ContainerOverviewService service;
@BeforeEach void setUp() { MockitoAnnotations.openMocks(this); }
@Test void markLocksContainerAndPersistsIdempotentImmutableHistory() {
ContainerBookingEntity booking = new ContainerBookingEntity(); booking.setId(41L); booking.setVersion(3);
when(containers.findForUpdate(41L)).thenReturn(booking);
service.mark(41L, 7L, "operator", "mark-1", new ExceptionRequest(3, "broken seal"));
verify(containers).findForUpdate(41L);
verify(mapper).insertException(anyLong(), eq(41L), eq("MARK"), eq("broken seal"), eq(7L),
eq("operator"), eq("mark-1"), matches("[0-9a-f]{64}"));
}
@Test void exactReplayReturnsWithoutLockingOrWriting() {
String hash = hash("MARK|41|3|broken seal");
when(mapper.exceptionOperation(41L, 7L, "mark-1")).thenReturn(new ContainerOverviewMapper.ExceptionRow(
1L, "MARK", "broken seal", 7L, "operator", LocalDateTime.now(), hash));
service.mark(41L, 7L, "operator", "mark-1", new ExceptionRequest(3, "broken seal"));
verifyNoInteractions(containers);
verify(mapper, never()).insertException(anyLong(), anyLong(), anyString(), anyString(), anyLong(),
anyString(), anyString(), anyString());
}
@Test void reusedIdempotencyKeyWithDifferentRequestFailsClosed() {
when(mapper.exceptionOperation(41L, 7L, "mark-1")).thenReturn(new ContainerOverviewMapper.ExceptionRow(
1L, "MARK", "other", 7L, "operator", LocalDateTime.now(), "a".repeat(64)));
assertThatThrownBy(() -> service.mark(41L, 7L, "operator", "mark-1",
new ExceptionRequest(3, "broken seal"))).isInstanceOf(BizException.class)
.hasMessageContaining("不同异常操作");
verifyNoInteractions(containers);
}
private static String hash(String value) {
try {
return java.util.HexFormat.of().formatHex(java.security.MessageDigest.getInstance("SHA-256")
.digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
} catch (java.security.NoSuchAlgorithmException exception) {
throw new IllegalStateException(exception);
}
}
}
@@ -441,6 +441,17 @@ class LoadingListServiceTest {
verify(repository, never()).createDocument(org.mockito.ArgumentMatchers.any());
}
@Test
void genericContainerDocumentsOnlyAcceptDeliveryAndPickupOrders() {
assertThatThrownBy(() -> service.containerLogisticsDocument(30L,
LoadingListService.DESTINATION_CLEARANCE)).isInstanceOf(BizException.class)
.hasMessageContaining("DELIVERY_ORDER或PICKUP_ORDER");
assertThatThrownBy(() -> service.containerLogisticsDocument(30L,
LoadingListService.CUSTOMS_DECLARATION)).isInstanceOf(BizException.class)
.hasMessageContaining("DELIVERY_ORDER或PICKUP_ORDER");
verify(repository, never()).aggregateByBooking(30L);
}
private static AggregateRow row(String status, int version) {
AggregateRow row = new AggregateRow();
row.setPlanId(10L);