import type { TenantId } from "@suaveplan/api-types/tenant"; import type { BrokerEnvelope } from "@suaveplan/api-ws/broker"; import { afterAll, beforeAll, describe, expect, it, } from "@suaveplan/testing/runner"; import { RedisFixture } from "@suaveplan/testing-integration-redis"; import { createRedisWsBroker } from "../../src/broker/broker.js"; import { bufferKey, decodeBufferMember, } from "../../src/event-buffer/event-buffer.js"; import { createRespClient } from "../../src/resp-client/resp-client.js"; const tenant = "tenant-it" as unknown as TenantId; let fixture: RedisFixture | undefined; let url: string | undefined; let dockerAvailable = false; beforeAll(async () => { fixture = new RedisFixture(); try { const config = await fixture.start(); url = config.url; dockerAvailable = true; } catch { // Docker not available — every test below skips itself via dockerAvailable. dockerAvailable = false; } }, 120_000); afterAll(async () => { if (fixture && dockerAvailable) { await fixture.stop(); } }); function skipIfNoDocker(): boolean { if (!dockerAvailable) { return true; } return false; } describe("Redis broker — real-container integration", () => { it("publish from broker A is delivered to subscriber on broker B", { timeout: 30_000, }, async () => { if (skipIfNoDocker()) return; const brokerA = await createRedisWsBroker<{ id: number }>({ connection: url!, hostId: "host-A", channelPrefix: "it.broadcast.", keyPrefix: "it.ws:", }); const brokerB = await createRedisWsBroker<{ id: number }>({ connection: url!, hostId: "host-B", channelPrefix: "it.broadcast.", keyPrefix: "it.ws:", }); try { const received: Array> = []; await brokerB.subscribe("orders", (env) => { received.push(env); }); // Tiny delay so the SUBSCRIBE is established before the publish. await new Promise((r) => setTimeout(r, 100)); await brokerA.publish("orders", { topic: "orders", tenant, payload: { id: 1 }, }); // Wait for fan-out for (let i = 0; i < 50 && received.length === 0; i += 1) { await new Promise((r) => setTimeout(r, 100)); } expect(received.length).toBe(1); expect(received[0]!.payload.id).toBe(1); expect(received[0]!.originHost).toBe("host-A"); } finally { await brokerA.dispose(); await brokerB.dispose(); } }); it("event buffer sorted-set: write, cap, and replay round-trip on a real Redis", { timeout: 30_000, }, async () => { if (skipIfNoDocker()) return; const broker = await createRedisWsBroker({ connection: url!, channelPrefix: "it.broadcast2.", keyPrefix: "it.ws2:", eventBufferMaxEvents: 4, eventBufferTtlSec: 60, }); try { for (let i = 1; i <= 6; i += 1) { await broker.writeBuffer(tenant, "feed", { eventId: i, payload: new Uint8Array([i, i + 1]), }); } const events = await broker.readBuffer(tenant, "feed", 0); // Cap = 4, so eventIds 3..6 survive. expect(events.map((e) => e.eventId)).toEqual([3, 4, 5, 6]); const tail = await broker.readBuffer(tenant, "feed", 4); expect(tail.map((e) => e.eventId)).toEqual([5, 6]); // Independently confirm via raw client that the TTL is set. const raw = createRespClient({ url: url! }); await new Promise((r) => setTimeout(r, 50)); const key = bufferKey("it.ws2:", tenant, "feed"); const card = await raw.zcard(key); expect(card).toBe(4); // Member format is "|" — decodeBufferMember keeps payload intact. const range = await raw.zrangebyscore(key, "-inf", "+inf"); expect(range.length).toBe(4); const decoded = decodeBufferMember(range[0]!); expect(decoded.eventId).toBe(3); await raw.quit(); } finally { await broker.dispose(); } }); it("healthz reports up against a live container and surfaces resolved options", { timeout: 30_000, }, async () => { if (skipIfNoDocker()) return; const broker = await createRedisWsBroker({ connection: url!, channelPrefix: "it.broadcast3.", keyPrefix: "it.ws3:", }); try { expect(broker.healthz().status).toBe("up"); expect(broker.options.keyPrefix).toBe("it.ws3:"); expect(broker.options.channelPrefix).toBe("it.broadcast3."); } finally { await broker.dispose(); } }); it("compressed payload round-trips end-to-end through real Redis", { timeout: 30_000, }, async () => { if (skipIfNoDocker()) return; const broker = await createRedisWsBroker<{ big: string }>({ connection: url!, channelPrefix: "it.broadcast4.", keyPrefix: "it.ws4:", compressionThresholdBytes: 64, }); try { const received: Array> = []; await broker.subscribe("compressed", (e) => { received.push(e); }); await new Promise((r) => setTimeout(r, 100)); const big = "abcdef".repeat(2_000); await broker.publish("compressed", { topic: "compressed", tenant, payload: { big }, }); for (let i = 0; i < 50 && received.length === 0; i += 1) { await new Promise((r) => setTimeout(r, 100)); } expect(received.length).toBe(1); expect(received[0]!.payload.big).toBe(big); } finally { await broker.dispose(); } }); });