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 { 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, }, (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 private nested RPC routes // that arrive 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; } }); });