124 lines
3.8 KiB
TypeScript
124 lines
3.8 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { defineRoom, type Middleware } from "@wrnexus/core";
|
|
import { createProductionHandlers } from "../src/prod.ts";
|
|
|
|
const room = defineRoom({
|
|
security: { requireUser: true },
|
|
authorize: (info) => info.user === "signed-in-user",
|
|
});
|
|
const auth: Middleware = async (ctx, next) => {
|
|
if (ctx.req.headers.get("cookie") === "test-session=valid") ctx.user = { id: "signed-in-user" };
|
|
return next();
|
|
};
|
|
function handlers(middleware: Middleware[] = [auth]) {
|
|
return createProductionHandlers(
|
|
{
|
|
pages: [],
|
|
api: [],
|
|
realtime: [{ raw: "/realtime/updates", mod: { default: room } }],
|
|
middleware,
|
|
components: [],
|
|
layouts: [],
|
|
},
|
|
{},
|
|
);
|
|
}
|
|
function request(
|
|
query = "",
|
|
cookie = "test-session=valid",
|
|
origin = "https://account.example.test",
|
|
) {
|
|
return new Request("https://account.example.test/realtime/updates" + query, {
|
|
headers: { host: "account.example.test", origin, cookie, upgrade: "websocket" },
|
|
});
|
|
}
|
|
test("room upgrade receives middleware identity after post-processing completes", async () => {
|
|
let finished = false;
|
|
const post: Middleware = async (_, next) => {
|
|
const res = await next();
|
|
finished = true;
|
|
return new Response(res.body, { status: res.status, headers: res.headers });
|
|
};
|
|
let upgraded = false;
|
|
const response = await handlers([auth, post]).fetch(request(), {
|
|
upgrade: (_, options) => {
|
|
expect(finished).toBe(true);
|
|
expect((options.data as any).meta.user).toBe("signed-in-user");
|
|
expect(options.headers?.has("x-wrnexus-upgrade-permit")).toBe(false);
|
|
upgraded = true;
|
|
return true;
|
|
},
|
|
});
|
|
expect(response).toBeUndefined();
|
|
expect(upgraded).toBe(true);
|
|
});
|
|
test("query user cannot impersonate an authenticated room member", async () => {
|
|
const response = await handlers().fetch(request("?user=signed-in-user", ""), {
|
|
upgrade: () => {
|
|
throw Error("must not upgrade");
|
|
},
|
|
});
|
|
expect(response?.status).toBe(403);
|
|
});
|
|
test("middleware rejection and cross-site origins never upgrade", async () => {
|
|
const server = {
|
|
upgrade: () => {
|
|
throw Error("must not upgrade");
|
|
},
|
|
};
|
|
expect(
|
|
(await handlers([() => new Response("denied", { status: 401 })]).fetch(request(), server))
|
|
?.status,
|
|
).toBe(401);
|
|
expect(
|
|
(await handlers().fetch(request("", "test-session=valid", "https://evil.example"), server))
|
|
?.status,
|
|
).toBe(403);
|
|
});
|
|
test("post-processing rejection overrides room authorization", async () => {
|
|
const deny: Middleware = async (_, next) => {
|
|
await next();
|
|
return new Response("denied", { status: 403 });
|
|
};
|
|
expect(
|
|
(
|
|
await handlers([auth, deny]).fetch(request(), {
|
|
upgrade: () => {
|
|
throw Error("must not upgrade");
|
|
},
|
|
})
|
|
)?.status,
|
|
).toBe(403);
|
|
});
|
|
|
|
test("Bun accepts a real socket with the middleware-authenticated identity", async () => {
|
|
const app = handlers();
|
|
const server = Bun.serve({ port: 0, fetch: app.fetch, websocket: app.websocket });
|
|
try {
|
|
await new Promise<void>((resolve, reject) => {
|
|
const Socket = WebSocket as unknown as {
|
|
new (url: string, options?: import("bun").WebSocketOptions): WebSocket;
|
|
};
|
|
const socket = new Socket(`ws://localhost:${server.port}/realtime/updates`, {
|
|
headers: { cookie: "test-session=valid", origin: `http://localhost:${server.port}` },
|
|
});
|
|
const timer = setTimeout(() => {
|
|
socket.close();
|
|
reject(Error("upgrade timed out"));
|
|
}, 3000);
|
|
socket.onopen = () => {
|
|
clearTimeout(timer);
|
|
socket.close();
|
|
resolve();
|
|
};
|
|
socket.onerror = () => {
|
|
clearTimeout(timer);
|
|
reject(Error("upgrade failed"));
|
|
};
|
|
});
|
|
} finally {
|
|
server.stop(true);
|
|
await app.shutdown();
|
|
}
|
|
});
|