Files
WRNexusJS/packages/rpc/test/integration.test.ts
T
ClintchizandClaude Opus 5 3eec9fd8c6 fix(rpc): close the four final-review blockers on inter-app RPC
- Resolve RPC call origins via a new WRNEXUS_INTERNAL_ORIGINS map (loopback
  origins the gateway hands each child before spawning it), falling back to
  the public appOrigin only when it is absent. Calls previously always went
  to the public gateway origin, which the gateway unconditionally 404s on
  the RPC prefix by design — every real cross-app call failed.
- Stop loadServices() from running ahead of routing and stop memoizing a
  rejected load: one bad file under app/services/ no longer permanently
  breaks every route in the app. A failed load logs loudly, is retried on
  the next RPC request, and the RPC path gets a structured RPC_UNKNOWN
  instead of an unhandled throw.
- Reject a service whose contract.name does not match the filename it is
  mounted under, naming both, instead of silently mounting under the
  filename while the typed client calls by contract name.
- Let ServiceError accept an explicit retryable and have the client pass the
  wire value through, instead of recomputing (and silently flipping) it from
  the error code alone.
- Document the gateway/X-Forwarded-* deployment requirement in the RPC
  README.

Each of the three code blockers has a new/extended test that was verified to
fail when its fix was reverted (rpc/test/integration.test.ts,
dev-server/test/rpc-services-loading.test.ts).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 20:36:31 +05:30

169 lines
6.1 KiB
TypeScript

import { afterEach, describe, expect, test } from "bun:test";
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 wire(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 httpWire(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,
});
}
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: wire(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: wire(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: httpWire(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",
transport: httpTransport(), // no resolveOrigin override — uses the real default
});
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;
}
});
});