2026-07-30 18:01:36 +08:00
|
|
|
package com.baiyue.common.event;
|
|
|
|
|
|
|
|
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
2026-07-31 16:53:07 +08:00
|
|
|
import org.junit.jupiter.api.DisplayName;
|
2026-07-30 18:01:36 +08:00
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
|
|
|
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
|
|
import java.time.Instant;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
|
|
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
|
|
|
|
2026-09-29 18:47:37 +08:00
|
|
|
@DisplayName("跨服务事件信封契约")
|
2026-07-30 18:01:36 +08:00
|
|
|
class EventEnvelopeTest {
|
|
|
|
|
private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
|
|
|
|
|
|
|
|
|
|
@Test
|
2026-07-31 16:53:07 +08:00
|
|
|
void standardUuidEventPassesValidationAndSerializationRoundTrip() throws Exception {
|
2026-07-30 18:01:36 +08:00
|
|
|
EventEnvelope source = validEnvelope(EventIdentifiers.newUuid());
|
|
|
|
|
|
|
|
|
|
source.requireValid();
|
|
|
|
|
String json = objectMapper.writeValueAsString(source);
|
|
|
|
|
EventEnvelope restored = objectMapper.readValue(json, EventEnvelope.class);
|
|
|
|
|
|
|
|
|
|
assertThat(restored).isEqualTo(source);
|
2026-09-29 18:47:37 +08:00
|
|
|
assertThat(objectMapper.readTree(json).has("sourceRegion")).isFalse();
|
|
|
|
|
assertThat(objectMapper.readTree(json).has("targetRegion")).isFalse();
|
2026-07-30 18:01:36 +08:00
|
|
|
assertThat(objectMapper.readTree(json).path("eventType").asText())
|
|
|
|
|
.isEqualTo(EventTypes.FIRST_LEG_SHIPMENT_DEPARTED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
2026-07-31 16:53:07 +08:00
|
|
|
void validUlidCanBeUsedAsEventIdentifier() {
|
2026-07-30 18:01:36 +08:00
|
|
|
EventEnvelope envelope = validEnvelope("01J8Z4QX9K7M2N3P4R5S6T7V8W");
|
|
|
|
|
|
|
|
|
|
assertThat(EventEnvelopeValidator.validate(envelope)).isEmpty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
2026-07-31 16:53:07 +08:00
|
|
|
void validatorReturnsAllBasicErrorsAtOnce() {
|
2026-07-30 18:01:36 +08:00
|
|
|
EventEnvelope envelope = new EventEnvelope(
|
2026-09-29 18:47:37 +08:00
|
|
|
"bad-id", "bad_type", 0, null, " ", "bad-type", " ",
|
2026-07-30 18:01:36 +08:00
|
|
|
-1, 0L, " ", " ", "bad-cause", null, "bad-hash");
|
|
|
|
|
|
|
|
|
|
List<String> errors = EventEnvelopeValidator.validate(envelope);
|
|
|
|
|
|
|
|
|
|
assertThat(errors).hasSizeGreaterThanOrEqualTo(10);
|
|
|
|
|
assertThatThrownBy(envelope::requireValid)
|
|
|
|
|
.isInstanceOf(IllegalArgumentException.class)
|
|
|
|
|
.hasMessageContaining("eventId")
|
|
|
|
|
.hasMessageContaining("payloadHash");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
2026-07-31 16:53:07 +08:00
|
|
|
void payloadDigestCanBeCalculatedAndValidated() {
|
2026-07-30 18:01:36 +08:00
|
|
|
byte[] payload = "{\"orderId\":\"90001\"}".getBytes(StandardCharsets.UTF_8);
|
|
|
|
|
String hash = EventPayloadHash.sha256(payload);
|
|
|
|
|
|
|
|
|
|
assertThat(hash).matches("sha256:[0-9a-f]{64}");
|
|
|
|
|
assertThat(EventPayloadHash.matches(payload, hash)).isTrue();
|
|
|
|
|
assertThat(EventPayloadHash.matches("{}".getBytes(StandardCharsets.UTF_8), hash)).isFalse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
2026-07-31 16:53:07 +08:00
|
|
|
void differentObjectFieldOrderProducesSameCanonicalDigest() throws Exception {
|
2026-07-30 18:01:36 +08:00
|
|
|
JsonNode first = objectMapper.readTree("{\"orderId\":\"90001\",\"route\":{\"country\":\"US\",\"city\":\"LA\"}}");
|
|
|
|
|
JsonNode second = objectMapper.readTree("{\"route\":{\"city\":\"LA\",\"country\":\"US\"},\"orderId\":\"90001\"}");
|
|
|
|
|
|
|
|
|
|
assertThat(EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(first)))
|
|
|
|
|
.isEqualTo(EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(second)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
2026-07-31 16:53:07 +08:00
|
|
|
void arrayOrderIsPreservedAsBusinessSemantic() throws Exception {
|
2026-07-30 18:01:36 +08:00
|
|
|
JsonNode first = objectMapper.readTree("{\"steps\":[\"A\",\"B\"]}");
|
|
|
|
|
JsonNode second = objectMapper.readTree("{\"steps\":[\"B\",\"A\"]}");
|
|
|
|
|
|
|
|
|
|
assertThat(EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(first)))
|
|
|
|
|
.isNotEqualTo(EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(second)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private EventEnvelope validEnvelope(String eventId) {
|
|
|
|
|
JsonNode payload = objectMapper.createObjectNode()
|
|
|
|
|
.put("containerNo", "MSCU1234567")
|
|
|
|
|
.put("orderId", "90001");
|
|
|
|
|
byte[] canonicalPayload = payload.toString().getBytes(StandardCharsets.UTF_8);
|
|
|
|
|
return new EventEnvelope(
|
|
|
|
|
eventId,
|
|
|
|
|
EventTypes.FIRST_LEG_SHIPMENT_DEPARTED,
|
|
|
|
|
1,
|
|
|
|
|
Instant.parse("2026-07-29T08:30:00Z"),
|
|
|
|
|
"20001",
|
|
|
|
|
"FIRST_LEG_SHIPMENT",
|
|
|
|
|
"90001",
|
|
|
|
|
7,
|
|
|
|
|
1L,
|
|
|
|
|
"trace-90001",
|
|
|
|
|
"handover-90001",
|
|
|
|
|
null,
|
|
|
|
|
payload,
|
|
|
|
|
EventPayloadHash.sha256(canonicalPayload));
|
|
|
|
|
}
|
|
|
|
|
}
|