Pipeline #5667 failed: receiverReturning500_deliveryRetried_notDelivered
condition timeout 20s. Root cause: assertion inside HttpServer handler
(`assertThat(body.length).isGreaterThan(0)`) бросала AssertionError
ВНУТРИ handler потока — exchange.sendResponseHeaders никогда не звался,
client получал connection reset вместо 500. Test ждал
lastStatusCode==500, получал null → timeout.
Fix:
- Убрана assertion из handler (test её не нужно — focus на retry flow)
- Wrapped body-drain в try-catch чтобы handler не падал
- Bumped timeout 20s → 30s (CI cold start + schedule cycles)
Закрывает Phase 5 deferred item. Завершает webhooks v2 покрытие.
WebhookE2ETest (2 tests):
Test 1: success path
- Стартует JDK HttpServer на random localhost port (no extra deps)
- Создаёт WebhookSubscription указывающий на /hook
- POST record в dictionary → outbox event
- Ждёт что receiver получит POST в течение 20s
- Verify body содержит business_key
- Verify HMAC signature header валиден (HmacSigner.sign(secret, body)
matches X-Ordinis-Signature)
- Verify Ordinis headers (X-Ordinis-Event-Type, X-Ordinis-Scope=PUBLIC)
- Verify DB delivery row.status = delivered + lastStatusCode = 200
Test 2: retry path
- Receiver всегда 500
- POST record → outbox → dispatcher tries deliver
- Verify delivery transitions to status=retrying + lastStatusCode=500
- Не ждём DLQ (1000 attempts × 1m backoff = недели)
SSRF guard через ordinis.webhook.allowed-hosts=localhost,127.0.0.1
test override (production deny private CIDR остаётся включённым).
Test config:
- ordinis.outbox.enabled=true (для outbox poller)
- ordinis.webhook.enabled=true (для dispatcher)
- match-interval-ms=200, send-interval-ms=200 (faster tests)
Webhooks v2 — все 5 phases закрыты.
ScopeContext.canAccess(DataScope) — единая точка проверки доступа,
использует DataScope.visibleTo (PUBLIC видит PUBLIC, INTERNAL видит
PUBLIC+INTERNAL, RESTRICTED видит всё).
DictionaryRecordController:
- requireReadAccess на GET /{businessKey} и /{businessKey}/history
→ 404 dictionary_not_found если scope словаря недоступен (намеренно
скрываем существование INTERNAL/RESTRICTED данных, защита от
enumeration)
- requireWriteAccess на POST/PUT/DELETE
→ 403 scope_insufficient если scope недостаточен для записи
DictionaryDefinitionController:
- list() фильтрует список по canAccess (PUBLIC-юзер не видит INTERNAL
словари в каталоге)
- get() → 404 если недоступен
- create()/update() → 403 если новый scope выше доступа юзера
(защита от scope escalation через PUT)
AuthE2ETest.publicUser_cannotSeeInternalRecords: TODO snять, expectation
обновлён на isNotFound() (раньше было isOk + комментарий о gap).
До этого fix: PUBLIC-юзер мог GET /api/v1/dictionaries/{n}/records/{k}
INTERNAL-словарь на writer-side. Read-api (отдельный модуль) фильтровал
корректно. Issue найден через AuthE2ETest в Phase 2c.
Все 6 AuthE2ETest и 24 unit-теста rest-api зелёные.
Покрытие выросло с 1 до 6 e2e тестов через Testcontainers (Postgres+Kafka).
BitemporalE2ETest (4 тесты):
- createThenUpdate_keepsBothVersionsInHistory: v1+v2 в /history, oldValidTo == newValidFrom
- closeRecord_setsValidTo_andGetReturns404: после DELETE active=null, history живёт
- idempotencyKey_returnsCachedResponse_doesntDuplicate: повторный POST с тем
же Idempotency-Key → тот же id, count=1 в БД
- breakingSchemaChange_blockedOnUpdate: documents текущее поведение PUT
/api/v1/dictionaries/{name} с breaking schema (status < 500 = OK)
OutboxKafkaE2ETest (1 тест):
- POST record → outbox event в БД → poller публикует в Kafka → consumer
получает envelope с правильным dictionaryName/dataScope/payload.businessKey
- Pre-create topic через AdminClient чтобы избежать race с auto-create.
- ordinis.outbox.enabled=true + poll-interval=200ms через @SpringBootTest
properties (общий test profile его выключает для скорости остальных тестов).
SmokeE2ETest: UUID-suffixed businessKey чтобы при testcontainers reuse=true
и параллельных тестах не было коллизий.
Logging: outbox DEBUG для диагностики publish flow.
pom.xml: + awaitility для polling-based проверок.
Все 6 тестов зелёные: 1 smoke + 4 bitemporal + 1 outbox-kafka. Total ~16 sec
включая Spring context boot. Reuse=true ускоряет повторные прогоны.
Phase 1 e2e — proof framework работает:
- E2ESupport: Postgres 18 (postgis/postgis:18-3.6) + Kafka (cp-kafka 7.6.0)
через Testcontainers, reuse=true. db-init.sql создаёт postgis+btree_gist
расширения которые ожидает Liquibase 0001.
- application-test.yml: тушит cloud-config/vault/otel/outbox publishing,
включает Liquibase autoconfig. Auth disabled для упрощения.
- SmokeE2ETest: full e2e — POST dictionary → POST record → GET record →
проверка audit_log row. UUID-suffix в dictName для идемпотентности.
- master.xml: relativeToChangelogFile=true для всех include — иначе
Liquibase из ordinis-app classpath не находит changes/X.xml в JAR.
- pom.xml: testcontainers 1.21.3, surefire env DOCKER_HOST + DOCKER_API_VERSION
для macOS Docker Desktop.
Найден реальный prod-баг: audit_log.ip_address был INET, Hibernate JPA не
делает автоматический cast String→INET → INSERT падал. AuditLogger.write
обёрнут в try/catch, поэтому тихо съедал ошибку — на staging audit_log
оставался пустым после CRUD.
- 0012-audit-log-ipaddress-varchar.xml: ALTER COLUMN INET → VARCHAR(64)
- AuditLog entity: убрал columnDefinition="inet", length=64.