Files
WRNexusJS/packages/rpc/test/server.test.ts
ClintchizandClaude Opus 5 6aaf21aa06 feat(rpc): add the caller-side example and close remaining coverage gaps
Adds examples/auth-showcase/app/services/greeter-client.ts so the showcase
demonstrates both halves - the review noted the example was callee-only, so a
developer had no working reference for making a call.

Raises integration coverage to the planned 3 tests and adds the missing
rpc-endpoint cases. Also wires the prod build path for services.

304 tests pass across rpc/router/dev-server/cli; typecheck, lint, format and
check:public-api all clean.

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

248 lines
7.5 KiB
TypeScript

import { afterEach, describe, expect, test } from "bun:test";
import { v } from "@wrnexus/validation";
import { defineService, procedure } from "../src/contract.ts";
import { RPC_ERROR_CODES } from "../src/errors.ts";
import { exportSubjectContext } from "../src/identity.ts";
import { implement } from "../src/server.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;
});
function configure(appName = "web") {
process.env.WRNEXUS_RPC_SECRET = "test-rpc-secret-at-least-32-chars-long";
process.env.WRNEXUS_APP_NAME = appName;
}
const openContract = defineService({
name: "demo",
procedures: {
add: procedure
.input(v.object({ a: v.number() }))
.output<{ a: number }>()
.build(),
},
});
const guardedContract = defineService({
name: "billing",
procedures: {
createInvoice: procedure
.input(v.object({ amountCents: v.number() }))
.output<{ invoiceId: string }>()
.permission("invoice:create")
.build(),
},
});
describe("implement()", () => {
test("invokes with validated input", async () => {
const service = implement(
openContract,
{ add: async ({ a }) => ({ a: a + 1 }) },
{ selfApp: "demo" },
);
const result = await service.invoke("add", { a: 1 });
expect(result).toEqual({ ok: true, value: { a: 2 } });
});
test("coerces through the schema", async () => {
let received: unknown;
const coercing = defineService({
name: "demo2",
procedures: {
add: procedure
.input(v.object({ a: v.number() }))
.output<{ a: number }>()
.build(),
},
});
const service = implement(
coercing,
{
add: async (input) => {
received = input;
return { a: (input as { a: number }).a };
},
},
{ selfApp: "demo2" },
);
await service.invoke("add", { a: "3" });
expect(received).toEqual({ a: 3 });
});
test("rejects schema-invalid input without invoking the handler", async () => {
let called = false;
const service = implement(
openContract,
{
add: async ({ a }) => {
called = true;
return { a };
},
},
{ selfApp: "demo" },
);
const result = await service.invoke("add", { a: "not-a-number" });
expect(called).toBe(false);
expect(result).toMatchObject({ ok: false, code: RPC_ERROR_CODES.invalid });
});
test("unknown procedure refused", async () => {
const service = implement(openContract, { add: async ({ a }) => ({ a }) }, { selfApp: "demo" });
const result = await service.invoke("subtract", {});
expect(result).toMatchObject({ ok: false, code: RPC_ERROR_CODES.unknown });
});
test("handler throw becomes opaque; a secret in the message does not survive", async () => {
const secret = "sk-super-secret-db-password";
const service = implement(
openContract,
{
add: async () => {
throw new Error(`db error using ${secret}`);
},
},
{ selfApp: "demo" },
);
const result = await service.invoke("add", { a: 1 });
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.code).toBe(RPC_ERROR_CODES.handler);
expect(result.message).not.toContain(secret);
}
});
test("a declared permission is enforced before the handler runs", async () => {
let called = false;
const service = implement(
guardedContract,
{
createInvoice: async ({ amountCents }) => {
called = true;
return { invoiceId: `inv_${amountCents}` };
},
},
{ selfApp: "billing", checkPermission: async () => false },
);
const result = await service.invoke("createInvoice", { amountCents: 5 });
expect(called).toBe(false);
expect(result).toMatchObject({ ok: false, code: RPC_ERROR_CODES.denied });
});
test("a permission check that throws denies", async () => {
let called = false;
const service = implement(
guardedContract,
{
createInvoice: async ({ amountCents }) => {
called = true;
return { invoiceId: `inv_${amountCents}` };
},
},
{
selfApp: "billing",
checkPermission: async () => {
throw new Error("permission store unavailable");
},
},
);
const result = await service.invoke("createInvoice", { amountCents: 5 });
expect(called).toBe(false);
expect(result).toMatchObject({ ok: false, code: RPC_ERROR_CODES.denied });
});
test("a declared permission with no checkPermission configured denies (fail closed)", async () => {
let called = false;
const service = implement(
guardedContract,
{
createInvoice: async ({ amountCents }) => {
called = true;
return { invoiceId: `inv_${amountCents}` };
},
},
{ selfApp: "billing" },
);
const result = await service.invoke("createInvoice", { amountCents: 5 });
expect(called).toBe(false);
expect(result).toMatchObject({ ok: false, code: RPC_ERROR_CODES.denied });
});
test("a valid identity token reaches the handler as a subject", async () => {
configure("web");
const token = await exportSubjectContext(
{ user: { id: "u1" }, locals: {} } as never,
"billing",
);
const service = implement(
guardedContract,
{
createInvoice: async ({ amountCents }, ctx) => ({
invoiceId: `${ctx.subject?.subjectId}_${amountCents}`,
}),
},
{ selfApp: "billing", checkPermission: async () => true },
);
const result = await service.invoke("createInvoice", { amountCents: 5 }, token);
expect(result).toEqual({ ok: true, value: { invoiceId: "u1_5" } });
});
test("a bad identity token is refused rather than downgraded to anonymous", async () => {
configure("web");
let called = false;
const service = implement(
openContract,
{
add: async ({ a }, ctx) => {
called = true;
expect(ctx.subject).toBeUndefined();
return { a };
},
},
{ selfApp: "demo" },
);
const result = await service.invoke("add", { a: 1 }, "garbage-token");
expect(called).toBe(false);
expect(result).toMatchObject({ ok: false, code: RPC_ERROR_CODES.identity });
});
describe("C1: prototype-chain procedure names cannot bypass the permission gate", () => {
const PROTO_NAMES = [
"constructor",
"toString",
"valueOf",
"hasOwnProperty",
"__proto__",
"isPrototypeOf",
];
for (const name of PROTO_NAMES) {
test(`"${name}" resolves as unknown, not as a handler`, async () => {
let permissionChecked = false;
const service = implement(
guardedContract,
{
createInvoice: async ({ amountCents }) => ({ invoiceId: `inv_${amountCents}` }),
},
{
selfApp: "billing",
checkPermission: async () => {
permissionChecked = true;
return false;
},
},
);
const result = await service.invoke(name, { amountCents: 1 });
expect(result).toMatchObject({ ok: false, code: RPC_ERROR_CODES.unknown });
expect(permissionChecked).toBe(false);
});
}
});
});