Files
WRNexusJS/packages/rpc/test/integration.test.ts
Clintchiz 2c960fc1dc
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
refactor: migrate legacy wire namespace to wrn
2026-08-12 18:51:15 +05:30

216 lines
8.2 KiB
TypeScript

import { afterEach, describe, expect, test } from "bun:test";
import * as http from "node:http";
import type { Context } from "@wrnexus/core";
import { v } from "@wrnexus/validation";
import { serviceClient } from "../src/client.ts";
import { defineService, procedure } from "../src/contract.ts";
import { RPC_ERROR_CODES } from "../src/errors.ts";
import { httpTransport } from "../src/http.ts";
import { implement } from "../src/server.ts";
import { inProcessTransport } from "../src/transport.ts";
import { handleRpcRequest } from "../../dev-server/src/rpc-dispatch.ts";
const originalRpcSecret = process.env.WRNEXUS_RPC_SECRET;
const originalAppName = process.env.WRNEXUS_APP_NAME;
afterEach(() => {
if (originalRpcSecret === undefined) delete process.env.WRNEXUS_RPC_SECRET;
else process.env.WRNEXUS_RPC_SECRET = originalRpcSecret;
if (originalAppName === undefined) delete process.env.WRNEXUS_APP_NAME;
else process.env.WRNEXUS_APP_NAME = originalAppName;
});
const billing = defineService({
name: "billing",
procedures: {
createInvoice: procedure
.input(v.object({ amountCents: v.number() }))
.output<{ invoiceId: string; forSubject: string }>()
.permission("invoice:create")
.build(),
},
});
function wrn(allowed: boolean) {
const service = implement(
billing,
{
createInvoice: async ({ amountCents }, ctx) => ({
invoiceId: `inv_${amountCents}`,
forSubject: ctx.subject?.subjectId ?? "anon",
}),
},
{ selfApp: "billing", checkPermission: async () => allowed },
);
return inProcessTransport({
"billing/createInvoice": (payload, identity) =>
service.invoke("createInvoice", payload, identity),
});
}
function createHttpTransport(allowed: boolean) {
const service = implement(
billing,
{
createInvoice: async ({ amountCents }, ctx) => ({
invoiceId: `inv_${amountCents}`,
forSubject: ctx.subject?.subjectId ?? "anon",
}),
},
{ selfApp: "billing", checkPermission: async () => allowed },
);
return httpTransport({
resolveOrigin: () => "http://billing.internal",
fetch: (async (input: RequestInfo | URL, init?: RequestInit) => {
const request = new Request(input, init);
return (await handleRpcRequest(
request,
new URL(request.url),
new Map([["billing", service]]),
))!;
}) as typeof fetch,
});
}
// packages/csr's actions.test.ts deletes globalThis.fetch (and restores its
// own captured copy) around each of its tests, and packages/csr's
// reactive.test.ts's "cache invalidation refetches matching client Async
// boundaries" test replaces globalThis.fetch with a mock and never restores
// it at all. bun test runs test files sequentially — importing a file and
// running its tests before moving to the next — rather than importing every
// file up front, so a module-level `const realFetch = fetch` captured here
// would already observe whatever packages/csr left behind by the time this
// file (which sorts after csr) is loaded. The variable under test below is
// `resolveOrigin`, not `fetch`, so instead of depending on the shared
// `globalThis.fetch` at all, this makes a real request over a real socket
// using node:http directly. That keeps the assertion about
// httpTransport()'s default origin resolution intact regardless of what any
// other suite does to the global `fetch` binding.
function realFetch(url: string, init: RequestInit): Promise<Response> {
return new Promise((resolve, reject) => {
const target = new URL(url);
const req = http.request(
{
hostname: target.hostname,
port: target.port,
path: `${target.pathname}${target.search}`,
method: init.method ?? "GET",
headers: init.headers as Record<string, string>,
},
(res) => {
const chunks: Buffer[] = [];
res.on("data", (chunk: Buffer) => chunks.push(chunk));
res.on("error", reject);
res.on("end", () => {
resolve(
new Response(Buffer.concat(chunks), {
status: res.statusCode ?? 500,
}),
);
});
},
);
req.on("error", reject);
if (typeof init.body === "string") req.write(init.body);
req.end();
});
}
describe("RPC integration", () => {
test("propagates identity and validates the callee permission", async () => {
process.env.WRNEXUS_RPC_SECRET = "test-rpc-secret-at-least-32-chars-long";
process.env.WRNEXUS_APP_NAME = "web";
const client = serviceClient(billing, {
app: "billing",
as: { user: { id: "u1" }, tenant: { id: "acme" }, locals: {} } as unknown as Context,
transport: wrn(true),
});
expect(await client.createInvoice({ amountCents: 250 })).toEqual({
invoiceId: "inv_250",
forSubject: "u1",
});
});
test("denies when the callee permission check refuses", async () => {
process.env.WRNEXUS_RPC_SECRET = "test-rpc-secret-at-least-32-chars-long";
process.env.WRNEXUS_APP_NAME = "web";
const client = serviceClient(billing, {
app: "billing",
as: { user: { id: "u1" }, locals: {} } as unknown as Context,
transport: wrn(false),
});
await expect(client.createInvoice({ amountCents: 1 })).rejects.toMatchObject({
code: RPC_ERROR_CODES.denied,
});
});
test("uses the real HTTP transport and private dispatcher end to end", async () => {
process.env.WRNEXUS_RPC_SECRET = "test-rpc-secret-at-least-32-chars-long";
process.env.WRNEXUS_APP_NAME = "web";
const client = serviceClient(billing, {
app: "billing",
as: { user: { id: "u1" }, locals: {} } as unknown as Context,
transport: createHttpTransport(true),
});
expect(await client.createInvoice({ amountCents: 7 })).toEqual({
invoiceId: "inv_7",
forSubject: "u1",
});
await expect(client.createInvoice({ amountCents: "bad" } as never)).rejects.toMatchObject({
code: RPC_ERROR_CODES.invalid,
});
});
test("the default httpTransport reaches the callee over a real socket via the internal origin", async () => {
const service = implement(
billing,
{
createInvoice: async ({ amountCents }, ctx) => ({
invoiceId: `inv_${amountCents}`,
forSubject: ctx.subject?.subjectId ?? "anon",
}),
},
{ selfApp: "billing", checkPermission: async () => true },
);
const server = Bun.serve({
port: 0,
hostname: "127.0.0.1",
async fetch(req) {
const url = new URL(req.url);
const res = await handleRpcRequest(req, url, new Map([["billing", service]]));
return res ?? new Response("Not found", { status: 404 });
},
});
const originalWorkspace = process.env.WRNEXUS_WORKSPACE_ORIGINS;
const originalInternal = process.env.WRNEXUS_INTERNAL_ORIGINS;
try {
// The workspace (public) origin deliberately points somewhere that
// cannot serve the RPC — the gateway 404s the RPC prefix on any
// request that arrives at a public origin. Only the internal-origin
// map points at the real server. If httpTransport() ever falls back
// to the public origin by default again, this call fails.
process.env.WRNEXUS_WORKSPACE_ORIGINS = JSON.stringify({
billing: "http://127.0.0.1:1", // unroutable — nothing listens here
});
process.env.WRNEXUS_INTERNAL_ORIGINS = JSON.stringify({
billing: `http://127.0.0.1:${server.port}`,
});
const client = serviceClient(billing, {
app: "billing",
// no resolveOrigin override — uses the real default; fetch is pinned
// to the node:http-backed implementation above (see comment there).
transport: httpTransport({ fetch: realFetch as unknown as typeof fetch }),
});
expect(await client.createInvoice({ amountCents: 42 })).toEqual({
invoiceId: "inv_42",
forSubject: "anon",
});
} finally {
server.stop(true);
if (originalWorkspace === undefined) delete process.env.WRNEXUS_WORKSPACE_ORIGINS;
else process.env.WRNEXUS_WORKSPACE_ORIGINS = originalWorkspace;
if (originalInternal === undefined) delete process.env.WRNEXUS_INTERNAL_ORIGINS;
else process.env.WRNEXUS_INTERNAL_ORIGINS = originalInternal;
}
});
});