This commit is contained in:
2026-09-29 17:55:54 +08:00
parent 41d8387cd5
commit 54ad26c607
111 changed files with 1837 additions and 962 deletions
@@ -2,12 +2,13 @@ package com.baiyue.lastleg.inboundplan.pda.service;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.event.EventEnvelope;
import com.baiyue.common.event.DeliveryAck;
import com.baiyue.common.event.EventPayloadCanonicalizer;
import com.baiyue.common.event.EventPayloadHash;
import com.baiyue.common.event.EventTypes;
import com.baiyue.common.event.OverseasContainerUnloadedV1;
import com.baiyue.common.event.RegionCode;
import com.baiyue.lastleg.client.EventBridgeClient;
import com.baiyue.lastleg.client.DirectEventClient;
import com.baiyue.lastleg.entity.LastlegEventOutbox;
import com.baiyue.lastleg.repository.UnloadCompletedOutboxRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -30,7 +31,7 @@ import com.baiyue.common.core.api.ResultCode;
@ExtendWith(MockitoExtension.class)
class InboundUnloadCompletedOutboxRelayTest {
@Mock private UnloadCompletedOutboxRepository repository;
@Mock private EventBridgeClient client;
@Mock private DirectEventClient client;
private ObjectMapper objectMapper;
private InboundUnloadCompletedOutboxRelay relay;
@@ -43,19 +44,18 @@ class InboundUnloadCompletedOutboxRelayTest {
ReflectionTestUtils.setField(relay, "objectMapper", objectMapper);
ReflectionTestUtils.setField(relay, "internalToken", "internal-token");
ReflectionTestUtils.setField(relay, "leaseSeconds", 60L);
ReflectionTestUtils.setField(relay, "retrySeconds", 30L);
ReflectionTestUtils.setField(relay, "baseDelaySeconds", 5L);
}
@Test
void marksOutboxPublishedAfterDomesticBridgeAcknowledgesSameEvent() throws Exception {
void marksOutboxPublishedAfterConsumerAcknowledgesSameEvent() throws Exception {
EventEnvelope event = event();
LastlegEventOutbox 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")));
.thenReturn(ApiResponse.success(DeliveryAck.of(event, DeliveryAck.Outcome.APPLIED)));
relay.relay();
@@ -64,23 +64,23 @@ class InboundUnloadCompletedOutboxRelayTest {
}
@Test
void releasesLeaseForRetryWhenBridgeDeliveryFails() throws Exception {
void releasesLeaseForRetryWhenConsumerDeliveryFails() throws Exception {
EventEnvelope event = event();
LastlegEventOutbox 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("bridge unavailable"));
.thenThrow(new IllegalStateException("consumer unavailable"));
relay.relay();
verify(repository).retry(eq(9001L), anyString(), any(LocalDateTime.class), eq("bridge unavailable"));
verify(repository).retry(eq(9001L), anyString(), any(LocalDateTime.class), eq("consumer unavailable"));
verify(repository, never()).published(anyLong(), anyString(), any());
}
@Test
void preservesDomesticBridgeBusinessErrorForDeploymentDiagnosis() throws Exception {
void preservesConsumerBusinessErrorForDeploymentDiagnosis() throws Exception {
EventEnvelope event = event();
LastlegEventOutbox row = row(event);
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
@@ -88,15 +88,58 @@ class InboundUnloadCompletedOutboxRelayTest {
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
when(client.receive(eq("internal-token"), any(EventEnvelope.class)))
.thenReturn(ApiResponse.failure(ResultCode.BAD_REQUEST,
"事件目标区域与当前事件桥区域不一致", "request-bridge", "trace-bridge"));
"消费事件类型不支持", "request-bridge", "trace-bridge"));
relay.relay();
verify(repository).retry(eq(9001L), anyString(), any(LocalDateTime.class),
contains("事件目标区域与当前事件桥区域不一致"));
contains("消费事件类型不支持"));
verify(repository, never()).published(anyLong(), anyString(), any());
}
@Test
void mismatchedHashOrUnallowedStaleCannotMarkPublished() throws Exception {
EventEnvelope event = event();
LastlegEventOutbox 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 DeliveryAck(event.eventId(), "sha256:" + "0".repeat(64), DeliveryAck.Outcome.APPLIED)))
.thenReturn(ApiResponse.success(DeliveryAck.of(event, DeliveryAck.Outcome.STALE)));
relay.relay();
relay.relay();
verify(repository, times(2)).retry(eq(9001L), anyString(), any(), anyString());
verify(repository, never()).published(anyLong(), anyString(), any());
}
@Test
void twelfthFailureGoesDeadInsteadOfSchedulingAnotherRetry() throws Exception {
LastlegEventOutbox row = row(event());
row.setRetryCount(11);
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(anyString(), any())).thenThrow(new IllegalStateException("consumer unavailable"));
relay.relay();
verify(repository).dead(eq(9001L), anyString(), eq(12), contains("consumer unavailable"));
verify(repository, never()).retry(anyLong(), anyString(), any(), anyString());
verify(repository, never()).published(anyLong(), anyString(), any());
}
@Test
void recoveredExhaustedLeaseDoesNotSendAgain() throws Exception {
LastlegEventOutbox row = row(event());
row.setRetryCount(12);
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);
relay.relay();
verifyNoInteractions(client);
verify(repository).dead(eq(9001L), anyString(), eq(12), contains("租约超时"));
verify(repository, never()).retry(anyLong(), anyString(), any(), anyString());
}
private LastlegEventOutbox row(EventEnvelope event) throws Exception {
LastlegEventOutbox row = new LastlegEventOutbox();
row.setId(9001L);