暂存提交
This commit is contained in:
+6
-20
@@ -1,43 +1,29 @@
|
||||
package com.baiyue.eventbridge.security;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
@DisplayName("内部服务令牌校验")
|
||||
@SpringJUnitConfig(InternalServiceTokenGuard.class)
|
||||
@TestPropertySource(properties = "baiyue.security.internal-service-token=a-secure-internal-token")
|
||||
class InternalServiceTokenGuardTest {
|
||||
@Resource private InternalServiceTokenGuard guard;
|
||||
|
||||
@AfterEach
|
||||
void resetToken() {
|
||||
ReflectionTestUtils.setField(guard, "expectedToken", "a-secure-internal-token");
|
||||
}
|
||||
@Test
|
||||
void validTokenAllowsAccess() {
|
||||
void 正确令牌允许访问() {
|
||||
InternalServiceTokenGuard guard = new InternalServiceTokenGuard("a-secure-internal-token");
|
||||
assertThatCode(() -> guard.verify("a-secure-internal-token")).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void unconfiguredTokenDisablesReceiveEndpoint() {
|
||||
ReflectionTestUtils.setField(guard, "expectedToken", "");
|
||||
void 未配置令牌时默认关闭接收接口() {
|
||||
InternalServiceTokenGuard guard = new InternalServiceTokenGuard("");
|
||||
assertThatThrownBy(() -> guard.verify("anything"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("接口已关闭");
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidTokenIsRejected() {
|
||||
ReflectionTestUtils.setField(guard, "expectedToken", "expected");
|
||||
void 错误令牌被拒绝() {
|
||||
InternalServiceTokenGuard guard = new InternalServiceTokenGuard("expected");
|
||||
assertThatThrownBy(() -> guard.verify("wrong"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("令牌无效");
|
||||
|
||||
+37
-91
@@ -6,16 +6,8 @@ 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 jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -25,35 +17,17 @@ 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;
|
||||
|
||||
@DisplayName("跨区域事件收件箱")
|
||||
@SpringJUnitConfig(classes = {EventInboxService.class, EventInboxServiceTest.TestConfig.class})
|
||||
@TestPropertySource(properties = "baiyue.region=OVERSEAS")
|
||||
class EventInboxServiceTest {
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
@MockBean
|
||||
private InboxStore store;
|
||||
@Resource
|
||||
private EventInboxService service;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfig {
|
||||
@Bean
|
||||
ObjectMapper objectMapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void resetRegion() {
|
||||
ReflectionTestUtils.setField(service, "localRegion", "OVERSEAS");
|
||||
}
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final InboxStore store = mock(InboxStore.class);
|
||||
private final EventInboxService service = service("OVERSEAS");
|
||||
|
||||
@Test
|
||||
void firstEventPersistsAndReturnsReceived() {
|
||||
void 首次事件落库并返回已接收() {
|
||||
EventEnvelope envelope = envelope("sha256:placeholder");
|
||||
envelope = withHash(envelope);
|
||||
|
||||
@@ -62,19 +36,31 @@ class EventInboxServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void identicalEventAndDigestDoesNotPersistAgain() {
|
||||
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);
|
||||
|
||||
assertThat(service.receive(envelope).result()).isEqualTo("已接收");
|
||||
}
|
||||
|
||||
@Test
|
||||
void 相同事件和摘要重复接收不再次写入() {
|
||||
EventEnvelope envelope = withHash(envelope("sha256:placeholder"));
|
||||
BridgeInbox existing = existing(envelope, envelope.payloadHash());
|
||||
BridgeInbox existing = existing(envelope.payloadHash());
|
||||
when(store.findByEventId(envelope.eventId())).thenReturn(existing);
|
||||
|
||||
assertThat(service.receive(envelope).result()).isEqualTo("重复");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameEventWithDifferentDigestRejectsOverwrite() {
|
||||
void 相同事件但摘要不同时拒绝覆盖() {
|
||||
EventEnvelope envelope = withHash(envelope("sha256:placeholder"));
|
||||
when(store.findByEventId(envelope.eventId()))
|
||||
.thenReturn(existing(envelope, "sha256:" + "0".repeat(64)));
|
||||
when(store.findByEventId(envelope.eventId())).thenReturn(existing("sha256:" + "0".repeat(64)));
|
||||
|
||||
assertThatThrownBy(() -> service.receive(envelope))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
@@ -82,45 +68,20 @@ class EventInboxServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameEventAndPayloadWithDifferentTenantRejectsOverwrite() {
|
||||
EventEnvelope original = withHash(envelope("sha256:placeholder"));
|
||||
EventEnvelope otherTenant = withTenant(original, "20002");
|
||||
when(store.findByEventId(otherTenant.eventId()))
|
||||
.thenReturn(existing(original, original.payloadHash()));
|
||||
|
||||
assertThatThrownBy(() -> service.receive(otherTenant))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("信封身份不一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameEventAndPayloadWithDifferentAggregateVersionRejectsOverwrite() {
|
||||
EventEnvelope original = withHash(envelope("sha256:placeholder"));
|
||||
EventEnvelope otherEnvelope = withAggregateVersion(original, 2L);
|
||||
when(store.findByEventId(otherEnvelope.eventId()))
|
||||
.thenReturn(existing(original, original.payloadHash()));
|
||||
|
||||
assertThatThrownBy(() -> service.receive(otherEnvelope))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("信封身份不一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateInsertAfterRaceIsTreatedAsDuplicate() {
|
||||
void 并发插入冲突后按既有记录判定为重复() {
|
||||
EventEnvelope envelope = withHash(envelope("sha256:placeholder"));
|
||||
when(store.findByEventId(envelope.eventId()))
|
||||
.thenReturn(null, existing(envelope, envelope.payloadHash()));
|
||||
when(store.findByEventId(envelope.eventId())).thenReturn(null, existing(envelope.payloadHash()));
|
||||
doThrow(new DuplicateKeyException("并发重复")).when(store).insert(any());
|
||||
|
||||
assertThat(service.receive(envelope).result()).isEqualTo("重复");
|
||||
}
|
||||
|
||||
@Test
|
||||
void targetRegionDifferentFromCurrentDeploymentIsRejected() {
|
||||
ReflectionTestUtils.setField(service, "localRegion", "CN");
|
||||
void 目标区域与当前部署区域不一致时拒绝接收() {
|
||||
EventInboxService cnService = service("CN");
|
||||
EventEnvelope envelope = withHash(envelope("sha256:placeholder"));
|
||||
|
||||
assertThatThrownBy(() -> service.receive(envelope))
|
||||
assertThatThrownBy(() -> cnService.receive(envelope))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("目标区域");
|
||||
}
|
||||
@@ -132,6 +93,14 @@ class EventInboxServiceTest {
|
||||
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(),
|
||||
@@ -140,32 +109,9 @@ class EventInboxServiceTest {
|
||||
source.correlationId(), source.causationId(), source.payload(), hash);
|
||||
}
|
||||
|
||||
private EventEnvelope withTenant(EventEnvelope source, String tenantId) {
|
||||
return new EventEnvelope(source.eventId(), source.eventType(), source.eventVersion(), source.occurredAt(),
|
||||
source.sourceRegion(), source.targetRegion(), tenantId, source.aggregateType(),
|
||||
source.aggregateId(), source.aggregateVersion(), source.handoverVersion(), source.traceId(),
|
||||
source.correlationId(), source.causationId(), source.payload(), source.payloadHash());
|
||||
}
|
||||
|
||||
private EventEnvelope withAggregateVersion(EventEnvelope source, Long aggregateVersion) {
|
||||
return new EventEnvelope(source.eventId(), source.eventType(), source.eventVersion(), source.occurredAt(),
|
||||
source.sourceRegion(), source.targetRegion(), source.tenantId(), source.aggregateType(),
|
||||
source.aggregateId(), aggregateVersion, source.handoverVersion(), source.traceId(),
|
||||
source.correlationId(), source.causationId(), source.payload(), source.payloadHash());
|
||||
}
|
||||
|
||||
private BridgeInbox existing(EventEnvelope envelope, String hash) {
|
||||
private BridgeInbox existing(String hash) {
|
||||
BridgeInbox inbox = new BridgeInbox();
|
||||
inbox.setEventId(envelope.eventId());
|
||||
inbox.setTenantId(envelope.tenantId());
|
||||
inbox.setEventType(envelope.eventType());
|
||||
inbox.setEventVersion(envelope.eventVersion());
|
||||
inbox.setSourceRegion(envelope.sourceRegion().name());
|
||||
inbox.setTargetRegion(envelope.targetRegion().name());
|
||||
inbox.setAggregateType(envelope.aggregateType());
|
||||
inbox.setAggregateId(envelope.aggregateId());
|
||||
inbox.setAggregateVersion(envelope.aggregateVersion());
|
||||
inbox.setHandoverVersion(envelope.handoverVersion());
|
||||
inbox.setEventId("550e8400-e29b-41d4-a716-446655440000");
|
||||
inbox.setPayloadHash(hash);
|
||||
inbox.setReceivedAt(LocalDateTime.of(2026, 7, 29, 18, 0));
|
||||
return inbox;
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.baiyue.eventbridge.service;
|
||||
|
||||
import com.baiyue.eventbridge.mapper.BridgeInboxMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class EventReplayServiceTest {
|
||||
private final BridgeInboxMapper mapper = mock(BridgeInboxMapper.class);
|
||||
private final EventReplayService service = service();
|
||||
|
||||
@Test
|
||||
void requeuesDeadEventWithAuditReason() {
|
||||
when(mapper.replayDead(eq("event-1"), contains("incident-42"))).thenReturn(1);
|
||||
|
||||
service.replayDead("event-1", "operator-7", "incident-42 approved");
|
||||
|
||||
verify(mapper).replayDead(eq("event-1"), contains("operator-7"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsEventThatIsNotDead() {
|
||||
when(mapper.replayDead(anyString(), anyString())).thenReturn(0);
|
||||
|
||||
assertThatThrownBy(() -> service.replayDead("event-1", "operator-7", "retry"))
|
||||
.isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
private EventReplayService service() {
|
||||
EventReplayService service = new EventReplayService();
|
||||
ReflectionTestUtils.setField(service, "inboxMapper", mapper);
|
||||
return service;
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package com.baiyue.eventbridge.service;
|
||||
|
||||
import com.baiyue.eventbridge.config.EventDispatcherProperties;
|
||||
import com.baiyue.eventbridge.domain.BridgeDeliveryAttempt;
|
||||
import com.baiyue.eventbridge.domain.BridgeInbox;
|
||||
import com.baiyue.eventbridge.mapper.BridgeDeliveryAttemptMapper;
|
||||
import com.baiyue.eventbridge.mapper.BridgeInboxMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class OverseasEventDispatcherTest {
|
||||
private final BridgeInboxMapper inboxMapper = mock(BridgeInboxMapper.class);
|
||||
private final BridgeDeliveryAttemptMapper attemptMapper = mock(BridgeDeliveryAttemptMapper.class);
|
||||
private final LastlegEventClient eventClient = mock(LastlegEventClient.class);
|
||||
private OverseasEventDispatcher dispatcher;
|
||||
private BridgeInbox event;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
EventDispatcherProperties properties = new EventDispatcherProperties(
|
||||
URI.create("https://lastleg.internal/events"), 50, 3,
|
||||
Duration.ofSeconds(30), Duration.ofSeconds(5), Duration.ofMinutes(1),
|
||||
Duration.ofSeconds(3), Duration.ofSeconds(10));
|
||||
dispatcher = new OverseasEventDispatcher();
|
||||
ReflectionTestUtils.setField(dispatcher, "inboxMapper", inboxMapper);
|
||||
ReflectionTestUtils.setField(dispatcher, "attemptMapper", attemptMapper);
|
||||
ReflectionTestUtils.setField(dispatcher, "eventClient", eventClient);
|
||||
ReflectionTestUtils.setField(dispatcher, "properties", properties);
|
||||
event = new BridgeInbox();
|
||||
event.setEventId("event-1");
|
||||
event.setSourceRegion("CN");
|
||||
event.setTargetRegion("OVERSEAS");
|
||||
event.setRetryCount(0);
|
||||
when(inboxMapper.claim(eq("event-1"), anyString(), anyLong())).thenReturn(1);
|
||||
when(inboxMapper.selectById("event-1")).thenReturn(event);
|
||||
when(inboxMapper.nextAttemptNo("event-1")).thenReturn(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfulDeliveryIsConfirmedAndAudited() throws Exception {
|
||||
when(eventClient.deliver(event)).thenReturn("200");
|
||||
when(inboxMapper.markProcessed(eq("event-1"), anyString())).thenReturn(1);
|
||||
|
||||
dispatcher.dispatchOne("event-1");
|
||||
|
||||
verify(inboxMapper).markProcessed(eq("event-1"), anyString());
|
||||
assertThat(updatedAttempt().getStatus()).isEqualTo("DELIVERED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedDeliveryIsScheduledForRetry() throws Exception {
|
||||
when(eventClient.deliver(event)).thenThrow(new IllegalStateException("network down"));
|
||||
|
||||
dispatcher.dispatchOne("event-1");
|
||||
|
||||
verify(inboxMapper).markFailed(eq("event-1"), anyString(), eq(3), anyLong(), eq("network down"));
|
||||
assertThat(updatedAttempt().getStatus()).isEqualTo("RETRY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void finalFailureIsMovedToDeadState() throws Exception {
|
||||
event.setRetryCount(2);
|
||||
when(eventClient.deliver(event)).thenThrow(new IllegalStateException("still down"));
|
||||
|
||||
dispatcher.dispatchOne("event-1");
|
||||
|
||||
assertThat(updatedAttempt().getStatus()).isEqualTo("DEAD");
|
||||
}
|
||||
|
||||
@Test
|
||||
void eventClaimedByAnotherInstanceIsSkipped() {
|
||||
when(inboxMapper.claim(eq("event-1"), anyString(), anyLong())).thenReturn(0);
|
||||
|
||||
dispatcher.dispatchOne("event-1");
|
||||
|
||||
verifyNoInteractions(eventClient);
|
||||
verify(attemptMapper, never()).insert(any(BridgeDeliveryAttempt.class));
|
||||
}
|
||||
|
||||
private BridgeDeliveryAttempt updatedAttempt() {
|
||||
ArgumentCaptor<BridgeDeliveryAttempt> captor = ArgumentCaptor.forClass(BridgeDeliveryAttempt.class);
|
||||
verify(attemptMapper).updateById(captor.capture());
|
||||
return captor.getValue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user