Merge remote-tracking branch 'origin/test' into dev
This commit is contained in:
+28
-7
@@ -1,6 +1,5 @@
|
||||
package com.baiyue.firstleg.oceanbooking.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.PrintPdfClient;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.Detail;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity;
|
||||
@@ -22,7 +21,6 @@ import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@@ -60,11 +58,34 @@ class OceanBookingPrintServiceTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test void printRejectsDraftWithoutCallingPrintService() {
|
||||
when(bookings.detail(10L)).thenReturn(detail("DRAFT"));
|
||||
assertThatThrownBy(() -> service.billConfirmation(10L, "req-2"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("放舱后");
|
||||
verifyNoInteractions(print);
|
||||
@Test void bothDocumentsPrintDraftWithMissingReleaseData() {
|
||||
Detail detail = detail("DRAFT");
|
||||
detail.oceanBooking().setBillOfLadingNo(null);
|
||||
detail.oceanBooking().setPlaceOfDelivery(null);
|
||||
detail.oceanBooking().setServiceTerm(null);
|
||||
detail.oceanBooking().setFreightTerm(null);
|
||||
detail = new Detail(detail.oceanBooking(), List.of(detail.parties().get(0)),
|
||||
detail.containerRequirements(), List.of(), List.of(), List.of());
|
||||
when(bookings.detail(10L)).thenReturn(detail);
|
||||
when(print.generateFreightA4Documents(eq("token"), eq("req-2"), any()))
|
||||
.thenReturn(ResponseEntity.ok().contentType(MediaType.APPLICATION_PDF).body(new byte[]{1, 2, 3}));
|
||||
|
||||
assertThat(service.shippingInstruction(10L, "req-2").content()).isNotEmpty();
|
||||
assertThat(service.billConfirmation(10L, "req-2").content()).isNotEmpty();
|
||||
|
||||
ArgumentCaptor<PrintPdfClient.FreightA4PdfRequest> request =
|
||||
ArgumentCaptor.forClass(PrintPdfClient.FreightA4PdfRequest.class);
|
||||
verify(print, times(2)).generateFreightA4Documents(eq("token"), eq("req-2"), request.capture());
|
||||
for (var captured : request.getAllValues()) {
|
||||
assertThat(captured.documents().get(0).fields())
|
||||
.containsEntry("shipper", "Shipper\nAddress")
|
||||
.containsEntry("consignee", "—")
|
||||
.containsEntry("notifyParty", "—")
|
||||
.containsEntry("placeOfDelivery", "—")
|
||||
.containsEntry("serviceTerm", "—")
|
||||
.containsEntry("freightTerm", "—");
|
||||
}
|
||||
assertThat(request.getValue().documents().get(0).fields()).containsEntry("bookingNumber", "—");
|
||||
}
|
||||
|
||||
private static Detail detail(String status) {
|
||||
|
||||
+3
@@ -28,6 +28,9 @@ class TerminalDrayageOpenApiContractTest {
|
||||
assertPermission("detail", "platform:firstleg:terminal-drayage:list");
|
||||
assertPermission("history", "platform:firstleg:terminal-drayage:list");
|
||||
assertPermission("selectFleet", "platform:firstleg:terminal-drayage:fleet:edit");
|
||||
assertPermission("sendPickupEmail", "platform:firstleg:terminal-drayage:fleet:edit");
|
||||
assertPermission("retryEmail", "platform:firstleg:terminal-drayage:fleet:edit");
|
||||
assertPermission("emails", "platform:firstleg:terminal-drayage:list");
|
||||
assertPermission("schedulePickup", "platform:firstleg:terminal-drayage:pickup-appointment:edit");
|
||||
assertPermission("recordPickup", "platform:firstleg:terminal-drayage:pickup:confirm");
|
||||
assertPermission("recordEmptyReturn", "platform:firstleg:terminal-drayage:empty-return:confirm");
|
||||
|
||||
+33
@@ -8,6 +8,39 @@ import java.nio.file.Path;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TerminalDrayageSqlContractTest {
|
||||
@Test
|
||||
void mailProjectionMatchesRecordAndPendingQueryBlocksEarlierUnsentMail() throws Exception {
|
||||
var mapperType = com.baiyue.firstleg.terminaldrayage.mapper.TerminalDrayageMapper.class;
|
||||
var fields = java.util.Arrays.stream(com.baiyue.firstleg.terminaldrayage.mapper.TerminalDrayageMapper.MailTask.class.getRecordComponents())
|
||||
.map(java.lang.reflect.RecordComponent::getName).toList();
|
||||
for (String name : java.util.List.of("mails", "lastPickupMail", "pendingMails")) {
|
||||
var method = java.util.Arrays.stream(mapperType.getMethods()).filter(value -> value.getName().equals(name)).findFirst().orElseThrow();
|
||||
String sql = String.join(" ", method.getAnnotation(org.apache.ibatis.annotations.Select.class).value());
|
||||
String projection = sql.substring(sql.indexOf("SELECT")+6, sql.indexOf("FROM"));
|
||||
var selected = java.util.Arrays.stream(projection.split(",")).map(String::trim)
|
||||
.map(value -> value.contains(" AS ") ? value.substring(value.indexOf(" AS ")+4) : value.replace("m.", "")).toList();
|
||||
assertThat(selected).isEqualTo(fields);
|
||||
if (name.equals("pendingMails")) assertThat(sql).contains("earlier.container_booking_id=m.container_booking_id", "earlier.id<m.id", "earlier.status<>'SENT'");
|
||||
}
|
||||
for (Path path : java.util.List.of(Path.of("src/main/resources/db/phase3-terminal-drayage-schema.sql"), Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"))) {
|
||||
assertThat(Files.readString(path)).contains("CREATE TABLE IF NOT EXISTS fl_terminal_drayage_mail (", "CHECK(status IN ('PENDING','SENDING','SENT','FAILED'))");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void bothSqlEntrypointsAllowReturnWithoutPickupAndRetainKnownTimeOrdering() throws Exception {
|
||||
for (Path path : java.util.List.of(
|
||||
Path.of("src/main/resources/db/phase3-terminal-drayage-schema.sql"),
|
||||
Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"))) {
|
||||
String sql = Files.readString(path);
|
||||
assertThat(sql).contains("actual_empty_return_at IS NULL OR actual_pickup_at IS NULL OR actual_empty_return_at >= actual_pickup_at")
|
||||
.doesNotContain("actual_empty_return_at IS NULL OR actual_pickup_at IS NOT NULL");
|
||||
}
|
||||
String release = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"));
|
||||
assertThat(release).contains("DROP CHECK chk_terminal_drayage_times",
|
||||
"ADD CONSTRAINT chk_terminal_drayage_times CHECK(");
|
||||
}
|
||||
|
||||
@Test
|
||||
void schemaKeepsOneAggregatePerContainerAndImmutableAudit() throws Exception {
|
||||
String sql = Files.readString(Path.of("src/main/resources/db/phase3-terminal-drayage-schema.sql"));
|
||||
|
||||
+2
-2
@@ -103,7 +103,7 @@ class ContainerWorkbenchServiceTest {
|
||||
var mapper = mock(com.baiyue.firstleg.terminaldrayage.mapper.TerminalDrayageMapper.class);
|
||||
var json = new com.fasterxml.jackson.databind.ObjectMapper().findAndRegisterModules();
|
||||
var realDrayages = new TerminalDrayageService(mapper,
|
||||
mock(com.baiyue.firstleg.service.WarehouseSupplierSnapshotService.class), carrierSchedules, json);
|
||||
mock(com.baiyue.firstleg.service.WarehouseSupplierSnapshotService.class), carrierSchedules, json, mock(TerminalDrayageMailService.class));
|
||||
ReflectionTestUtils.setField(service, "terminalDrayages", realDrayages);
|
||||
ContainerBookingEntity container = container();
|
||||
container.setUnloadingCompletedAt(LocalDateTime.of(2026, 9, 5, 8, 0));
|
||||
@@ -148,7 +148,7 @@ class ContainerWorkbenchServiceTest {
|
||||
var mapper = mock(com.baiyue.firstleg.terminaldrayage.mapper.TerminalDrayageMapper.class);
|
||||
var realDrayages = new TerminalDrayageService(mapper,
|
||||
mock(com.baiyue.firstleg.service.WarehouseSupplierSnapshotService.class), carrierSchedules,
|
||||
new com.fasterxml.jackson.databind.ObjectMapper().findAndRegisterModules());
|
||||
new com.fasterxml.jackson.databind.ObjectMapper().findAndRegisterModules(), mock(TerminalDrayageMailService.class));
|
||||
ReflectionTestUtils.setField(service, "terminalDrayages", realDrayages);
|
||||
ContainerBookingEntity container = container();
|
||||
LocalDateTime unloadedAt = LocalDateTime.of(2026, 9, 5, 8, 0);
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.baiyue.firstleg.terminaldrayage.service;
|
||||
|
||||
import com.baiyue.firstleg.terminaldrayage.mapper.TerminalDrayageMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mail.MailSendException;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
import org.springframework.mail.javamail.JavaMailSenderImpl;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.util.List;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
|
||||
class TerminalDrayageMailDispatcherTest {
|
||||
TerminalDrayageMapper mapper = mock(TerminalDrayageMapper.class);
|
||||
JavaMailSenderImpl sender = mock(JavaMailSenderImpl.class);
|
||||
TerminalDrayageMailDispatcher worker = spy(new TerminalDrayageMailDispatcher(mapper));
|
||||
|
||||
void setup() {
|
||||
ReflectionTestUtils.setField(worker,"from","operations@example.com");
|
||||
doReturn(sender).when(worker).sender();
|
||||
when(mapper.pendingMails()).thenReturn(List.of(new TerminalDrayageMapper.MailTask(
|
||||
1L,11L,2L,"Fleet","fleet@example.com","REQUEST_PICKUP","Pickup","Body","PENDING",0,null,null,null)));
|
||||
when(mapper.claimMail(1L)).thenReturn(1);
|
||||
when(mapper.completeMail(1L)).thenReturn(1);
|
||||
}
|
||||
|
||||
@Test void onlyClaimsOnceAndMarksSentAfterSmtpAccepted() {
|
||||
setup(); worker.dispatch();
|
||||
var ordered = inOrder(mapper,sender);
|
||||
ordered.verify(mapper).claimMail(1L);
|
||||
ordered.verify(sender).send(any(SimpleMailMessage.class));
|
||||
ordered.verify(mapper).completeMail(1L);
|
||||
when(mapper.claimMail(1L)).thenReturn(0);
|
||||
worker.dispatch();
|
||||
verify(sender,times(1)).send(any(SimpleMailMessage.class));
|
||||
}
|
||||
|
||||
@Test void smtpFailureRequiresReviewInsteadOfReportingSuccess() {
|
||||
setup(); doThrow(new MailSendException("uncertain")).when(sender).send(any(SimpleMailMessage.class));
|
||||
worker.dispatch();
|
||||
verify(mapper).failMail(eq(1L),contains("结果不确定"));
|
||||
verify(mapper,never()).completeMail(anyLong());
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package com.baiyue.firstleg.terminaldrayage.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.WarehouseSupplierClient;
|
||||
import com.baiyue.firstleg.terminaldrayage.mapper.TerminalDrayageMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
|
||||
class TerminalDrayageMailServiceTest {
|
||||
TerminalDrayageMapper mapper = mock(TerminalDrayageMapper.class);
|
||||
WarehouseSupplierClient suppliers = mock(WarehouseSupplierClient.class);
|
||||
TerminalDrayageMailService service = new TerminalDrayageMailService(mapper, suppliers);
|
||||
|
||||
@BeforeEach void setup() {
|
||||
ReflectionTestUtils.setField(service, "internalToken", "test-token");
|
||||
when(suppliers.truckingContact("test-token", 1L, false)).thenReturn(ApiResponse.success(
|
||||
new WarehouseSupplierClient.TruckingContact(1L, "Old Fleet", "old@example.com")));
|
||||
when(suppliers.truckingContact("test-token", 2L, true)).thenReturn(ApiResponse.success(
|
||||
new WarehouseSupplierClient.TruckingContact(2L, "New Fleet", "new@example.com")));
|
||||
}
|
||||
|
||||
@Test void replacementQueuesCancellationThenRequestWithSeparateFrozenRecipients() {
|
||||
service.replaceFleet(11L, "MSCU1234567", 1L, 2L, 7L);
|
||||
var order = inOrder(mapper);
|
||||
order.verify(mapper).insertMail(anyLong(), eq(11L), eq(1L), eq("Old Fleet"), eq("old@example.com"),
|
||||
eq("CANCEL_PICKUP"), eq("Cancel port pickup - MSCU1234567"), contains("Please cancel"), eq(7L));
|
||||
order.verify(mapper).insertMail(anyLong(), eq(11L), eq(2L), eq("New Fleet"), eq("new@example.com"),
|
||||
eq("REQUEST_PICKUP"), eq("Port pickup request - MSCU1234567"), contains("Please arrange"), eq(7L));
|
||||
}
|
||||
|
||||
@Test void cancellationUsesOriginalRecipientEvenIfSupplierEmailChanged() {
|
||||
when(mapper.lastPickupMail(11L, 1L)).thenReturn(new TerminalDrayageMapper.MailTask(
|
||||
90L,11L,1L,"Frozen Fleet","original@example.com","REQUEST_PICKUP","subject","body","SENT",1,null,null,null));
|
||||
service.replaceFleet(11L, "MSCU1234567", 1L, 2L, 7L);
|
||||
verify(suppliers, never()).truckingContact("test-token", 1L, false);
|
||||
verify(mapper).insertMail(anyLong(), eq(11L), eq(1L), eq("Frozen Fleet"), eq("original@example.com"),
|
||||
eq("CANCEL_PICKUP"), anyString(), anyString(), eq(7L));
|
||||
}
|
||||
|
||||
@Test void missingNewEmailQueuesNeitherMail() {
|
||||
when(suppliers.truckingContact("test-token", 2L, true)).thenReturn(ApiResponse.success(
|
||||
new WarehouseSupplierClient.TruckingContact(2L, "New Fleet", null)));
|
||||
assertThatThrownBy(() -> service.replaceFleet(11L, "MSCU1234567", 1L, 2L, 7L)).isInstanceOf(BizException.class);
|
||||
verify(mapper, never()).insertMail(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString(), anyString(), anyString(), anyLong());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings={"", "not-an-email", "a@example.com,b@example.com", "a@example.com\r\nBcc: b@example.com", "Name <a@example.com>"})
|
||||
void rejectsInvalidOrMultipleRecipients(String email) {
|
||||
assertThatThrownBy(() -> TerminalDrayageMailService.validateAddress(email)).isInstanceOf(BizException.class);
|
||||
}
|
||||
|
||||
@Test void retriesOnlyFailedMailBelongingToContainer() {
|
||||
assertThatThrownBy(() -> service.retry(11L, 55L, 7L)).isInstanceOf(BizException.class);
|
||||
when(mapper.retryMail(11L,55L,7L)).thenReturn(1);
|
||||
service.retry(11L,55L,7L);
|
||||
}
|
||||
}
|
||||
+47
-2
@@ -37,6 +37,7 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class TerminalDrayageServiceTest {
|
||||
@@ -45,6 +46,7 @@ class TerminalDrayageServiceTest {
|
||||
private WarehouseSupplierSnapshotService suppliers;
|
||||
private CarrierScheduleQueryService carrierSchedules;
|
||||
private TerminalDrayageService service;
|
||||
private TerminalDrayageMailService mails;
|
||||
private Deque<ContainerActualFacts> facts;
|
||||
|
||||
@BeforeEach
|
||||
@@ -57,7 +59,8 @@ class TerminalDrayageServiceTest {
|
||||
if (facts.isEmpty()) return List.of();
|
||||
return List.of(facts.size() > 1 ? facts.removeFirst() : facts.peekFirst());
|
||||
});
|
||||
service = new TerminalDrayageService(mapper, suppliers, carrierSchedules, new ObjectMapper().findAndRegisterModules());
|
||||
mails = mock(TerminalDrayageMailService.class);
|
||||
service = new TerminalDrayageService(mapper, suppliers, carrierSchedules, new ObjectMapper().findAndRegisterModules(), mails);
|
||||
service.setClock(Clock.fixed(Instant.parse("2026-09-05T02:00:00Z"), ZoneOffset.UTC));
|
||||
}
|
||||
|
||||
@@ -95,7 +98,7 @@ class TerminalDrayageServiceTest {
|
||||
assertThat(result.version()).isEqualTo(1);
|
||||
assertThat(result.fleetSupplierId()).isEqualTo(91L);
|
||||
assertThat(result.fleetSupplierName()).isEqualTo("服务端车队");
|
||||
assertThat(result.allowedActions()).containsExactlyInAnyOrder("SELECT_FLEET", "SCHEDULE_PICKUP");
|
||||
assertThat(result.allowedActions()).containsExactlyInAnyOrder("SELECT_FLEET", "SCHEDULE_PICKUP", "SEND_PICKUP_EMAIL");
|
||||
verify(mapper).insertHistory(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString(),
|
||||
anyInt(), anyInt(), any(), anyLong(), anyString());
|
||||
verify(mapper).insertOperation(anyLong(), anyLong(), anyString(), anyString(), anyString(), anyInt(), anyLong());
|
||||
@@ -343,6 +346,48 @@ class TerminalDrayageServiceTest {
|
||||
assertThat(result.version()).isEqualTo(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectingSameFleetDoesNotQueueMailOrChangeVersion() {
|
||||
when(mapper.lockContext(11L)).thenReturn(context("ARRIVED_DESTINATION_PORT", null));
|
||||
when(mapper.lock(11L)).thenReturn(row(3));
|
||||
when(suppliers.trucking(91L, null)).thenReturn(new WarehouseSupplierClient.Snapshot(91L,"车队","TRUCKING","ACTIVE",List.of()));
|
||||
service.selectFleet(11L,7L,"admin","same-fleet",new FleetSelectionRequest(3,91L,null,null));
|
||||
verifyNoInteractions(mails);
|
||||
verify(mapper,never()).updateAggregate(any(),anyInt(),anyLong(),anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fleetReplacementQueuesBothEmailsBeforeSavingNewFleet() {
|
||||
Context context = context("ARRIVED_DESTINATION_PORT",null);
|
||||
when(mapper.lockContext(11L)).thenReturn(context);
|
||||
when(mapper.context(11L)).thenReturn(context);
|
||||
TerminalDrayageEntity row = row(3);
|
||||
when(mapper.lock(11L)).thenReturn(row);
|
||||
when(mapper.find(11L)).thenReturn(row);
|
||||
when(mapper.updateAggregate(any(),anyInt(),anyLong(),anyString())).thenReturn(1);
|
||||
when(suppliers.trucking(92L,null)).thenReturn(new WarehouseSupplierClient.Snapshot(92L,"新车队","TRUCKING","ACTIVE",List.of()));
|
||||
service.selectFleet(11L,7L,"admin","change-fleet",new FleetSelectionRequest(3,92L,null,null));
|
||||
var ordered = org.mockito.Mockito.inOrder(mails,mapper);
|
||||
ordered.verify(mails).replaceFleet(11L,"MSCU1234567",91L,92L,7L);
|
||||
ordered.verify(mapper).updateAggregate(any(),org.mockito.ArgumentMatchers.eq(3),org.mockito.ArgumentMatchers.eq(7L),org.mockito.ArgumentMatchers.eq("admin"));
|
||||
assertThat(row.getFleetSupplierId()).isEqualTo(92L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualPickupEmailUsesLockedVersionAndRejectsStaleClick() {
|
||||
Context context = context("ARRIVED_DESTINATION_PORT",null);
|
||||
when(mapper.lockContext(11L)).thenReturn(context);
|
||||
when(mapper.context(11L)).thenReturn(context);
|
||||
TerminalDrayageEntity row = row(3);
|
||||
when(mapper.lock(11L)).thenReturn(row);
|
||||
when(mapper.find(11L)).thenReturn(row);
|
||||
when(mapper.updateAggregate(any(),anyInt(),anyLong(),anyString())).thenAnswer(invocation -> {row.setVersion(4);return 1;});
|
||||
var request = new com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.EmailRequest(3);
|
||||
service.sendPickupEmail(11L,7L,"admin","mail-1",request);
|
||||
assertThatThrownBy(() -> service.sendPickupEmail(11L,7L,"admin","mail-2",request)).isInstanceOf(BizException.class);
|
||||
verify(mails,times(1)).request(11L,"MSCU1234567",91L,7L);
|
||||
}
|
||||
|
||||
private Context context(String node, LocalDateTime unloadedAt) {
|
||||
int sequenceVersion = switch (node) {
|
||||
case "PICKED_UP" -> 5;
|
||||
|
||||
Reference in New Issue
Block a user