package com.baiyue.eventbridge.service; import com.baiyue.common.event.EventEnvelope; import com.baiyue.common.event.EventPayloadCanonicalizer; import com.baiyue.common.event.EventPayloadHash; import com.baiyue.common.event.RegionCode; import com.baiyue.eventbridge.domain.BridgeInbox; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.dao.DuplicateKeyException; import org.springframework.test.util.ReflectionTestUtils; 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.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class EventInboxServiceTest { private final ObjectMapper objectMapper = new ObjectMapper(); private final InboxStore store = mock(InboxStore.class); private final EventInboxService service = service("OVERSEAS"); @Test void 首次事件落库并返回已接收() { EventEnvelope envelope = envelope("sha256:placeholder"); envelope = withHash(envelope); assertThat(service.receive(envelope).result()).isEqualTo("已接收"); verify(store).insert(any(BridgeInbox.class)); } @Test void 拒绝旧数字规范化摘要() { EventEnvelope source = envelope("sha256:placeholder"); source.payload().withObject("/snapshot").put("weight", 30.0).put("volume", 0.3150); String legacyHash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalizeLegacy(source.payload())); EventEnvelope envelope = new EventEnvelope(source.eventId(), source.eventType(), source.eventVersion(), source.occurredAt(), source.sourceRegion(), source.targetRegion(), source.tenantId(), source.aggregateType(), source.aggregateId(), source.aggregateVersion(), source.handoverVersion(), source.traceId(), source.correlationId(), source.causationId(), source.payload(), legacyHash); assertThatThrownBy(() -> service.receive(envelope)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("payloadHash"); } @Test void 相同事件和摘要重复接收不再次写入() { EventEnvelope envelope = withHash(envelope("sha256:placeholder")); BridgeInbox existing = existing(envelope, envelope.payloadHash()); when(store.findByEventId(envelope.eventId())).thenReturn(existing); assertThat(service.receive(envelope).result()).isEqualTo("重复"); } @Test void 相同事件但摘要不同时拒绝覆盖() { EventEnvelope envelope = withHash(envelope("sha256:placeholder")); when(store.findByEventId(envelope.eventId())).thenReturn(existing(envelope, "sha256:" + "0".repeat(64))); assertThatThrownBy(() -> service.receive(envelope)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("拒绝覆盖"); } @Test void 相同事件和摘要但聚合标识不同时拒绝覆盖() { EventEnvelope envelope = withHash(envelope("sha256:placeholder")); EventEnvelope conflicting = new EventEnvelope(envelope.eventId(), envelope.eventType(), envelope.eventVersion(), envelope.occurredAt(), envelope.sourceRegion(), envelope.targetRegion(), envelope.tenantId(), envelope.aggregateType(), "DIFFERENT", envelope.aggregateVersion(), envelope.handoverVersion(), envelope.traceId(), envelope.correlationId(), envelope.causationId(), envelope.payload(), envelope.payloadHash()); when(store.findByEventId(conflicting.eventId())).thenReturn(existing(envelope, envelope.payloadHash())); assertThatThrownBy(() -> service.receive(conflicting)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("事件信封"); } @Test void 并发插入冲突后按既有记录判定为重复() { EventEnvelope envelope = withHash(envelope("sha256:placeholder")); when(store.findByEventId(envelope.eventId())).thenReturn(null, existing(envelope, envelope.payloadHash())); doThrow(new DuplicateKeyException("并发重复")).when(store).insert(any()); assertThat(service.receive(envelope).result()).isEqualTo("重复"); } @Test void 目标区域与当前部署区域不一致时拒绝接收() { EventInboxService cnService = service("CN"); EventEnvelope envelope = withHash(envelope("sha256:placeholder")); assertThatThrownBy(() -> cnService.receive(envelope)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("目标区域"); } private EventEnvelope envelope(String hash) { return new EventEnvelope("550e8400-e29b-41d4-a716-446655440000", "OrderApproved", 1, Instant.parse("2026-07-29T08:30:00Z"), RegionCode.CN, RegionCode.OVERSEAS, "20001", "ORDER", "90001", 1, null, "trace-1", "flow-1", null, objectMapper.createObjectNode().put("orderId", "90001"), hash); } private EventInboxService service(String region) { EventInboxService service = new EventInboxService(); ReflectionTestUtils.setField(service, "inboxStore", store); ReflectionTestUtils.setField(service, "objectMapper", objectMapper); ReflectionTestUtils.setField(service, "localRegion", region); return service; } private EventEnvelope withHash(EventEnvelope source) { String hash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(source.payload())); return new EventEnvelope(source.eventId(), source.eventType(), source.eventVersion(), source.occurredAt(), source.sourceRegion(), source.targetRegion(), source.tenantId(), source.aggregateType(), source.aggregateId(), source.aggregateVersion(), source.handoverVersion(), source.traceId(), source.correlationId(), source.causationId(), source.payload(), hash); } private BridgeInbox existing(EventEnvelope envelope, String hash) { BridgeInbox inbox = new BridgeInbox(); inbox.setEventId(envelope.eventId()); inbox.setEventType(envelope.eventType()); inbox.setEventVersion(envelope.eventVersion()); inbox.setSourceRegion(envelope.sourceRegion().name()); inbox.setTargetRegion(envelope.targetRegion().name()); inbox.setCustomerId(envelope.tenantId()); inbox.setAggregateType(envelope.aggregateType()); inbox.setAggregateId(envelope.aggregateId()); inbox.setAggregateVersion(envelope.aggregateVersion()); inbox.setHandoverVersion(envelope.handoverVersion()); inbox.setOccurredAt(envelope.occurredAt()); inbox.setTraceId(envelope.traceId()); inbox.setCorrelationId(envelope.correlationId()); inbox.setCausationId(envelope.causationId()); inbox.setPayloadHash(hash); inbox.setReceivedAt(LocalDateTime.of(2026, 7, 29, 18, 0)); return inbox; } }