TRPCError UNAUTHORIZED during integration testing of context-dependent procedures
0 reputation · 09 Oct 2022, 04:20 UTC
0 reputation · 09 Oct 2022, 04:20 UTC
When designing integration tests for tRPC v10+ procedures, the goal is to verify failure paths—such as unauthorized access—without utilizing production credentials or a live authentication provider.
The current implementation relies on a ctx object that is strictly typed. In a test harness, providing a mocked context that triggers a TRPCError with the UNAUTHORIZED code is the documented method for signaling these failures. However, there is uncertainty regarding how mergeRouters affects type inference for the ctx shape when routers are split across multiple modules.
If the appRouter loses the specific procedure context during the merge, the test harness may encounter TypeScript errors or runtime undefined values before the TRPCError can be thrown.
ctx type be explicitly aligned across merged routers to ensure consistent TRPCError triggering in test environments?appRouter maintain full type inference for the context when using modular router definitions in v10?33775 reputation · 09 Oct 2022, 06:01 UTC
Define a single shared Context interface and use it as the generic argument for every router’s createRouter call. When merging, pass the same interface to mergeRouters. This preserves the exact context shape, so your test harness can supply a mock ctx that triggers TRPCError.UNAUTHORIZED without TypeScript errors or runtime undefined.
createRouter<Context>() fixes that type.mergeRouters<Context>(routerA, routerB) retains the generic, preventing widening to a union.When routers are defined in separate files without explicitly specifying the context generic, each file may infer a slightly different context (e.g., from local middleware). During mergeRouters the inferred types are merged, producing a union where fields like user become optional. Your test harness then passes a ctx missing user, causing the procedure to fail with a TypeError before the unauthorized middleware can throw TRPCError.UNAUTHORIZED.
src/trpc/context.ts:
export interface Context {
user?: { id: string; role: string } | null;
// add any other fields your procedures need
}
import { initTRPC } from '@trpc/server';
import type { Context } from '../context';
const t = initTRPC.context<Context>().createRouter();
export const publicRouter = t.router({
// …
});
export const protectedRouter = t.router({
// middleware that checks ctx.user and throws TRPCError.UNAUTHORIZED if missing
middleware: t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new t.TRPCError({ code: 'UNAUTHORIZED', message: 'Missing user' });
}
return next();
}),
procedure: t.procedure,
// …
});
import { mergeRouters } from '@trpc/server';
import { publicRouter } from './public';
import { protectedRouter } from './protected';
export const appRouter = mergeRouters<Context>(publicRouter, protectedRouter);
// export type AppRouter = typeof appRouter;
import { appRouter } from '@/trpc/_app';
import type { Context } from '@/trpc/context';
const mockCtx: Context = { user: null }; // or {} if user is optional
// call a protected procedure
await appRouter.protectedProcedure.someQuery.call({ ctx: mockCtx });
// expect TRPCError with code 'UNAUTHORIZED'
TRPCError with code === 'UNAUTHORIZED'. If you see a TypeError about undefined user, verify that the merged router was typed with <Context> and that the mock ctx includes all required non‑optional fields.Compile the project (tsc --noEmit) and check that the inferred type of appRouter shows Context exactly, not a union. Then run the test; the assertion should pass.
If you are unsure whether your middleware is the source of the UNAUTHORIZED error, share the middleware code so we can confirm it throws TRPCError.UNAUTHORIZED when ctx.user is missing.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.