/** * A2 — unit tests for the ioredis → RedisPublisher adapter (boot/redis.ts). The adapter is the only * non-trivial logic in the server entrypoint's Redis wiring: it must forward publish(channel, message) * verbatim to the underlying client and surface the driver's subscriber-count result unchanged. The * live `createRedisClient` (a thin `new Redis(url)`) needs a real broker and is covered by the boot * smoke, not here. */ import { describe, test, expect } from 'vitest' import type { Redis } from 'ioredis' import { createRedisPublisher } from '../src/boot/redis.js' import { createRedisRevocationBus } from '../src/routing/bus.js' import { RELAY_REVOCATIONS_CHANNEL, type KillSignal } from 'relay-contracts' /** Minimal ioredis stand-in recording publish calls; cast to Redis at the seam (only `publish` is used). */ function fakeRedis(returnValue = 1): { calls: Array<[string, string]>; client: Redis } { const calls: Array<[string, string]> = [] const client = { publish: async (channel: string, message: string): Promise => { calls.push([channel, message]) return returnValue }, } as unknown as Redis return { calls, client } } describe('A2 createRedisPublisher (RedisPublisher adapter)', () => { test('forwards channel + message verbatim and returns the driver subscriber count', async () => { // Arrange const { calls, client } = fakeRedis(3) const publisher = createRedisPublisher(client) // Act const receivers = await publisher.publish('relay:revocations', 'payload') // Assert expect(receivers).toBe(3) expect(calls).toEqual([['relay:revocations', 'payload']]) }) test('drives createRedisRevocationBus onto the FROZEN relay:revocations channel with JSON payload', async () => { // Arrange const { calls, client } = fakeRedis() const bus = createRedisRevocationBus(createRedisPublisher(client)) const signal: KillSignal = { scope: { kind: 'host', hostId: '11111111-1111-4111-8111-111111111111' }, at: 1_700_000_000, reason: 'revoked', } // Act await bus.publish(signal) // Assert — bus publishes exactly one message on the shared channel, JSON-encoded. expect(calls).toHaveLength(1) const [channel, message] = calls[0]! expect(channel).toBe(RELAY_REVOCATIONS_CHANNEL) expect(JSON.parse(message)).toMatchObject({ scope: { kind: 'host', hostId: signal.scope.kind === 'host' ? signal.scope.hostId : '' } }) }) })