Initial Commit
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
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<BrokerEnvelope<{ id: number }>> = [];
|
||||
await brokerB.subscribe("orders", (env) => {
|
||||
received.push(env);
|
||||
});
|
||||
// Tiny delay so the SUBSCRIBE is established before the publish.
|
||||
await new Promise<void>((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<void>((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<void>((r) => setTimeout(r, 50));
|
||||
const key = bufferKey("it.ws2:", tenant, "feed");
|
||||
const card = await raw.zcard(key);
|
||||
expect(card).toBe(4);
|
||||
// Member format is "<id>|<base64>" — 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<BrokerEnvelope<{ big: string }>> = [];
|
||||
await broker.subscribe("compressed", (e) => {
|
||||
received.push(e);
|
||||
});
|
||||
await new Promise<void>((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<void>((r) => setTimeout(r, 100));
|
||||
}
|
||||
expect(received.length).toBe(1);
|
||||
expect(received[0]!.payload.big).toBe(big);
|
||||
} finally {
|
||||
await broker.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user