learning_ai_common_plat/packages/fastify-auth/src/index.test.ts
saravanakumardb1 f61a1f0b04 feat(fastify-auth): create @bytelyst/fastify-auth package with JWT auth + request context
- createAuthMiddleware(): RS256 JWKS + HS256 fallback (parameterized)
- createRequestContext(): productId validation + getUserId()
- Fastify request type augmentation for jwtPayload
- 15 tests passing
2026-03-20 07:30:53 -07:00

146 lines
4.6 KiB
TypeScript

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<string, string | undefined>;
jwtPayload?: JwtPayload;
}
function makeReq(token?: string): MockReq {
return {
headers: {
authorization: token ? `Bearer ${token}` : undefined,
},
jwtPayload: undefined,
};
}
async function signToken(payload: Record<string, unknown>, 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<string, string | undefined>;
}) {
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'
);
});
});