Skip to content

feat(backend): gateway integration tests — multi-node, multi-device (#215)#288

Open
daveades wants to merge 5 commits into
codebestia:mainfrom
daveades:feat/issue-215-gateway-integration-tests
Open

feat(backend): gateway integration tests — multi-node, multi-device (#215)#288
daveades wants to merge 5 commits into
codebestia:mainfrom
daveades:feat/issue-215-gateway-integration-tests

Conversation

@daveades

Copy link
Copy Markdown

Summary

Closes #215

  • Cross-node delivery: spins up two socket.io servers sharing a @socket.io/redis-adapter instance; asserts a message sent on node-1 arrives at a socket connected to node-2
  • Multi-device fanout: connects two devices for the same user on different nodes and verifies both receive new_message when per-device envelopes are provided
  • Persist-before-deliver: injects artificial DB latency in the db.insert().returning() mock and asserts db_insert_done always precedes new_message_received in the event order array
  • Revocation disconnect: publishes device_revoked:{deviceId} directly to Redis and asserts the socket receives device_revoked and is disconnected, exercising the cross-instance pub/sub path in startDeviceRevocationListener
  • Resume/sync after drop: pre-populates the per-user Redis stream via recordEphemeralEvent, triggers resume with an empty cursor, asserts all events are replayed in order; then re-issues resume with the advanced cursor and asserts no duplicates

Implementation notes

  • All DB access is mocked (vi.mock) following the existing test patterns; only Redis is real (ioredis connected to the service container)
  • A vi.hoisted reference holder wires the real Redis instance into the lib/redis.js module mock so presence, rate-limit, and resume-stream services share the same connection
  • Rate limiting, heartbeat, and backpressure are no-op mocked to keep tests focused on delivery semantics
  • A lightweight createGatewayNode() factory assembles the same middleware stack as index.ts without the side-effecting boot sequence

CI changes

Added a redis:7-alpine service to .github/workflows/backend-ci.yml and exposed REDIS_URL=redis://localhost:6379 so the integration tests run in the existing backend job. Also bumped vitest.config.ts testTimeout to 15 s for network I/O headroom.

Test plan

  • pnpm test in apps/backend with a local Redis running passes all 5 new tests
  • Existing 146 unit tests continue to pass
  • CI backend workflow passes with the added Redis service

…-device scenarios (codebestia#215)

- Spin up two Socket.IO instances sharing a real Redis adapter to verify
  cross-node delivery: message sent on node-1 reaches a socket on node-2
- Assert multi-device fanout: all active devices of a recipient receive
  the new_message event when per-device envelopes are provided
- Verify persist-before-deliver ordering via an intentional DB latency
  probe — new_message is never emitted until the DB insert resolves
- Test revocation disconnect: publishing device_revoked:{id} to Redis
  triggers force-disconnect and client notification on any node
- Test resume/sync determinism: missed ephemeral events are replayed
  from the Redis stream on reconnect, idempotent with an advanced cursor

Add redis:7-alpine service to the backend-ci GitHub Actions workflow
so these tests run in CI. Bump vitest testTimeout to 15 s for network I/O.
@drips-wave

drips-wave Bot commented Jun 29, 2026

Copy link
Copy Markdown

@daveades Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

dave added 4 commits June 29, 2026 11:36
…in integration suite

- Add vi.mock('../lib/redis.js', () => ({ redis: null })) to readReceipts.test.ts
  so the if (redis) branch in message_read never runs during these unit tests.
  Adding a Redis service to CI (codebestia#215) made redis truthy, triggering a findMany
  call that was absent from the db mock and failing two tests.
- Add conversationMembers.findMany and the missing drizzle-orm exports (ne,
  isNull, inArray, sql) to readReceipts.test.ts for completeness.
- Replace pub/sub .disconnect() with await .quit().catch(() => {}) in the
  gateway integration test to prevent unhandled 'Connection is closed'
  rejections during suite teardown.
…s on teardown

ioredis rejects pending-command Promises in its event_handler.js close()
function when a connection is force-closed.  Those rejections are internal to
ioredis and cannot be caught via .catch() on the quit() call because they are
created from event callbacks, not from the quit() promise chain.

Register a targeted unhandledRejection handler before the suite starts that
silences only Error('Connection is closed.') messages and re-throws everything
else, then removes itself in afterAll once the Redis connection is fully torn
down.  This prevents Vitest from treating the ioredis teardown noise as test
errors while preserving visibility of any other genuine unhandled rejections.
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.

Gateway integration tests (multi-node, multi-device)

1 participant