优化
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.firstleg.dto.ContainerOverviewDtos.ContainerBookingEntityView;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ContainerOverviewTransportContractTest {
|
||||
@Test
|
||||
void overviewPublishesMaritimeAndEmptyReportFacts() {
|
||||
assertThat(Arrays.stream(ContainerBookingEntityView.class.getRecordComponents()).map(value -> value.getName()))
|
||||
.contains("billOfLadingNo", "shippingCompanyId", "shippingCompanyName", "departurePortCode",
|
||||
"departurePortName", "destinationPortCode", "destinationPortName", "estimatedSailingAt",
|
||||
"actualSailingAt", "estimatedPortArrivalAt", "actualPortArrivalAt",
|
||||
"estimatedWarehouseArrivalAt", "actualWarehouseArrivalAt", "unloadingCompletedAt",
|
||||
"unloadingPhotoUploadedAt", "estimatedEmptyReportedAt", "actualEmptyReportedAt",
|
||||
"emptyReportSourceVersion");
|
||||
}
|
||||
}
|
||||
+21
@@ -8,6 +8,7 @@ import com.baiyue.firstleg.outbound.dispatch.gate.OutboundDispatchGateContributo
|
||||
import com.baiyue.firstleg.outbound.dispatch.mapper.OutboundDispatchReadMapper;
|
||||
import com.baiyue.firstleg.outbound.dispatch.repository.OutboundDispatchRepository;
|
||||
import com.baiyue.firstleg.outbound.dispatch.service.OutboundDispatchService;
|
||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
@@ -67,6 +68,26 @@ class OutboundDispatchServiceTest {
|
||||
verify(repository).completeOperation(33L, 9L, "CONFIRM_DISPATCH", "confirm-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmCreatesCanonicalCarrierScheduleTaskInTheSameTransaction() {
|
||||
CarrierScheduleQueryService carrierSchedules = mock(CarrierScheduleQueryService.class);
|
||||
OutboundDispatchService service = new OutboundDispatchService(repository,
|
||||
List.of(passing("CUSTOMS_CLEARANCE"), passing("SAILING_SCHEDULE")), carrierSchedules);
|
||||
stubReadyChain();
|
||||
OutboundDispatchEntity state = new OutboundDispatchEntity();
|
||||
state.setId(33L); state.setTenantId(7L); state.setInboundId(11L);
|
||||
state.setContainerBookingId(22L); state.setStatus("READY_FOR_DISPATCH"); state.setVersion(0);
|
||||
when(repository.dispatch(7L, 11L, 22L)).thenReturn(state);
|
||||
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq("confirm-carrier"), anyString())).thenReturn(true);
|
||||
when(repository.confirm(33L, 0, 9L, "admin", null)).thenReturn(1);
|
||||
when(repository.completeOperation(33L, 9L, "CONFIRM_DISPATCH", "confirm-carrier")).thenReturn(1);
|
||||
when(repository.timeline(any(), any(), any())).thenReturn(List.of());
|
||||
|
||||
service.confirm(9L, "admin", "confirm-carrier", new ActionRequest(11L, 22L, 0, null));
|
||||
|
||||
verify(carrierSchedules).ensureFromOutboundConfirm(7L, 66L, 22L, 9L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmNeverWritesStateWhenAnyRealGateFails() {
|
||||
OutboundDispatchService service = new OutboundDispatchService(repository,
|
||||
|
||||
+5
-7
@@ -9,19 +9,17 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CarrierScheduleQueryOpenApiContractTest {
|
||||
@Test
|
||||
void publishesFrequencyFreshnessAndManualFallbackContract() throws Exception {
|
||||
void publishesBusinessNodesWithoutSchedulerOrAttemptEndpoints() throws Exception {
|
||||
String yaml = new ClassPathResource("openapi/carrier-schedule-query-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(yaml)
|
||||
.contains("/api/platform/v1/carrier-schedule-queries:")
|
||||
.contains("/api/platform/v1/carrier-schedule-queries/{queryId}/attempts:")
|
||||
.contains("/api/platform/v1/carrier-schedule-queries/{queryId}/manual-results:")
|
||||
.contains("enum: [MATSON, SML, ZIM, COSCO, EMC, HEDE]")
|
||||
.contains("DEPARTED_WAREHOUSE, AT_ORIGIN_PORT, DEPARTED_ORIGIN_PORT, IN_TRANSIT, ARRIVED, PICKUP_READY")
|
||||
.contains("MANUAL_REQUIRED")
|
||||
.contains("coverageGap")
|
||||
.contains("lastAttemptAt")
|
||||
.contains("lastSuccessAt")
|
||||
.contains("sourceUpdatedAt")
|
||||
.contains("nextAllowedAt");
|
||||
.contains("scheduler leases and raw attempts are never exposed")
|
||||
.doesNotContain("/{queryId}/attempts:")
|
||||
.doesNotContain("port-schedule-query:attempt");
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -22,16 +22,17 @@ class PlatformCarrierScheduleQueryControllerContractTest {
|
||||
"detail", "platform:firstleg:port-schedule-query:list",
|
||||
"history", "platform:firstleg:port-schedule-query:list",
|
||||
"create", "platform:firstleg:port-schedule-query:create",
|
||||
"attempt", "platform:firstleg:port-schedule-query:attempt",
|
||||
"manualResult", "platform:firstleg:port-schedule-query:manual");
|
||||
permissions.forEach((name, permission) -> {
|
||||
Method method = Arrays.stream(PlatformCarrierScheduleQueryController.class.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals(name)).findFirst().orElseThrow();
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('" + permission + "')");
|
||||
if (name.equals("create") || name.equals("attempt") || name.equals("manualResult")) {
|
||||
if (name.equals("create") || name.equals("manualResult")) {
|
||||
assertThat(method.getAnnotation(BusinessAudit.class)).isNotNull();
|
||||
}
|
||||
});
|
||||
assertThat(Arrays.stream(PlatformCarrierScheduleQueryController.class.getDeclaredMethods())
|
||||
.map(Method::getName)).doesNotContain("attempt");
|
||||
}
|
||||
}
|
||||
|
||||
+9
-4
@@ -17,11 +17,16 @@ class CarrierScheduleQuerySqlContractTest {
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_attempt")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_history")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_operation")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_carrier_schedule_node_fact")
|
||||
.contains("attempt_count BETWEEN 0 AND 3")
|
||||
.contains("window_attempt_no BETWEEN 1 AND 3")
|
||||
.contains("UNIQUE KEY uk_carrier_schedule_operation (operator_id,idempotency_key)")
|
||||
.contains("船司查询尝试不可修改")
|
||||
.contains("船司查询历史不可删除");
|
||||
.contains("船司查询历史不可删除")
|
||||
.contains("标准船节点事实不可修改")
|
||||
.contains("ARRIVED','PICKUP_READY','STOPPED")
|
||||
.contains("lease_owner")
|
||||
.contains("consecutive_failures");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -29,9 +34,9 @@ class CarrierScheduleQuerySqlContractTest {
|
||||
assertThat(moduleSql())
|
||||
.contains("booking.id=NEW.ocean_booking_id AND booking.customer_id=NEW.customer_id")
|
||||
.contains("allocation.container_booking_id=NEW.container_booking_id")
|
||||
.contains("5102184 AND 5102188")
|
||||
.contains("platform:firstleg:port-schedule-query:attempt")
|
||||
.contains("id IN (5102184,5102186,5102188)")
|
||||
.contains("platform:firstleg:port-schedule-query:manual")
|
||||
.contains("'platform:firstleg:port-schedule-query:attempt'")
|
||||
.contains("role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')")
|
||||
.contains("船司查询任务角色授权未完整创建");
|
||||
}
|
||||
@@ -43,7 +48,7 @@ class CarrierScheduleQuerySqlContractTest {
|
||||
assertThat(unified)
|
||||
.contains("fl_carrier_schedule_query")
|
||||
.contains("uk_carrier_schedule_query_scope")
|
||||
.contains("5102184 AND 5102188")
|
||||
.contains("id IN (5102184,5102186,5102188)")
|
||||
.contains("assert_carrier_schedule_query_schema")
|
||||
.contains("船司查询任务角色授权未完整创建");
|
||||
}
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.baiyue.firstleg.portschedule.service;
|
||||
|
||||
import com.baiyue.firstleg.client.CarrierScheduleIntegrationClient;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryEntity;
|
||||
import com.baiyue.firstleg.portschedule.repository.CarrierScheduleQueryRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class CarrierScheduleQuerySchedulerTest {
|
||||
@Test
|
||||
void leaseWinnerPersistsStandardNodesAndCompletesAttempt() {
|
||||
CarrierScheduleQueryRepository repository = mock(CarrierScheduleQueryRepository.class);
|
||||
CarrierScheduleQueryService service = mock(CarrierScheduleQueryService.class);
|
||||
CarrierScheduleIntegrationClient integration = mock(CarrierScheduleIntegrationClient.class);
|
||||
CarrierScheduleQueryScheduler scheduler = new CarrierScheduleQueryScheduler(repository, service, integration);
|
||||
CarrierScheduleQueryEntity task = new CarrierScheduleQueryEntity(); task.setId(1L);
|
||||
task.setCarrierCode("COSCO"); task.setTrackingType("CONTAINER_NO"); task.setTrackingNo("CSNU1234567");
|
||||
LocalDateTime now = LocalDateTime.of(2026, 8, 20, 12, 0);
|
||||
when(repository.claimLease(anyLong(), anyString(), any(), any())).thenReturn(true);
|
||||
var observation = new CarrierScheduleIntegrationClient.Observation("evt-1", "DISCHARGED", "ARRIVED",
|
||||
now, null, now, "USLAX", "Los Angeles", "provider:evt-1",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
when(integration.query(any(), any())).thenReturn(new CarrierScheduleIntegrationClient.Envelope<>("200", null,
|
||||
new CarrierScheduleIntegrationClient.QueryResult("TRACK_API", "SUCCESS", List.of(observation),
|
||||
null, null, now)));
|
||||
|
||||
scheduler.execute(task, now);
|
||||
|
||||
verify(service).applyProviderObservation(anyLong(), any());
|
||||
verify(service).completeScheduledAttempt(1L, "SUCCESS", null, null);
|
||||
}
|
||||
}
|
||||
+31
-27
@@ -3,10 +3,9 @@ package com.baiyue.firstleg.portschedule.service;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.AttemptRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CreateRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualResultRequest;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryAttemptEntity;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ProviderObservationRequest;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.PortScheduleEntity;
|
||||
import com.baiyue.firstleg.portschedule.repository.CarrierScheduleQueryRepository;
|
||||
@@ -14,7 +13,6 @@ import com.baiyue.firstleg.portschedule.repository.PortScheduleRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
@@ -48,7 +46,7 @@ class CarrierScheduleQueryServiceTest {
|
||||
set("clock", Clock.fixed(Instant.parse("2026-08-10T04:00:00Z"), ZoneOffset.UTC));
|
||||
when(repository.claimOperation(any(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.completeOperation(anyLong(), anyLong(), anyString(), anyString())).thenReturn(1);
|
||||
when(repository.attempts(anyLong())).thenReturn(List.of());
|
||||
when(repository.nodes(anyLong())).thenReturn(List.of());
|
||||
when(repository.histories(anyLong())).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@@ -62,42 +60,48 @@ class CarrierScheduleQueryServiceTest {
|
||||
var result = service.create(7L, "admin", "carrier-create",
|
||||
new CreateRequest(0, 10001L, 10L, 20L, "ZIM", "BILL_OF_LADING", "GAOU6898053", null));
|
||||
|
||||
assertThat(result.query().query().getCarrierCode()).isEqualTo("ZIM");
|
||||
assertThat(result.query().query().getStatus()).isEqualTo("READY");
|
||||
assertThat(result.query().query().carrierCode()).isEqualTo("ZIM");
|
||||
assertThat(result.query().query().status()).isEqualTo("READY");
|
||||
assertThat(result.query().expired()).isTrue();
|
||||
verify(repository).insert(any(CarrierScheduleQueryEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void recordsManualRequiredAttemptWithoutBypassingCaptcha() {
|
||||
CarrierScheduleQueryEntity task = task(0, 0);
|
||||
when(repository.lock(1L)).thenReturn(task);
|
||||
when(repository.updateState(any(), any(Integer.class), anyLong(), anyString())).thenReturn(1);
|
||||
when(repository.find(1L)).thenAnswer(invocation -> task);
|
||||
void outboundConfirmationCreatesTaskWithContainerTrackingAndDepartureNode() {
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setCustomerId(10001L);
|
||||
booking.setShippingCompanyName("中远海运"); booking.setBookingNo("OB-260810-0001");
|
||||
var container = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
container.setContainerNo("CSNU1234567");
|
||||
when(portSchedules.oceanBooking(10L)).thenReturn(booking);
|
||||
when(portSchedules.containerBooking(20L)).thenReturn(container);
|
||||
when(portSchedules.allocation(10L, 20L)).thenReturn(new OceanBookingContainerAllocationEntity());
|
||||
|
||||
var result = service.attempt(1L, 7L, "admin", "carrier-attempt",
|
||||
new AttemptRequest(0, "官网查询"));
|
||||
CarrierScheduleQueryEntity result = service.ensureFromOutboundConfirm(10001L, 10L, 20L, 7L, "admin");
|
||||
|
||||
assertThat(result.query().query().getStatus()).isEqualTo("MANUAL_REQUIRED");
|
||||
assertThat(result.query().query().getAttemptCount()).isEqualTo(1);
|
||||
assertThat(result.query().query().getManualReason()).contains("hCaptcha");
|
||||
ArgumentCaptor<CarrierScheduleQueryAttemptEntity> attempt =
|
||||
ArgumentCaptor.forClass(CarrierScheduleQueryAttemptEntity.class);
|
||||
verify(repository).insert(attempt.capture());
|
||||
assertThat(attempt.getValue().getCoverageGap()).isTrue();
|
||||
assertThat(attempt.getValue().getCoverageGapCode()).isEqualTo("HCAPTCHA_REQUIRED");
|
||||
assertThat(result.getTriggerSource()).isEqualTo("OUTBOUND_CONFIRM");
|
||||
assertThat(result.getCarrierCode()).isEqualTo("COSCO");
|
||||
assertThat(result.getTrackingType()).isEqualTo("CONTAINER_NO");
|
||||
assertThat(result.getLatestStandardNode()).isEqualTo("DEPARTED_WAREHOUSE");
|
||||
verify(repository).insertIfAbsent(any(com.baiyue.firstleg.portschedule.entity.CarrierScheduleNodeFactEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void blocksFourthAttemptInsideSameThreeDayWindow() {
|
||||
CarrierScheduleQueryEntity task = task(3, 4);
|
||||
void providerObservationAdvancesToArrivedWithoutExposingAttempts() {
|
||||
CarrierScheduleQueryEntity task = task(0, 0);
|
||||
task.setLatestStandardNode("IN_TRANSIT");
|
||||
when(repository.lock(1L)).thenReturn(task);
|
||||
when(repository.find(1L)).thenReturn(task);
|
||||
when(repository.insertIfAbsent(any(com.baiyue.firstleg.portschedule.entity.CarrierScheduleNodeFactEntity.class)))
|
||||
.thenReturn(true);
|
||||
when(repository.updateState(any(), any(Integer.class), anyLong(), anyString())).thenReturn(1);
|
||||
|
||||
assertThatThrownBy(() -> service.attempt(1L, 7L, "admin", "carrier-fourth",
|
||||
new AttemptRequest(4, null)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("最多查询3次");
|
||||
var result = service.applyProviderObservation(1L, new ProviderObservationRequest("TRACK_API", "evt-1",
|
||||
"DISCHARGED", "ARRIVED", NOW, null, NOW, "USLAX", "Los Angeles", "provider:evt-1",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
|
||||
|
||||
verify(repository, never()).updateState(any(), any(Integer.class), anyLong(), anyString());
|
||||
assertThat(result.query().query().status()).isEqualTo("ARRIVED");
|
||||
assertThat(result.query().query().latestStandardNode()).isEqualTo("ARRIVED");
|
||||
assertThat(result.nodes()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
.contains("存在缺失中文注释的业务表")
|
||||
.contains("存在缺失中文注释的业务字段")
|
||||
.doesNotContain("CREATE PROCEDURE upgrade_column_comment");
|
||||
assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(150);
|
||||
assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(151);
|
||||
assertThat(count(sql, "CALL queue_column_comment(")).isGreaterThanOrEqualTo(2459);
|
||||
assertThat(sql)
|
||||
.contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'supplier_id'")
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
import com.baiyue.common.event.FirstLegLoadingPlanCompletedV1;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.firstleg.client.EventBridgeClient;
|
||||
import com.baiyue.firstleg.entity.InboundOutboxEntity;
|
||||
import com.baiyue.firstleg.repository.LoadingCompletedOutboxRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LoadingCompletedOutboxRelayTest {
|
||||
@Mock private LoadingCompletedOutboxRepository repository;
|
||||
@Mock private EventBridgeClient client;
|
||||
private ObjectMapper objectMapper;
|
||||
private LoadingCompletedOutboxRelay relay;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
relay = new LoadingCompletedOutboxRelay();
|
||||
ReflectionTestUtils.setField(relay, "repository", repository);
|
||||
ReflectionTestUtils.setField(relay, "client", client);
|
||||
ReflectionTestUtils.setField(relay, "objectMapper", objectMapper);
|
||||
ReflectionTestUtils.setField(relay, "internalToken", "internal-token");
|
||||
ReflectionTestUtils.setField(relay, "leaseSeconds", 60L);
|
||||
ReflectionTestUtils.setField(relay, "retrySeconds", 30L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void claimsExpiredLeasesAndPublishesAfterOverseasBridgeAcknowledgement() throws Exception {
|
||||
EventEnvelope event = event();
|
||||
InboundOutboxEntity row = row(event);
|
||||
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
|
||||
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
|
||||
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
|
||||
when(client.receive(eq("internal-token"), any(EventEnvelope.class))).thenReturn(ApiResponse.success(
|
||||
new EventBridgeClient.Acknowledgement(event.eventId(), "已接收", "2026-08-20T03:01:00")));
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(repository).published(eq(9001L), anyString(), any(LocalDateTime.class));
|
||||
verify(repository, never()).retry(anyLong(), anyString(), any(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void redactsInternalTokenBeforePersistingRetryError() throws Exception {
|
||||
EventEnvelope event = event();
|
||||
InboundOutboxEntity row = row(event);
|
||||
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
|
||||
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
|
||||
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
|
||||
when(client.receive(eq("internal-token"), any(EventEnvelope.class)))
|
||||
.thenThrow(new IllegalStateException("failed internal-token\nremote"));
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(repository).retry(eq(9001L), anyString(), any(LocalDateTime.class), eq("failed *** remote"));
|
||||
}
|
||||
|
||||
private InboundOutboxEntity row(EventEnvelope event) throws Exception {
|
||||
InboundOutboxEntity row = new InboundOutboxEntity();
|
||||
row.setId(9001L);
|
||||
row.setEventKey(event.eventId());
|
||||
row.setPayloadJson(objectMapper.writeValueAsString(event));
|
||||
return row;
|
||||
}
|
||||
|
||||
private EventEnvelope event() {
|
||||
var payload = objectMapper.valueToTree(new FirstLegLoadingPlanCompletedV1(
|
||||
"LOAD-1", "PLAN-1", "BOOK-1", "MSCU1234567", null,
|
||||
"WH-1", "Warehouse", List.of()));
|
||||
return new EventEnvelope("00000000-0000-4000-8000-000000000019", EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED, 1,
|
||||
Instant.parse("2026-08-20T03:00:01Z"), RegionCode.CN, RegionCode.OVERSEAS, "1",
|
||||
"LOADING_PLAN", "LOAD-1", 7L, 1L, "trace-12345678", "correlation-12345678", null,
|
||||
payload, EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload)));
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -43,12 +43,12 @@ class OverseasContainerEventConsumerTest {
|
||||
@Test
|
||||
void derivesEstimatedEmptyReportedAtFromPhotoUploadAndPersistsInbox() {
|
||||
ContainerBookingEntity container = container();
|
||||
when(containers.selectActiveByContainerNo("MSCU1234567")).thenReturn(container);
|
||||
when(containers.selectActiveBookingForUpdate(1001L)).thenReturn(container);
|
||||
Instant completedAt = Instant.parse("2026-08-20T03:00:00Z");
|
||||
Instant uploadedAt = Instant.parse("2026-08-20T02:30:00Z");
|
||||
|
||||
consumer.consume(event("00000000-0000-4000-8000-000000000001", 7,
|
||||
new OverseasContainerUnloadedV1("MSCU1234567", completedAt, uploadedAt, null)));
|
||||
new OverseasContainerUnloadedV1("1001", "MSCU1234567", completedAt, uploadedAt, null)));
|
||||
|
||||
verify(containers).applyEmptyReport(eq(1001L), eq(LocalDateTime.parse("2026-08-20T03:00:00")),
|
||||
eq(LocalDateTime.parse("2026-08-20T02:30:00")),
|
||||
@@ -62,13 +62,13 @@ class OverseasContainerEventConsumerTest {
|
||||
|
||||
@Test
|
||||
void repeatedEventWithSameHashDoesNotApplyAgain() {
|
||||
OverseasContainerUnloadedV1 payload = new OverseasContainerUnloadedV1("MSCU1234567",
|
||||
OverseasContainerUnloadedV1 payload = new OverseasContainerUnloadedV1("1001", "MSCU1234567",
|
||||
Instant.parse("2026-08-20T03:00:00Z"), Instant.parse("2026-08-20T02:30:00Z"), null);
|
||||
EventEnvelope event = event("00000000-0000-4000-8000-000000000002", 7, payload);
|
||||
ContainerEventInboxEntity existing = new ContainerEventInboxEntity();
|
||||
existing.setPayloadHash(event.payloadHash());
|
||||
when(inbox.selectById(event.eventId())).thenReturn(existing);
|
||||
when(containers.selectActiveByContainerNo("MSCU1234567")).thenReturn(container());
|
||||
when(containers.selectActiveBookingForUpdate(1001L)).thenReturn(container());
|
||||
|
||||
consumer.consume(event);
|
||||
|
||||
@@ -79,7 +79,7 @@ class OverseasContainerEventConsumerTest {
|
||||
@Test
|
||||
void rejectsTamperedPayloadHash() {
|
||||
EventEnvelope valid = event("00000000-0000-4000-8000-000000000003", 7,
|
||||
new OverseasContainerUnloadedV1("MSCU1234567", Instant.parse("2026-08-20T03:00:00Z"),
|
||||
new OverseasContainerUnloadedV1("1001", "MSCU1234567", Instant.parse("2026-08-20T03:00:00Z"),
|
||||
Instant.parse("2026-08-20T02:30:00Z"), null));
|
||||
EventEnvelope tampered = new EventEnvelope(valid.eventId(), valid.eventType(), valid.eventVersion(),
|
||||
valid.occurredAt(), valid.sourceRegion(), valid.targetRegion(), valid.tenantId(), valid.aggregateType(),
|
||||
@@ -96,7 +96,7 @@ class OverseasContainerEventConsumerTest {
|
||||
String hash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node));
|
||||
return new EventEnvelope(eventId, EventTypes.OVERSEAS_CONTAINER_UNLOADED, 1,
|
||||
Instant.parse("2026-08-20T02:31:00Z"), RegionCode.OVERSEAS, RegionCode.CN, "1",
|
||||
"CONTAINER_BOOKING", payload.containerNo(), aggregateVersion, null,
|
||||
"CONTAINER_BOOKING", payload.sourceContainerBookingId(), aggregateVersion, null,
|
||||
"trace-12345678", "correlation-12345678", null, node, hash);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user