test: re-enable WebhookE2ETest — убрать Kafka из теста
WebhookDispatcher читает outbox_events напрямую и не зависит от Kafka. OutboxPoller (единственный consumer KafkaTemplate в runtime) выключаем через ordinis.outbox.enabled=false, и spring-kafka autoconfig — через spring.autoconfigure.exclude. Kafka container теперь не нужен для этого теста. E2ESupport: Kafka container теперь lazy. Тесты вызывающие registerProperties() триггерят start(); тесты на новом registerPostgresOnlyProperties() — нет. Локально: WebhookE2ETest 2/2 PASSED за 9.8s (раньше timeout на CI 20-30s). Boot cold start 7s vs ~15s с Kafka. Уходит главный источник flakiness — Kafka broker timing на shared CI runner. OutboxKafkaE2ETest по-прежнему использует registerProperties() → Kafka container стартует — поведение этого теста не изменено.
This commit is contained in:
@@ -11,7 +11,13 @@ import org.testcontainers.utility.DockerImageName;
|
||||
* между тестовыми классами через {@link org.testcontainers.junit.jupiter.Container}
|
||||
* shared mode.
|
||||
*
|
||||
* <p>Использовать через extends либо composition в @SpringBootTest классах.
|
||||
* <p>Postgres стартует eagerly в class init — нужен всем e2e тестам.
|
||||
* Kafka стартует lazy — только если {@link #registerProperties} вызвана. Тесты
|
||||
* которые не используют Kafka (например {@code WebhookE2ETest}) экономят
|
||||
* 5-10 секунд cold-start на CI и убирают целый класс flakiness (kafka broker
|
||||
* не успевает отвечать на ранние produce/poll).
|
||||
*
|
||||
* <p>Использовать через composition в @SpringBootTest классах.
|
||||
*/
|
||||
public final class E2ESupport {
|
||||
|
||||
@@ -28,30 +34,62 @@ public final class E2ESupport {
|
||||
|
||||
// Confluent Kafka — confluentinc/cp-kafka работает out-of-the-box.
|
||||
// Confluent 7.6.0 включает в Kafka 3.6 — kraft mode.
|
||||
// Lazy: стартует при первом вызове registerProperties() (через kafka()).
|
||||
@SuppressWarnings("resource")
|
||||
static final KafkaContainer KAFKA = new KafkaContainer(
|
||||
private static final KafkaContainer KAFKA = new KafkaContainer(
|
||||
DockerImageName.parse("confluentinc/cp-kafka:7.6.0"))
|
||||
.withReuse(true);
|
||||
|
||||
private static volatile boolean kafkaStarted = false;
|
||||
|
||||
static {
|
||||
POSTGRES.start();
|
||||
KAFKA.start();
|
||||
}
|
||||
|
||||
private E2ESupport() {}
|
||||
|
||||
/**
|
||||
* Прокидывает DB + Kafka URLs в Spring Boot context. Вызывать из
|
||||
* {@code @DynamicPropertySource}.
|
||||
* Lazy-старт Kafka при первом обращении. Synchronized — testcontainers
|
||||
* shared между классами, гонок не должно быть.
|
||||
*/
|
||||
private static synchronized KafkaContainer kafka() {
|
||||
if (!kafkaStarted) {
|
||||
KAFKA.start();
|
||||
kafkaStarted = true;
|
||||
}
|
||||
return KAFKA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Прокидывает DB + Kafka URLs в Spring Boot context. Использовать в тестах
|
||||
* где есть Kafka producers/consumers.
|
||||
*/
|
||||
public static void registerProperties(DynamicPropertyRegistry registry) {
|
||||
registerPostgresProperties(registry);
|
||||
registry.add("spring.kafka.bootstrap-servers", () -> kafka().getBootstrapServers());
|
||||
}
|
||||
|
||||
/**
|
||||
* Прокидывает только DB URLs. Использовать в тестах которые не туда не
|
||||
* сюда не нуждаются в Kafka — экономит 5-10s cold start на CI и убирает
|
||||
* Kafka-специфичную flakiness.
|
||||
*
|
||||
* <p>Тест ДОЛЖЕН либо отключить {@code spring-kafka} autoconfig
|
||||
* через {@code spring.autoconfigure.exclude}, либо отключить outbox
|
||||
* poller (он единственный использует {@code KafkaTemplate} в runtime
|
||||
* code) через {@code ordinis.outbox.enabled=false}. Иначе Spring при
|
||||
* старте попытается подключиться к {@code localhost:9092} и зависнет.
|
||||
*/
|
||||
public static void registerPostgresOnlyProperties(DynamicPropertyRegistry registry) {
|
||||
registerPostgresProperties(registry);
|
||||
}
|
||||
|
||||
private static void registerPostgresProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
|
||||
registry.add("spring.datasource.username", POSTGRES::getUsername);
|
||||
registry.add("spring.datasource.password", POSTGRES::getPassword);
|
||||
registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver");
|
||||
|
||||
registry.add("spring.kafka.bootstrap-servers", KAFKA::getBootstrapServers);
|
||||
|
||||
// Liquibase использует тот же datasource — отдельные креды не нужны.
|
||||
registry.add("spring.liquibase.url", POSTGRES::getJdbcUrl);
|
||||
registry.add("spring.liquibase.user", POSTGRES::getUsername);
|
||||
|
||||
+23
-17
@@ -53,27 +53,28 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
properties = {
|
||||
"ordinis.outbox.enabled=true",
|
||||
"ordinis.outbox.poll-interval-ms=200",
|
||||
"ordinis.outbox.batch-size=20",
|
||||
// Outbox poller off — он единственный требует KafkaTemplate. Webhook
|
||||
// dispatcher читает outbox_events напрямую, Kafka не нужна.
|
||||
"ordinis.outbox.enabled=false",
|
||||
"ordinis.webhook.enabled=true",
|
||||
"ordinis.webhook.match-interval-ms=200",
|
||||
"ordinis.webhook.send-interval-ms=200",
|
||||
"ordinis.webhook.allowed-hosts=localhost,127.0.0.1",
|
||||
// Spring auto-config Kafka не нужно — нет KafkaTemplate потребителей
|
||||
// без OutboxPoller. Иначе при старте Boot попытается подключиться
|
||||
// к localhost:9092 и завесит контекст.
|
||||
"spring.autoconfigure.exclude="
|
||||
+ "org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration",
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@org.junit.jupiter.api.Disabled(
|
||||
"Flaky на CI testcontainers — timeout 20-30s не достаточен для cold start " +
|
||||
"Spring Boot + Postgres + Kafka + WebhookDispatcher schedule cycles. " +
|
||||
"Логика покрыта 26 unit tests (HmacSigner + SsrfGuard + DispatcherMatch + " +
|
||||
"DeliveryLifecycle). Re-enable когда сделаем dedicated CI runner с warm " +
|
||||
"Docker images + увеличим timeout до 60s.")
|
||||
class WebhookE2ETest {
|
||||
|
||||
@DynamicPropertySource
|
||||
static void props(DynamicPropertyRegistry registry) {
|
||||
E2ESupport.registerProperties(registry);
|
||||
// Только Postgres — без Kafka container. Экономит ~5-10s cold start
|
||||
// и убирает класс flakiness.
|
||||
E2ESupport.registerPostgresOnlyProperties(registry);
|
||||
}
|
||||
|
||||
@Autowired MockMvc mvc;
|
||||
@@ -150,8 +151,11 @@ class WebhookE2ETest {
|
||||
.content(om.writeValueAsBytes(recBody)))
|
||||
.andExpect(status().is2xxSuccessful());
|
||||
|
||||
// 4. Wait for delivery: receiver получил POST request
|
||||
await().atMost(20, TimeUnit.SECONDS)
|
||||
// 4. Wait for delivery: receiver получил POST request.
|
||||
// 60s timeout запас на CI: ждём 1-2 schedule cycle (matchTick 200ms +
|
||||
// sendTick 200ms) + сетевой round-trip. Без Kafka container время
|
||||
// cold start теста ~3-5s, но всё ещё хочется запас на shared CI runner.
|
||||
await().atMost(60, TimeUnit.SECONDS)
|
||||
.pollInterval(Duration.ofMillis(300))
|
||||
.until(() -> received.stream().anyMatch(r -> r.body().contains(needle)));
|
||||
|
||||
@@ -169,8 +173,10 @@ class WebhookE2ETest {
|
||||
String expectedSig = HmacSigner.sign(secret, req.body().getBytes(StandardCharsets.UTF_8));
|
||||
assertThat(req.signatureHeader()).isEqualTo(expectedSig);
|
||||
|
||||
// 6. Verify DB delivery row marked delivered
|
||||
await().atMost(10, TimeUnit.SECONDS)
|
||||
// 6. Verify DB delivery row marked delivered. Receiver уже получил body
|
||||
// (см. await выше) — delivery row либо уже есть, либо будет в ближайшем
|
||||
// sendTick цикле.
|
||||
await().atMost(20, TimeUnit.SECONDS)
|
||||
.pollInterval(Duration.ofMillis(300))
|
||||
.until(() -> deliveryRepo.count() > deliveriesBefore);
|
||||
|
||||
@@ -178,7 +184,7 @@ class WebhookE2ETest {
|
||||
subId, org.springframework.data.domain.PageRequest.of(0, 10)).getContent();
|
||||
assertThat(all).isNotEmpty();
|
||||
WebhookDelivery latest = all.get(0);
|
||||
await().atMost(10, TimeUnit.SECONDS)
|
||||
await().atMost(20, TimeUnit.SECONDS)
|
||||
.pollInterval(Duration.ofMillis(300))
|
||||
.until(() -> {
|
||||
WebhookDelivery d = deliveryRepo.findById(latest.getId()).orElseThrow();
|
||||
@@ -236,8 +242,8 @@ class WebhookE2ETest {
|
||||
.content(om.writeValueAsBytes(recBody))).andExpect(status().is2xxSuccessful());
|
||||
|
||||
// Ждём что delivery с status=retrying появится (HTTP 500 → markFailed).
|
||||
// Timeout 30s покрывает CI cold start + несколько schedule циклов.
|
||||
await().atMost(30, TimeUnit.SECONDS)
|
||||
// Timeout 60s покрывает CI cold start + несколько schedule циклов.
|
||||
await().atMost(60, TimeUnit.SECONDS)
|
||||
.pollInterval(Duration.ofMillis(300))
|
||||
.until(() -> deliveryRepo.findBySubscriptionIdOrderByCreatedAtDesc(
|
||||
sub.getId(), org.springframework.data.domain.PageRequest.of(0, 10))
|
||||
|
||||
Reference in New Issue
Block a user