UNPKG

trpc-shield

Version:

tRPC permissions as another layer of abstraction!

173 lines (143 loc) 5.09 kB
import { describe, it, expect } from 'vitest'; import { shield } from '../src/shield'; import { rule } from '../src/constructors'; import { createTestContext, createMockMiddlewareOpts } from './__helpers__/setup'; describe('Context Extension', () => { it('should extend context when rule returns context object', async () => { // Rule that extends context with user information const isAuthenticated = rule<{ user?: { id: string; role: string } | null }>()(async ( ctx, _type, _path, _input, _rawInput, _options, ) => { // Simulate authentication that adds user to context if (!ctx.user) { return { ctx: { user: { id: '1', role: 'user' } } }; } return true; }); const permissions = shield<{ user?: { id: string; role: string } | null }>({ query: { findManyUser: isAuthenticated, }, }); const ctx = createTestContext(); // ctx.user is null initially const opts = createMockMiddlewareOpts(ctx, 'query', 'findManyUser'); // Mock next to capture the context passed to it let nextCalledWith: any = null; opts.next = async (contextUpdate?: any) => { nextCalledWith = contextUpdate; return Promise.resolve(); }; await permissions(opts); // Verify the context was extended with user info expect(nextCalledWith?.ctx?.user).toEqual({ id: '1', role: 'user' }); }); it('should pass through context when rule returns true', async () => { const alwaysAllow = rule<{ user?: { id: string; role: string } | null }>()(async () => true); const permissions = shield<{ user?: { id: string; role: string } | null }>({ query: { publicQuery: alwaysAllow, }, }); const ctx = createTestContext(); const opts = createMockMiddlewareOpts(ctx, 'query', 'publicQuery'); let nextCalledWith: any = null; opts.next = async (contextUpdate?: any) => { nextCalledWith = contextUpdate; return Promise.resolve(); }; await permissions(opts); // When rule returns true, next() should be called without context update expect(nextCalledWith).toBeUndefined(); }); it('should reproduce the original bug report scenario', async () => { // This test reproduces the exact scenario from the bug report type Context = { user?: string | null; }; // isAuthenticated rule that extends context with user info (like from JWT token) const isAuthenticated = rule<Context>()(async (ctx) => { // Simulate token validation that would normally happen here if (!ctx.user) { // When authentication succeeds, extend context with user info return { ctx: { user: 'authenticated-user-id' } }; } return true; }); const permissions = shield<Context>({ query: { findManyUser: isAuthenticated, }, }); // Initial context with no user (like an unauthenticated request) const initialCtx: Context = { user: null }; const opts = createMockMiddlewareOpts(initialCtx, 'query', 'findManyUser'); let extendedContext: any = null; opts.next = async (contextUpdate?: any) => { extendedContext = contextUpdate; return Promise.resolve(); }; await permissions(opts); // After shield middleware runs, the context should be extended with user info expect(extendedContext?.ctx?.user).toBe('authenticated-user-id'); }); it('should handle context extension with typed interfaces', async () => { interface AuthContext { user?: { id: string; role: 'admin' | 'user'; email: string; } | null; } const authenticateWithRole = rule<AuthContext>()(async (ctx) => { if (!ctx.user) { // Simulate successful authentication with full user object return { ctx: { user: { id: 'user-123', role: 'admin' as const, email: 'admin@example.com', }, }, }; } return true; }); const permissions = shield<AuthContext>({ query: { adminQuery: authenticateWithRole, }, }); const ctx: AuthContext = { user: null }; const opts = createMockMiddlewareOpts(ctx, 'query', 'adminQuery'); let result: any = null; opts.next = async (contextUpdate?: any) => { result = contextUpdate; return Promise.resolve(); }; await permissions(opts); expect(result?.ctx?.user).toEqual({ id: 'user-123', role: 'admin', email: 'admin@example.com', }); }); it('should show how tRPC context extension should work', async () => { // This is how tRPC middleware context extension should work: const mockNext = async (contextUpdate?: { ctx?: any }) => { return Promise.resolve(contextUpdate); }; // Proper tRPC middleware pattern const result = await mockNext({ ctx: { user: { id: '1', role: 'user' }, }, }); expect(result?.ctx?.user).toEqual({ id: '1', role: 'user' }); }); });