Skip to content

Add webhooks tests 1574 - #1700

Open
Stanley-Owoh wants to merge 3 commits into
crackedstudio:masterfrom
Stanley-Owoh:add-webhooks-tests-1574
Open

Stanley-Owoh wants to merge 3 commits into
crackedstudio:masterfrom
Stanley-Owoh:add-webhooks-tests-1574

Conversation

@Stanley-Owoh

Copy link
Copy Markdown

fix(indexer): persist DrawTriggered raffle state as DRAWING

Summary

DrawTriggered was decoded, classified, and routed through the ingestion pipeline — but
CursorAdvance logged it and returned null, so nothing was ever written to the database.
A raffle that had entered the draw process stayed OPEN in the indexer indefinitely, which
made raffles that were mid-draw indistinguishable from raffles still selling tickets (API
responses, active-raffle listings, and the UI all keyed off status).

This PR wires the event through to a real state transition, so the persisted lifecycle now
matches the contract:

RaffleCreated  -> OPEN
DrawTriggered  -> DRAWING
RaffleFinalized-> FINALIZED
RaffleCancelled-> CANCELLED

RaffleStatus.DRAWING already existed in @tikka/types and in the raffle_status database
enum, so this required no schema, enum, or generated-file changes — only the missing write path.

What changed

indexer/src/processors/raffle.processor.ts

New handleDrawTriggered(raffleId, ledger, txHash, schemaVersion), mirroring the existing
raffle handlers:

  • opens its own QueryRunner and transaction, and returns the runner — CursorAdvance
    owns commit/release, consistent with handleRaffleCreated / handleRaffleFinalized
  • UPDATE raffles SET status = DRAWING WHERE id = :raffleId AND status = :open
  • appends a DrawTriggered row to raffle_events with orIgnore() (unique tx_hash)
  • invalidates the raffle-detail and active-raffles caches
  • on failure: rollback, release, log, rethrow

indexer/src/ingestor/cursor-advance.ts

DrawTriggered now routes to handleDrawTriggered instead of logging and returning null,
so the event is committed through the normal cursor-advance path.

indexer/src/ingestor/duplicate-detector.ts

  • eventNeedsDatabase("DrawTriggered") → true (it mutates durable state, so the dispatcher
    reports succeeded rather than skipped)
  • handler name → RaffleProcessor.handleDrawTriggered, so DLQ entries and tracing spans are
    attributed to a real processor instead of DrawTriggeredHandler
  • RandomnessRequested / RandomnessReceived intentionally remain non-persisted

indexer/src/ingestor/ingestion-dispatcher.service.ts

The same three changes mirrored into the service's legacy private copies of
eventNeedsDatabase / applyEvent / getHandlerName. These are currently unreachable
(dispatch goes through CursorAdvance + DuplicateDetector), but leaving them stale would
mislead the next reader.

Idempotency and terminal-state safety

The transition is guarded on status = :open rather than status != :drawing, which gives three
properties at once:

Scenario Result
Duplicate delivery (same tx_hash) audit row ignored, update is a no-op
Different DrawTriggered tx for an already-DRAWING raffle no-op
Late DrawTriggered on a FINALIZED / CANCELLED raffle no-op — terminal state is never reversed

Tests

  • raffle.processor.spec.ts — 5 new cases: status set to DRAWING, the status = :open guard
    clause, cache invalidation, runner returned without committing, and rollback/error propagation
  • cursor-advance.spec.ts — routing asserts the exact processor arguments plus commit/release
  • duplicate-detector.spec.ts — DrawTriggered asserts needsDatabase: true and the new handler
    name; randomness cases still assert false
  • handlers/duplicate-delivery.spec.ts — replaced the previous "leaves empty durable state"
    expectation with two real behavioural tests: duplicate delivery leaves an identical DRAWING
    raffle with exactly one audit row, and a parameterized case proving FINALIZED and CANCELLED
    raffles are not reopened. The in-memory QueryRunner harness was extended to interpret the
    status = :open guard (and return affected: 0 when blocked).

Verification

# focused
pnpm exec jest src/processors/raffle.processor.spec.ts \
                 src/ingestor/cursor-advance.spec.ts \
                 src/ingestor/duplicate-detector.spec.ts \
                 src/ingestor/handlers/duplicate-delivery.spec.ts
# 4 suites, 47 tests passed

# full indexer unit suite
pnpm exec jest --testPathIgnorePatterns=integration
# 51 suites, 603 passed / 1 skipped

pnpm run typecheck   # clean
pnpm run lint        # 0 errors (63 pre-existing warnings)

Because a green suite proves little on its own, each new behaviour was mutation-tested to
confirm the tests actually fail when the fix is removed:

Mutation Tests that failed
drop AND status = :open guard 3 — guard-clause test + does not reopen a finalized raffle + does not reopen a cancelled raffle
revert routing to return null 2 — routing test + duplicate delivery leaves an identical DRAWING raffle
set needsDatabase back to false 1 — marks DrawTriggered as requiring durable database mutation

All mutations were reverted; the final diff is byte-identical to the pre-mutation state.

Notes / follow-ups (not in this PR)

  • handleDrawTriggered does not emit a webhookService.dispatch("DrawTriggered", ...) call.
    A drawing transition is arguably subscriber-relevant, but introducing a new externally visible
    webhook event felt out of scope for a persistence fix — happy to add it as a follow-up.
  • The indexer integration suite (pnpm test:integration) requires Docker/Testcontainers
    Postgres and could not run in this environment, so the WHERE clause is verified via the
    in-memory QueryRunner harness and mock assertions rather than against a real database.
    Worth confirming in CI.
  • IngestionDispatcherService's eventNeedsDatabase / applyEvent / getHandlerName are dead
    code. They were updated for consistency, but no test can cover them; deleting them would be the
    real fix.

feat(backend): add webhook tests

Also on this branch — hardening and test coverage for inbound webhook signature verification,
kept separate from the indexer fix above.

webhook-signature-verification.interceptor.ts

  • signs and compares over the raw request body rather than a re-serialized object, so
    signatures no longer break on key ordering or whitespace
  • timingSafeEqual with equal-length buffers instead of ===
  • rejects timestamps outside a 5-minute MAX_TIMESTAMP_AGE_MS window
  • validates the x-tikka-webhook-source header format and resolves per-source secrets
    (<SOURCE>_WEBHOOK_SECRET, defaulting to INDEXER_WEBHOOK_SECRET)
  • supports x-webhook-signature / x-tikka-signature header variants
  • fails closed with UnauthorizedException when the raw body or signature is missing

main.ts

Enables Nest/Fastify rawBody: true — without it the interceptor cannot verify signatures at all.

Tests

  • webhook-signature-verification.interceptor.spec.ts — 302 lines covering valid signatures,
    tampered bodies, bad timestamps, prefixed/alternate signature headers, missing raw body,
    invalid sources, and constant-time comparison
  • webhooks.controller.spec.ts — 188 lines of controller coverage
  • test/webhooks-signature.e2e-spec.ts — rewritten to POST real raw bodies end-to-end
  • webhook-delivery.worker.spec.ts — fixed BullMQ injection to use
    getQueueToken(WEBHOOK_DELIVERY_QUEUE) and added shutdown-path coverage
  • backend/package.json — Jest discovery/transform fixes and Zod 4 import compatibility;
    zod-validation.pipe.ts and supabase.provider.ts adjusted accordingly

Known baseline failures (pre-existing, unrelated to this work): a full backend
pnpm exec jest run still reports 24 failing suites, and
tsc --noEmit reports strict-property errors in files such as
src/api/rest/monitor/dto/replay-response.dto.ts. The focused webhook suites and the
signature e2e suite pass.

Closes #1574

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[backend] The webhooks module has no tests at all

1 participant