Files
WRNexusJS/packages/rpc/test/client.test.ts
T
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

218 lines
7.4 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, ServiceError, failure, success } from "../src/errors.ts";
import type { RpcTarget } from "../src/transport.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 billing = defineService({
name: "billing",
procedures: {
createInvoice: procedure
.input(v.object({ amountCents: v.number() }))
.output<{ invoiceId: string }>()
.build(),
},
});
describe("serviceClient", () => {
test("returns the handler's value unwrapped", async () => {
const client = serviceClient(billing, {
transport: { call: async () => success({ invoiceId: "inv_1" }) },
});
expect(await client.createInvoice({ amountCents: 1 })).toEqual({ invoiceId: "inv_1" });
});
test("throws a ServiceError carrying code and retryable on failure", async () => {
const client = serviceClient(billing, {
transport: { call: async () => failure(RPC_ERROR_CODES.transport, "down") },
});
let caught: unknown;
try {
await client.createInvoice({ amountCents: 1 });
} catch (err) {
caught = err;
}
expect(caught).toBeInstanceOf(ServiceError);
expect(caught).toMatchObject({ code: RPC_ERROR_CODES.transport, retryable: true });
});
test("attaches an identity token when given a context and omits it for an anonymous context", async () => {
configure("web");
let seenIdentity: string | undefined = "unset";
const client = serviceClient(billing, {
as: { user: { id: "u1" }, locals: {} } as unknown as Context,
transport: {
call: async (_target, _payload, options) => {
seenIdentity = options.identity;
return success({ invoiceId: "inv_1" });
},
},
});
await client.createInvoice({ amountCents: 1 });
expect(seenIdentity).toBeTypeOf("string");
let seenAnon: unknown = "unset";
const anonClient = serviceClient(billing, {
transport: {
call: async (_target, _payload, options) => {
seenAnon = options.identity;
return success({ invoiceId: "inv_1" });
},
},
});
await anonClient.createInvoice({ amountCents: 1 });
expect(seenAnon).toBeUndefined();
});
test("an undeclared procedure is undefined rather than a function that throws", async () => {
let called = false;
const client = serviceClient(billing, {
transport: {
call: async () => {
called = true;
return success({});
},
},
});
const proxy = client as unknown as Record<string, unknown>;
expect(proxy.deleteEverything).toBeUndefined();
expect(called).toBe(false);
});
test("the proxy has no `then` escape — await/return does not trigger a call", async () => {
let called = false;
const client = serviceClient(billing, {
transport: {
call: async () => {
called = true;
return success({ invoiceId: "inv_1" });
},
},
});
const proxy = client as unknown as Record<string, unknown>;
expect(proxy.then).toBeUndefined();
expect(proxy.catch).toBeUndefined();
expect(proxy.finally).toBeUndefined();
// `await client` must resolve to the proxy object itself, not reject.
const awaited = await client;
expect(awaited).toBe(client);
expect(called).toBe(false);
});
test("the signal arrives at the transport", async () => {
let seenSignal: AbortSignal | undefined;
const client = serviceClient(billing, {
transport: {
call: async (_target, _payload, options) => {
seenSignal = options.signal;
return success({ invoiceId: "inv_1" });
},
},
});
await client.createInvoice({ amountCents: 1 });
expect(seenSignal).toBeInstanceOf(AbortSignal);
});
test("the call aborts at timeoutMs when the transport ignores the signal", async () => {
const client = serviceClient(billing, {
timeoutMs: 20,
transport: {
call: () => new Promise(() => {}), // never resolves; ignores the signal
},
});
let caught: unknown;
const start = Date.now();
try {
await client.createInvoice({ amountCents: 1 });
} catch (err) {
caught = err;
}
const elapsed = Date.now() - start;
expect(caught).toBeInstanceOf(ServiceError);
expect(caught).toMatchObject({ code: RPC_ERROR_CODES.transport });
expect(elapsed).toBeLessThan(500);
});
test("the timer is cleared on both success and failure", async () => {
const originalClearTimeout = globalThis.clearTimeout;
let clearCount = 0;
globalThis.clearTimeout = ((...args: Parameters<typeof clearTimeout>) => {
clearCount++;
return originalClearTimeout(...args);
}) as typeof clearTimeout;
try {
const okClient = serviceClient(billing, {
transport: { call: async () => success({ invoiceId: "inv_1" }) },
});
await okClient.createInvoice({ amountCents: 1 });
expect(clearCount).toBe(1);
const failClient = serviceClient(billing, {
transport: { call: async () => failure(RPC_ERROR_CODES.transport, "down") },
});
await expect(failClient.createInvoice({ amountCents: 1 })).rejects.toBeInstanceOf(
ServiceError,
);
expect(clearCount).toBe(2);
} finally {
globalThis.clearTimeout = originalClearTimeout;
}
});
test("the app defaults to the service name", async () => {
let seenTarget: RpcTarget | undefined;
const client = serviceClient(billing, {
transport: {
call: async (target) => {
seenTarget = target;
return success({ invoiceId: "inv_1" });
},
},
});
await client.createInvoice({ amountCents: 1 });
expect(seenTarget?.app).toBe("billing");
expect(seenTarget?.service).toBe("billing");
});
test("a wrn-level non-retryable failure (e.g. a 403) surfaces at the client as retryable: false", async () => {
// RPC_TRANSPORT recomputes to retryable: true from the code alone — that
// is correct for a 5xx, but the transport already determined a 403 is
// NOT retryable via the bounded status check. The client must pass that
// wrn value through rather than recompute it from the code.
const client = serviceClient(billing, {
transport: {
call: async () => ({
ok: false,
code: RPC_ERROR_CODES.transport,
message: "Service returned 403",
retryable: false,
}),
},
});
let caught: unknown;
try {
await client.createInvoice({ amountCents: 1 });
} catch (err) {
caught = err;
}
expect(caught).toBeInstanceOf(ServiceError);
expect(caught).toMatchObject({ code: RPC_ERROR_CODES.transport, retryable: false });
});
});