test(web-next): e2e realtime-ping (4 checkpoints)
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.0",
|
||||
"socket.io-client": "^4.7.0",
|
||||
"@repo/core-eslint": "workspace:*",
|
||||
"@repo/core-testing": "workspace:*",
|
||||
"@repo/core-typescript": "workspace:*",
|
||||
|
||||
97
apps/web-next/src/__tests__/realtime-ping.test.ts
Normal file
97
apps/web-next/src/__tests__/realtime-ping.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// e2e proof-of-life: connect → subscribe → emit ping → receive pong via the
|
||||
// production-shaped binder + Socket.IO server, with cookie-session auth
|
||||
// against a seeded test session.
|
||||
import "reflect-metadata";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { createServer, type Server as HttpServer } from "node:http";
|
||||
import { Server as IOServer } from "socket.io";
|
||||
import { io as ioClient } from "socket.io-client";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import {
|
||||
RealtimeHandlerRegistry,
|
||||
SocketIORealtimeBroadcaster,
|
||||
SocketIORealtimeServer,
|
||||
realtimePingInboundDescriptor,
|
||||
realtimePongChannel,
|
||||
type IRealtimeAuthenticator,
|
||||
} from "@repo/core-realtime";
|
||||
|
||||
describe("e2e: realtime-ping exercises all four checkpoints", () => {
|
||||
let httpServer: HttpServer;
|
||||
let realtimeServer: SocketIORealtimeServer;
|
||||
let port: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
httpServer = createServer();
|
||||
const io = new IOServer(httpServer);
|
||||
const broadcaster = new SocketIORealtimeBroadcaster(io);
|
||||
const registry = new RealtimeHandlerRegistry();
|
||||
registry.register(realtimePingInboundDescriptor(broadcaster));
|
||||
registry.registerChannel(realtimePongChannel);
|
||||
|
||||
const authenticator: IRealtimeAuthenticator = {
|
||||
authenticate: async ({ cookies }) =>
|
||||
cookies.session === "valid-session"
|
||||
? { userId: "user_test", roles: [] }
|
||||
: null,
|
||||
};
|
||||
|
||||
realtimeServer = new SocketIORealtimeServer({
|
||||
httpServer,
|
||||
io,
|
||||
authenticator,
|
||||
registry,
|
||||
});
|
||||
await realtimeServer.start();
|
||||
|
||||
await new Promise<void>((r) => httpServer.listen(0, r));
|
||||
port = (httpServer.address() as AddressInfo).port;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await realtimeServer.stop();
|
||||
await new Promise<void>((r) => httpServer.close(() => r()));
|
||||
});
|
||||
|
||||
it("authenticated client gets pong after ping", async () => {
|
||||
const client = ioClient(`http://localhost:${port}`, {
|
||||
extraHeaders: { Cookie: "session=valid-session" },
|
||||
});
|
||||
await new Promise<void>((r) => client.on("connect", () => r()));
|
||||
|
||||
const subAck = await new Promise<{ ok: boolean }>((r) =>
|
||||
client.emit("subscribe", "realtime.pong", r),
|
||||
);
|
||||
expect(subAck.ok).toBe(true);
|
||||
|
||||
const pongs: { at: string; echo: string }[] = [];
|
||||
client.on("realtime.pong", (p) => pongs.push(p));
|
||||
|
||||
const sentAt = "2026-05-08T12:00:00.000Z";
|
||||
const pingAck = await new Promise<{ ok: boolean }>((r) =>
|
||||
client.emit("realtime.ping", { at: sentAt }, r),
|
||||
);
|
||||
expect(pingAck.ok).toBe(true);
|
||||
|
||||
// Pong arrives synchronously after handler runs.
|
||||
await new Promise<void>((r) => setImmediate(r));
|
||||
|
||||
expect(pongs).toHaveLength(1);
|
||||
expect(pongs[0]).toEqual({ at: sentAt, echo: "user_test" });
|
||||
|
||||
client.disconnect();
|
||||
});
|
||||
|
||||
it("anonymous client cannot subscribe to pong (authenticated scope)", async () => {
|
||||
const client = ioClient(`http://localhost:${port}`);
|
||||
await new Promise<void>((r) => client.on("connect", () => r()));
|
||||
|
||||
const subAck = await new Promise<{ ok: boolean; error?: string }>((r) =>
|
||||
client.emit("subscribe", "realtime.pong", r),
|
||||
);
|
||||
|
||||
expect(subAck.ok).toBe(false);
|
||||
expect(subAck.error).toBe("forbidden");
|
||||
client.disconnect();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user