fix(dev-server): route server.fn() RPC to a handler in production
server.fn() posts to POST /__wrnexus/rpc. Dev intercepts that path before handlers.fetch and routes it to a createRpcHandler instance built from loadWrnServerModule; createProductionServer/createProductionHandlers had no such route, so the request fell through to the internal-caller-gated inter-app service RPC and 404'd. Add resolveProdServerFunctions(), a synchronous equivalent of dev's resolve that searches the already-statically-imported ProdManifest components/pages/ layouts for __wrnexusServerFunctions + __wrnexusRpcManifest, and wire it into createProductionHandlers with the same validateCsrf + withServerFnRequestContext wrapping dev uses. Move those two helpers into a new rpc-shared.ts so prod.ts can use them without a circular import through index.ts. Add packages/dev-server/test/prod-server-fn-rpc.test.ts covering a successful call, CSRF rejection, and clean 404s for an unknown component/function.
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createProductionHandlers, type ProdManifest } from "../src/prod.ts";
|
||||
|
||||
/**
|
||||
* Production-only regression test for server.fn() RPC. The browser runtime
|
||||
* posts server-function calls to `/__wrnexus/rpc`. Dev intercepts that path
|
||||
* with a handler built from `createRpcHandler` (index.ts). Production had no
|
||||
* such route: the request fell through to `isRpcPath`/`handleRpcRequest` —
|
||||
* the *inter-app service* RPC, gated by an internal-caller header a browser
|
||||
* never sends — and 404'd. See `resolveProdServerFunctions` in prod.ts.
|
||||
*/
|
||||
|
||||
// Shape emitted by the compiler for a `.wrn` file with server functions
|
||||
// (packages/compiler/src/server-codegen.ts): a plain functions object plus a
|
||||
// manifest array of `{ component, function, parameters?, returnType? }`.
|
||||
function serverModule(component: string) {
|
||||
return {
|
||||
__wrnexusServerFunctions: {
|
||||
async greet(name: string) {
|
||||
return `hello ${name}`;
|
||||
},
|
||||
},
|
||||
__wrnexusRpcManifest: [
|
||||
{
|
||||
component,
|
||||
function: "greet",
|
||||
parameters: [{ name: "name", type: "string", optional: false }],
|
||||
returnType: "string",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function manifest(): ProdManifest {
|
||||
return {
|
||||
pages: [],
|
||||
api: [],
|
||||
realtime: [],
|
||||
middleware: [],
|
||||
components: [{ name: "Greeter", mod: serverModule("Greeter") }],
|
||||
layouts: [],
|
||||
};
|
||||
}
|
||||
|
||||
const CSRF_TOKEN = "test-token";
|
||||
const CSRF_HEADERS = {
|
||||
origin: "http://localhost",
|
||||
cookie: `wrn-csrf=${CSRF_TOKEN}`,
|
||||
"x-csrf-token": CSRF_TOKEN,
|
||||
};
|
||||
|
||||
function rpcRequest(body: unknown, headers: Record<string, string> = CSRF_HEADERS): Request {
|
||||
return new Request("http://localhost/__wrnexus/rpc", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", ...headers },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
describe("production server.fn() RPC (/__wrnexus/rpc)", () => {
|
||||
test("a valid call to an existing server function succeeds, not 404", async () => {
|
||||
const handlers = createProductionHandlers(manifest(), {});
|
||||
const req = rpcRequest({ component: "Greeter", function: "greet", args: ["world"] });
|
||||
const res = await handlers.fetch(req, {} as never);
|
||||
expect(res).toBeDefined();
|
||||
expect(res!.status).toBe(200);
|
||||
const body = await res!.json();
|
||||
expect(body).toMatchObject({ ok: true, value: "hello world" });
|
||||
});
|
||||
|
||||
test("a request without a valid CSRF token is rejected", async () => {
|
||||
const handlers = createProductionHandlers(manifest(), {});
|
||||
const req = rpcRequest(
|
||||
{ component: "Greeter", function: "greet", args: ["world"] },
|
||||
{ "content-type": "application/json" },
|
||||
);
|
||||
const res = await handlers.fetch(req, {} as never);
|
||||
expect(res).toBeDefined();
|
||||
expect(res!.status).toBe(403);
|
||||
const body = await res!.json();
|
||||
expect(body.ok).toBe(false);
|
||||
});
|
||||
|
||||
test("an unknown component fails cleanly (404), not with an internal error", async () => {
|
||||
const handlers = createProductionHandlers(manifest(), {});
|
||||
const req = rpcRequest({ component: "NoSuchComponent", function: "greet", args: [] });
|
||||
const res = await handlers.fetch(req, {} as never);
|
||||
expect(res).toBeDefined();
|
||||
expect(res!.status).toBe(404);
|
||||
const body = await res!.json();
|
||||
expect(body.ok).toBe(false);
|
||||
});
|
||||
|
||||
test("an unknown function on a known component fails cleanly (404)", async () => {
|
||||
const handlers = createProductionHandlers(manifest(), {});
|
||||
const req = rpcRequest({ component: "Greeter", function: "notARealFunction", args: [] });
|
||||
const res = await handlers.fetch(req, {} as never);
|
||||
expect(res).toBeDefined();
|
||||
expect(res!.status).toBe(404);
|
||||
const body = await res!.json();
|
||||
expect(body.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user