This commit is contained in:
2026-08-20 15:20:05 +08:00
parent 6c7a670f1e
commit b55b049e77
30 changed files with 727 additions and 12 deletions
@@ -0,0 +1,108 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.event.EventEnvelope;
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.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.entity.ContainerEventInboxEntity;
import com.baiyue.firstleg.mapper.ContainerBookingMapper;
import com.baiyue.firstleg.mapper.ContainerEventInboxMapper;
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.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.Instant;
import java.time.LocalDateTime;
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.*;
@ExtendWith(MockitoExtension.class)
class OverseasContainerEventConsumerTest {
@Mock private ContainerBookingMapper containers;
@Mock private ContainerEventInboxMapper inbox;
private ObjectMapper objectMapper;
private OverseasContainerEventConsumer consumer;
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
consumer = new OverseasContainerEventConsumer(containers, inbox, objectMapper);
}
@Test
void derivesEstimatedEmptyReportedAtFromPhotoUploadAndPersistsInbox() {
ContainerBookingEntity container = container();
when(containers.selectActiveByContainerNo("MSCU1234567")).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)));
verify(containers).applyEmptyReport(eq(1001L), eq(LocalDateTime.parse("2026-08-20T03:00:00")),
eq(LocalDateTime.parse("2026-08-20T02:30:00")),
eq(LocalDateTime.parse("2026-08-22T02:30:00")), isNull(), eq(7L));
ArgumentCaptor<ContainerEventInboxEntity> captor = ArgumentCaptor.forClass(ContainerEventInboxEntity.class);
verify(inbox).insert(captor.capture());
assertThat(captor.getValue().getAggregateId()).isEqualTo("1001");
assertThat(captor.getValue().getStatus()).isEqualTo("PROCESSED");
verify(inbox).updateById(captor.getValue());
}
@Test
void repeatedEventWithSameHashDoesNotApplyAgain() {
OverseasContainerUnloadedV1 payload = new OverseasContainerUnloadedV1("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());
consumer.consume(event);
verify(containers, never()).applyEmptyReport(anyLong(), any(), any(), any(), any(), anyLong());
verify(inbox, never()).insert(any(ContainerEventInboxEntity.class));
}
@Test
void rejectsTamperedPayloadHash() {
EventEnvelope valid = event("00000000-0000-4000-8000-000000000003", 7,
new OverseasContainerUnloadedV1("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(),
valid.aggregateId(), valid.aggregateVersion(), valid.handoverVersion(), valid.traceId(),
valid.correlationId(), valid.causationId(), valid.payload(), "sha256:" + "0".repeat(64));
assertThatThrownBy(() -> consumer.consume(tampered))
.isInstanceOf(IllegalArgumentException.class).hasMessageContaining("payloadHash");
verifyNoInteractions(containers, inbox);
}
private EventEnvelope event(String eventId, long aggregateVersion, OverseasContainerUnloadedV1 payload) {
var node = objectMapper.valueToTree(payload);
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,
"trace-12345678", "correlation-12345678", null, node, hash);
}
private ContainerBookingEntity container() {
ContainerBookingEntity value = new ContainerBookingEntity();
value.setId(1001L); value.setContainerNo("MSCU1234567");
return value;
}
}