package com.baiyue.common.event; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.junit.jupiter.api.DisplayName; 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; @DisplayName("跨服务事件信封契约") class EventEnvelopeTest { private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); @Test void standardUuidEventPassesValidationAndSerializationRoundTrip() throws Exception { EventEnvelope source = validEnvelope(EventIdentifiers.newUuid()); source.requireValid(); String json = objectMapper.writeValueAsString(source); EventEnvelope restored = objectMapper.readValue(json, EventEnvelope.class); assertThat(restored).isEqualTo(source); assertThat(objectMapper.readTree(json).has("sourceRegion")).isFalse(); assertThat(objectMapper.readTree(json).has("targetRegion")).isFalse(); assertThat(objectMapper.readTree(json).path("eventType").asText()) .isEqualTo(EventTypes.FIRST_LEG_SHIPMENT_DEPARTED); } @Test void validUlidCanBeUsedAsEventIdentifier() { EventEnvelope envelope = validEnvelope("01J8Z4QX9K7M2N3P4R5S6T7V8W"); assertThat(EventEnvelopeValidator.validate(envelope)).isEmpty(); } @Test void validatorReturnsAllBasicErrorsAtOnce() { EventEnvelope envelope = new EventEnvelope( "bad-id", "bad_type", 0, null, " ", "bad-type", " ", -1, 0L, " ", " ", "bad-cause", null, "bad-hash"); List errors = EventEnvelopeValidator.validate(envelope); assertThat(errors).hasSizeGreaterThanOrEqualTo(10); assertThatThrownBy(envelope::requireValid) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("eventId") .hasMessageContaining("payloadHash"); } @Test void payloadDigestCanBeCalculatedAndValidated() { 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 void differentObjectFieldOrderProducesSameCanonicalDigest() throws Exception { 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 void arrayOrderIsPreservedAsBusinessSemantic() throws Exception { 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)); } }