import { describe, it, expect } from 'vitest'; import { SignJWT } from 'jose'; import { createAuthMiddleware, createRequestContext } from './index.js'; import type { JwtPayload } from './types.js'; const TEST_SECRET = 'test-jwt-secret-for-fastify-auth-package'; interface MockReq { headers: Record; jwtPayload?: JwtPayload; } function makeReq(token?: string): MockReq { return { headers: { authorization: token ? `Bearer ${token}` : undefined, }, jwtPayload: undefined, }; } async function signToken(payload: Record, secret = TEST_SECRET) { return new SignJWT(payload) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('5m') .sign(new TextEncoder().encode(secret)); } describe('createAuthMiddleware', () => { const { extractAuth, requireRole } = createAuthMiddleware({ jwtSecret: TEST_SECRET, }); it('extracts auth payload from valid access token', async () => { const token = await signToken({ sub: 'user-1', email: 'test@test.com', role: 'admin', type: 'access', }); const payload = await extractAuth(makeReq(token)); expect(payload.sub).toBe('user-1'); expect(payload.email).toBe('test@test.com'); expect(payload.role).toBe('admin'); }); it('throws UnauthorizedError when no Authorization header', async () => { await expect(extractAuth(makeReq())).rejects.toThrow('Unauthorized'); }); it('throws UnauthorizedError for invalid token', async () => { await expect(extractAuth(makeReq('bad-token'))).rejects.toThrow('Invalid or expired token'); }); it('throws UnauthorizedError for non-access token type', async () => { const token = await signToken({ sub: 'u1', type: 'refresh' }); await expect(extractAuth(makeReq(token))).rejects.toThrow('Invalid or expired token'); }); it('throws UnauthorizedError for wrong secret', async () => { const token = await signToken({ sub: 'u1', type: 'access' }, 'wrong'); await expect(extractAuth(makeReq(token))).rejects.toThrow('Invalid or expired token'); }); it('requireRole passes when role matches', async () => { const token = await signToken({ sub: 'u1', role: 'admin', type: 'access', }); const payload = await requireRole(makeReq(token), 'admin', 'superadmin'); expect(payload.sub).toBe('u1'); }); it('requireRole throws ForbiddenError when role does not match', async () => { const token = await signToken({ sub: 'u1', role: 'viewer', type: 'access', }); await expect(requireRole(makeReq(token), 'admin')).rejects.toThrow('Insufficient permissions'); }); it('requireRole passes with no required roles (any authenticated user)', async () => { const token = await signToken({ sub: 'u1', type: 'access', }); const payload = await requireRole(makeReq(token)); expect(payload.sub).toBe('u1'); }); }); describe('createRequestContext', () => { const { getRequestProductId, getUserId } = createRequestContext({ productId: 'testproduct', }); function makeFastifyReq(overrides?: { jwtPayload?: JwtPayload; headers?: Record; }) { const req = makeReq(); if (overrides?.jwtPayload !== undefined) req.jwtPayload = overrides.jwtPayload; if (overrides?.headers) Object.assign(req.headers, overrides.headers); return req as unknown as import('fastify').FastifyRequest; } it('returns product ID for valid request', () => { expect(getRequestProductId(makeFastifyReq())).toBe('testproduct'); }); it('returns product ID when JWT productId matches', () => { expect( getRequestProductId(makeFastifyReq({ jwtPayload: { sub: 'u1', productId: 'testproduct' } })) ).toBe('testproduct'); }); it('throws BadRequestError when JWT productId does not match', () => { expect(() => getRequestProductId(makeFastifyReq({ jwtPayload: { sub: 'u1', productId: 'wrong' } })) ).toThrow('Invalid productId'); }); it('throws BadRequestError when X-Product-Id header does not match', () => { expect(() => getRequestProductId(makeFastifyReq({ headers: { 'x-product-id': 'wrong' } })) ).toThrow('Invalid productId'); }); it('getUserId returns sub from JWT payload', () => { expect(getUserId(makeFastifyReq({ jwtPayload: { sub: 'user-42' } }))).toBe('user-42'); }); it('getUserId throws when no JWT payload', () => { expect(() => getUserId(makeFastifyReq())).toThrow('Missing userId'); }); it('getUserId throws when JWT has no sub', () => { expect(() => getUserId(makeFastifyReq({ jwtPayload: {} as JwtPayload }))).toThrow( 'Missing userId' ); }); });