优化
This commit is contained in:
@@ -14,6 +14,8 @@
|
||||
<name>baiyue-common-web</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency><groupId>com.baiyue.shared</groupId><artifactId>common-event-contract</artifactId><version>${project.version}</version></dependency>
|
||||
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>test</scope></dependency>
|
||||
<dependency>
|
||||
<groupId>com.baiyue.shared</groupId>
|
||||
<artifactId>baiyue-common-core</artifactId>
|
||||
|
||||
@@ -3,6 +3,13 @@ package com.baiyue.common.web;
|
||||
import feign.Request;
|
||||
import feign.Capability;
|
||||
import feign.Retryer;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.DecodeException;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -22,6 +29,9 @@ public class EventDeliveryFeignConfiguration {
|
||||
}
|
||||
/** Applied at Feign build time after global/client properties, without changing other clients. */
|
||||
public static final class DeliveryTransportCapability implements Capability {
|
||||
private static final ObjectMapper RESPONSE_JSON = new ObjectMapper()
|
||||
.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION)
|
||||
.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
|
||||
private final Request.Options options;
|
||||
public DeliveryTransportCapability(Request.Options options) { this.options = options; }
|
||||
@Override public Request.Options enrich(Request.Options ignored) { return options; }
|
||||
@@ -29,5 +39,26 @@ public class EventDeliveryFeignConfiguration {
|
||||
// The persisted Outbox owns the retry budget.
|
||||
return Retryer.NEVER_RETRY;
|
||||
}
|
||||
@Override public Decoder enrich(Decoder decoder) {
|
||||
return (response, type) -> {
|
||||
if (type instanceof ParameterizedType parameterized && parameterized.getRawType() == ApiResponse.class) {
|
||||
// Jackson normally coerces a numeric 200 into String. Inspect the wire value first.
|
||||
if (response.body() == null) throw new DecodeException(response.status(), "Missing event response", response.request());
|
||||
byte[] bytes;
|
||||
try (var stream = response.body().asInputStream()) { bytes = stream.readNBytes(65537); }
|
||||
if (bytes.length > 65536) throw new DecodeException(response.status(), "Event response exceeds limit", response.request());
|
||||
try {
|
||||
var root = RESPONSE_JSON.readTree(bytes);
|
||||
if (root == null || !root.isObject() || !root.path("code").isTextual()) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
} catch (Exception invalid) {
|
||||
throw new DecodeException(response.status(), "Invalid event response contract", response.request());
|
||||
}
|
||||
return decoder.decode(response.toBuilder().body(bytes).build(), type);
|
||||
}
|
||||
return decoder.decode(response, type);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import io.micrometer.core.instrument.Gauge;
|
||||
import com.baiyue.common.event.EventDeliveryRoute;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/** Fixed labels only. Scheduled samplers query the local schema; scrape callbacks only read memory. */
|
||||
public final class EventDeliveryMetrics {
|
||||
public enum Stream {
|
||||
FIRSTLEG("firstleg", "fl_inbound_outbox", "publish_status", "PROCESSING",
|
||||
List.of("FirstLegLoadingPlanCompleted", "FirstLegContainerShippingNodeRecorded")),
|
||||
LASTLEG("lastleg", "ll_event_outbox", "status", "PROCESSING",
|
||||
List.of("OverseasContainerUnloaded", "OverseasInboundOrderDetainedChanged", "OverseasDeliveryCompleted", "WalletCommandRequested")),
|
||||
FEE("lastleg", "ll_event_outbox", "status", "PROCESSING", List.of("BusinessFeeSourceChanged")),
|
||||
FINANCE("finance", "fin_wallet_event_outbox", "status", "SENDING", List.of("WalletCommandCompleted"));
|
||||
public final String service, table, status, processing;
|
||||
public final List<String> types;
|
||||
Stream(String service, String table, String status, String processing, List<String> types) {
|
||||
this.service = service; this.table = table; this.status = status; this.processing = processing; this.types = types;
|
||||
}
|
||||
public String type(String candidate) { return candidate != null && types.contains(candidate) ? candidate : "UNKNOWN"; }
|
||||
public String target(String type) {
|
||||
if (!types.contains(type)) return "UNRESOLVED"; // Audit only; never a transport fallback.
|
||||
var route = EventDeliveryRoute.forEventType(type);
|
||||
return route.serviceName() + route.path();
|
||||
}
|
||||
}
|
||||
private record Snapshot(double pending, double dead, double oldestSeconds, double sampledAt, double success) { }
|
||||
private static final class Cell { volatile Snapshot value = new Snapshot(Double.NaN, Double.NaN, Double.NaN, 0, 0); }
|
||||
private final JdbcTemplate jdbc;
|
||||
private final MeterRegistry registry;
|
||||
private final Map<String, Cell> cells = new ConcurrentHashMap<>();
|
||||
public EventDeliveryMetrics(JdbcTemplate jdbc, MeterRegistry registry) { this.jdbc = jdbc; this.registry = registry; }
|
||||
|
||||
public void sample(Stream stream, LocalDateTime now) {
|
||||
for (String type : stream.types) {
|
||||
Cell cell = cells.computeIfAbsent(stream.name() + ":" + type, ignored -> register(stream, type));
|
||||
try {
|
||||
String filter = stream == Stream.FINANCE ? "" : " WHERE event_type=?";
|
||||
String sql = "SELECT SUM(CASE WHEN " + stream.status + " IN ('PENDING','" + stream.processing
|
||||
+ "') THEN 1 ELSE 0 END) AS pending_count, SUM(CASE WHEN " + stream.status
|
||||
+ "='DEAD' THEN 1 ELSE 0 END) AS dead_count, MIN(CASE WHEN " + stream.status
|
||||
+ " IN ('PENDING','" + stream.processing + "') THEN created_at END) AS oldest FROM " + stream.table + filter;
|
||||
Map<String, Object> result = stream == Stream.FINANCE ? jdbc.queryForMap(sql) : jdbc.queryForMap(sql, type);
|
||||
LocalDateTime oldest = databaseTime(result.get("oldest"));
|
||||
cell.value = new Snapshot(count(result.get("pending_count")), count(result.get("dead_count")),
|
||||
oldest == null ? 0 : Math.max(0, Duration.between(oldest, now).toMillis() / 1000.0),
|
||||
System.currentTimeMillis() / 1000.0, 1);
|
||||
} catch (RuntimeException failure) {
|
||||
Snapshot old = cell.value;
|
||||
cell.value = new Snapshot(old.pending, old.dead, old.oldestSeconds, old.sampledAt, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
private Cell register(Stream stream, String type) {
|
||||
Cell cell = new Cell();
|
||||
Gauge.builder("baiyue.event.delivery.pending", cell, c -> c.value.pending).tags("service", stream.service, "event_type", type).register(registry);
|
||||
Gauge.builder("baiyue.event.delivery.dead", cell, c -> c.value.dead).tags("service", stream.service, "event_type", type).register(registry);
|
||||
Gauge.builder("baiyue.event.delivery.oldest.pending.seconds", cell, c -> c.value.oldestSeconds).tags("service", stream.service, "event_type", type).register(registry);
|
||||
Gauge.builder("baiyue.event.delivery.sample.timestamp", cell, c -> c.value.sampledAt).tags("service", stream.service, "event_type", type).register(registry);
|
||||
Gauge.builder("baiyue.event.delivery.sample.success", cell, c -> c.value.success).tags("service", stream.service, "event_type", type).register(registry);
|
||||
return cell;
|
||||
}
|
||||
private static LocalDateTime databaseTime(Object value) {
|
||||
if (value == null) return null;
|
||||
if (value instanceof LocalDateTime local) return local;
|
||||
if (value instanceof Timestamp timestamp) return timestamp.toLocalDateTime();
|
||||
throw new IllegalArgumentException("Unsupported Outbox timestamp type");
|
||||
}
|
||||
private static double count(Object value) { return value == null ? 0 : ((Number) value).doubleValue(); }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.baiyue.common.event.EventIdentifiers;
|
||||
import feign.FeignException;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Set;
|
||||
|
||||
/** Diagnostic identity and finite audit error codes; never serialize exception messages or payloads. */
|
||||
public final class EventDeliveryObservation {
|
||||
private EventDeliveryObservation() { }
|
||||
public static String eventId(ObjectMapper mapper, String json) {
|
||||
try {
|
||||
String id = mapper.readTree(json).path("eventId").asText("");
|
||||
return EventIdentifiers.isSupported(id) ? id : null;
|
||||
} catch (Exception invalid) { return null; } // Invalid payloads are still audited by Outbox entry ID.
|
||||
}
|
||||
public static String errorCode(Throwable error, boolean permanent, boolean requestStarted) {
|
||||
Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
for (Throwable cause = error; cause != null && seen.add(cause); cause = cause.getCause()) {
|
||||
if (cause instanceof FeignException http && http.status() >= 100 && http.status() <= 599) {
|
||||
return "HTTP_" + http.status();
|
||||
}
|
||||
}
|
||||
if (!requestStarted) return "INVALID_EVENT";
|
||||
return permanent ? "PERMANENT_FAILURE" : "DELIVERY_FAILURE";
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import com.baiyue.common.core.outbox.EventDeliveryJournal;
|
||||
import com.baiyue.common.core.outbox.EventDeliveryProducer;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
@AutoConfiguration(after = {JdbcTemplateAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class})
|
||||
@ConditionalOnBean(EventDeliveryProducer.class)
|
||||
public class EventDeliveryObservationAutoConfiguration {
|
||||
@Bean
|
||||
EventDeliveryJournal eventDeliveryJournal(JdbcTemplate jdbc, PlatformTransactionManager manager) {
|
||||
return new EventDeliveryJournal(jdbc, manager);
|
||||
}
|
||||
@Bean
|
||||
EventDeliveryMetrics eventDeliveryMetrics(JdbcTemplate jdbc, MeterRegistry registry) {
|
||||
return new EventDeliveryMetrics(jdbc, registry);
|
||||
}
|
||||
}
|
||||
+1
@@ -3,3 +3,4 @@ com.baiyue.common.web.CommonWebAutoConfiguration
|
||||
com.baiyue.common.web.FeignTracePropagationAutoConfiguration
|
||||
com.baiyue.common.web.RestClientTracePropagationAutoConfiguration
|
||||
com.baiyue.common.web.BaiyueSchedulingAutoConfiguration
|
||||
com.baiyue.common.web.EventDeliveryObservationAutoConfiguration
|
||||
|
||||
+25
@@ -11,6 +11,31 @@ import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class EventDeliveryFeignConfigurationTest {
|
||||
interface Endpoint { @RequestLine("POST /events") String send(); }
|
||||
interface TypedEndpoint { @RequestLine("POST /events") com.baiyue.common.core.api.ApiResponse<Map<String, Object>> send(); }
|
||||
|
||||
@Test void wireCodeMustBeAStringAndAmbiguousJsonCannotBeAcknowledged() {
|
||||
for (String body : List.of("{\"code\":200}", "{\"code\":true}", "{\"code\":\"200\",\"code\":\"500\"}", "{\"code\":\"200\"} {}", "{}")) {
|
||||
var decoded = new AtomicInteger();
|
||||
TypedEndpoint endpoint = Feign.builder()
|
||||
.decoder((response, type) -> { decoded.incrementAndGet(); return com.baiyue.common.core.api.ApiResponse.success(Map.of()); })
|
||||
.addCapability(new EventDeliveryFeignConfiguration().eventDeliveryCapability(3000, 15000, 60))
|
||||
.client((request, options) -> Response.builder().request(request).status(200).reason("OK").headers(Map.of()).body(body, StandardCharsets.UTF_8).build())
|
||||
.target(TypedEndpoint.class, "http://event-consumer.invalid");
|
||||
assertThatThrownBy(endpoint::send).isInstanceOf(feign.codec.DecodeException.class);
|
||||
assertThat(decoded.get()).isZero();
|
||||
}
|
||||
}
|
||||
|
||||
@Test void validStringCodePreservesBodyForTheConfiguredDecoder() {
|
||||
String body = "{\"code\":\"200\",\"data\":{\"outcome\":\"APPLIED\"}}";
|
||||
TypedEndpoint endpoint = Feign.builder().decoder((response, type) -> {
|
||||
assertThat(new String(response.body().asInputStream().readAllBytes(), StandardCharsets.UTF_8)).isEqualTo(body);
|
||||
return com.baiyue.common.core.api.ApiResponse.success(Map.of("outcome", "APPLIED"));
|
||||
}).addCapability(new EventDeliveryFeignConfiguration().eventDeliveryCapability(3000, 15000, 60))
|
||||
.client((request, options) -> Response.builder().request(request).status(200).reason("OK").headers(Map.of()).body(body, StandardCharsets.UTF_8).build())
|
||||
.target(TypedEndpoint.class, "http://event-consumer.invalid");
|
||||
assertThat(endpoint.send().data()).containsEntry("outcome", "APPLIED");
|
||||
}
|
||||
|
||||
@Test void builtClientOverridesLegacyTimeoutAndRedirectSettings() {
|
||||
var capability = new EventDeliveryFeignConfiguration().eventDeliveryCapability(3000, 15000, 60);
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import java.util.UUID;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class EventDeliveryMetricsTest {
|
||||
@Test void samplesBoundedTypesAndPreservesPriorValuesWhenDatabaseFailsWithoutQueryingDuringScrapes() {
|
||||
var jdbc = spy(new JdbcTemplate(new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "")));
|
||||
jdbc.execute("CREATE TABLE ll_event_outbox(event_type VARCHAR(200),status VARCHAR(32),created_at TIMESTAMP)");
|
||||
var now = LocalDateTime.now();
|
||||
jdbc.update("INSERT INTO ll_event_outbox VALUES('BusinessFeeSourceChanged','PENDING',?)", now.minusSeconds(60));
|
||||
jdbc.update("INSERT INTO ll_event_outbox VALUES('BusinessFeeSourceChanged','PROCESSING',?)", now.minusSeconds(30));
|
||||
jdbc.update("INSERT INTO ll_event_outbox VALUES('BusinessFeeSourceChanged','DEAD',?)", now.minusSeconds(120));
|
||||
jdbc.update("INSERT INTO ll_event_outbox VALUES('user-controlled-unknown-type','PENDING',?)", now.minusYears(1));
|
||||
var registry = new SimpleMeterRegistry();
|
||||
var metrics = new EventDeliveryMetrics(jdbc, registry);
|
||||
metrics.sample(EventDeliveryMetrics.Stream.FEE, now);
|
||||
assertThat(registry.get("baiyue.event.delivery.pending").gauge().value()).isEqualTo(2);
|
||||
assertThat(registry.get("baiyue.event.delivery.dead").gauge().value()).isEqualTo(1);
|
||||
assertThat(registry.get("baiyue.event.delivery.oldest.pending.seconds").gauge().value()).isBetween(59.99, 60.01);
|
||||
double sampledAt = registry.get("baiyue.event.delivery.sample.timestamp").gauge().value();
|
||||
jdbc.execute("DROP TABLE ll_event_outbox");
|
||||
metrics.sample(EventDeliveryMetrics.Stream.FEE, now.plusMinutes(1));
|
||||
clearInvocations(jdbc);
|
||||
assertThat(registry.get("baiyue.event.delivery.pending").gauge().value()).isEqualTo(2);
|
||||
assertThat(registry.get("baiyue.event.delivery.sample.timestamp").gauge().value()).isEqualTo(sampledAt);
|
||||
assertThat(registry.get("baiyue.event.delivery.sample.success").gauge().value()).isZero();
|
||||
registry.getMeters().forEach(meter -> {
|
||||
assertThat(meter.getId().getTag("event_type")).isEqualTo("BusinessFeeSourceChanged");
|
||||
meter.measure().forEach(measurement -> measurement.getValue());
|
||||
});
|
||||
verifyNoInteractions(jdbc);
|
||||
}
|
||||
@Test void supportsBothMysqlLocalDateTimeAndJdbcTimestampAndRejectsUnknownTypes() {
|
||||
var jdbc = mock(JdbcTemplate.class);
|
||||
var now = LocalDateTime.of(2026, 9, 29, 12, 0);
|
||||
var registry = new SimpleMeterRegistry();
|
||||
var metrics = new EventDeliveryMetrics(jdbc, registry);
|
||||
for (Object oldest : List.of(now.minusSeconds(45), Timestamp.valueOf(now.minusSeconds(45)))) {
|
||||
Map<String, Object> row = Map.of("pending_count", 1L, "dead_count", 0L, "oldest", oldest);
|
||||
when(jdbc.queryForMap(anyString(), eq("BusinessFeeSourceChanged"))).thenReturn(row);
|
||||
metrics.sample(EventDeliveryMetrics.Stream.FEE, now);
|
||||
assertThat(registry.get("baiyue.event.delivery.oldest.pending.seconds").gauge().value()).isEqualTo(45);
|
||||
assertThat(registry.get("baiyue.event.delivery.sample.success").gauge().value()).isEqualTo(1);
|
||||
}
|
||||
when(jdbc.queryForMap(anyString(), eq("BusinessFeeSourceChanged")))
|
||||
.thenReturn(Map.of("pending_count", 0L, "dead_count", 0L, "oldest", "not a JDBC time"));
|
||||
metrics.sample(EventDeliveryMetrics.Stream.FEE, now);
|
||||
assertThat(registry.get("baiyue.event.delivery.pending").gauge().value()).isEqualTo(1);
|
||||
assertThat(registry.get("baiyue.event.delivery.oldest.pending.seconds").gauge().value()).isEqualTo(45);
|
||||
assertThat(registry.get("baiyue.event.delivery.sample.success").gauge().value()).isZero();
|
||||
}
|
||||
@Test void initialFailureIsUnknownRatherThanFalseZero() {
|
||||
var jdbc = new JdbcTemplate(new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID(), "sa", ""));
|
||||
var registry = new SimpleMeterRegistry();
|
||||
new EventDeliveryMetrics(jdbc, registry).sample(EventDeliveryMetrics.Stream.FINANCE, LocalDateTime.now());
|
||||
assertThat(registry.get("baiyue.event.delivery.pending").gauge().value()).isNaN();
|
||||
assertThat(registry.get("baiyue.event.delivery.sample.timestamp").gauge().value()).isZero();
|
||||
assertThat(registry.get("baiyue.event.delivery.sample.success").gauge().value()).isZero();
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import com.baiyue.common.core.outbox.EventDeliveryJournal;
|
||||
import com.baiyue.common.core.outbox.EventDeliveryProducer;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class EventDeliveryObservationAutoConfigurationTest {
|
||||
@Test void configuresMandatoryAuditAndMetricsForServiceLocalJdbc() {
|
||||
var jdbc = new JdbcTemplate(new DriverManagerDataSource("jdbc:h2:mem:auto", "sa", ""));
|
||||
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(EventDeliveryObservationAutoConfiguration.class))
|
||||
.withBean(EventDeliveryProducer.class, () -> new EventDeliveryProducer() { })
|
||||
.withBean(JdbcTemplate.class, () -> jdbc)
|
||||
.withBean(PlatformTransactionManager.class, () -> new DataSourceTransactionManager(jdbc.getDataSource()))
|
||||
.withBean(MeterRegistry.class, SimpleMeterRegistry::new)
|
||||
.run(context -> {
|
||||
assertThat(context).hasSingleBean(EventDeliveryJournal.class);
|
||||
assertThat(context).hasSingleBean(EventDeliveryMetrics.class);
|
||||
});
|
||||
}
|
||||
@Test void otherJdbcServicesDoNotAcquireTransactionOrMetricsRequirements() {
|
||||
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(EventDeliveryObservationAutoConfiguration.class))
|
||||
.withBean(JdbcTemplate.class, () -> new JdbcTemplate(new DriverManagerDataSource("jdbc:h2:mem:other", "sa", "")))
|
||||
.run(context -> {
|
||||
assertThat(context).hasNotFailed();
|
||||
assertThat(context).doesNotHaveBean(EventDeliveryJournal.class);
|
||||
assertThat(context).doesNotHaveBean(EventDeliveryMetrics.class);
|
||||
});
|
||||
}
|
||||
@Test void producerWithoutMeterRegistryFailsInsteadOfSilentlyDisablingObservation() {
|
||||
var jdbc = new JdbcTemplate(new DriverManagerDataSource("jdbc:h2:mem:noRegistry", "sa", ""));
|
||||
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(EventDeliveryObservationAutoConfiguration.class))
|
||||
.withBean(EventDeliveryProducer.class, () -> new EventDeliveryProducer() { })
|
||||
.withBean(JdbcTemplate.class, () -> jdbc)
|
||||
.withBean(PlatformTransactionManager.class, () -> new DataSourceTransactionManager(jdbc.getDataSource()))
|
||||
.run(context -> assertThat(context).hasFailed());
|
||||
}
|
||||
@Test void missingTransactionManagerFailsRatherThanInstallingNonTransactionalFallback() {
|
||||
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(EventDeliveryObservationAutoConfiguration.class))
|
||||
.withBean(EventDeliveryProducer.class, () -> new EventDeliveryProducer() { })
|
||||
.withBean(JdbcTemplate.class, () -> new JdbcTemplate(new DriverManagerDataSource("jdbc:h2:mem:missing", "sa", "")))
|
||||
.withBean(MeterRegistry.class, SimpleMeterRegistry::new)
|
||||
.run(context -> assertThat(context).hasFailed());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class EventDeliveryObservationTest {
|
||||
@Test void auditIdentitySupportsTheFormalUuidAndUlidContractWithoutSavingMalformedPayload() {
|
||||
var mapper = new ObjectMapper();
|
||||
for (String id : new String[]{"00000000-0000-4000-8000-000000000019", "01ARZ3NDEKTSV4RRFFQ69G5FAV"}) {
|
||||
assertThat(EventDeliveryObservation.eventId(mapper, "{\"eventId\":\"" + id + "\",\"payload\":\"secret\"}")).isEqualTo(id);
|
||||
}
|
||||
assertThat(EventDeliveryObservation.eventId(mapper, "bad secret JSON")).isNull();
|
||||
assertThat(EventDeliveryObservation.eventId(mapper, "{\"eventId\":\"Bearer secret\"}")).isNull();
|
||||
}
|
||||
@Test void arbitraryExceptionsAndRemoteMessagesCannotLeakIntoAttemptAudit() {
|
||||
var failure = new IllegalStateException("payload={secret} Authorization=secret");
|
||||
assertThat(EventDeliveryObservation.errorCode(failure, false, true)).isEqualTo("DELIVERY_FAILURE");
|
||||
assertThat(EventDeliveryObservation.errorCode(failure, true, true)).isEqualTo("PERMANENT_FAILURE");
|
||||
assertThat(EventDeliveryObservation.errorCode(failure, true, false)).isEqualTo("INVALID_EVENT");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user