diff --git a/example/basic/index.js b/example/basic/index.js new file mode 100644 index 0000000..3fe95c9 --- /dev/null +++ b/example/basic/index.js @@ -0,0 +1,22 @@ +import { dirname, join } from 'path' +import { fileURLToPath } from 'url' + +import openapiAutoload from '../../index.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const fixturesDir = join(__dirname, 'routes') + +export default async function app (fastify, opts) { + fastify.register(openapiAutoload, { + handlersDir: join(fixturesDir, 'handlers'), + openapiOpts: { specification: join(fixturesDir, 'spec', 'test-spec.yaml') } + }) +} + +// export const options = { +// https: { +// key: readFileSync(join('local-certs', 'autotelic.localhost-key.pem')), +// cert: readFileSync(join('local-certs', 'autotelic.localhost.pem')) +// }, +// maxParamLength: 500 +// } diff --git a/example/basic/package.json b/example/basic/package.json new file mode 100644 index 0000000..79f0d33 --- /dev/null +++ b/example/basic/package.json @@ -0,0 +1,14 @@ +{ + "name": "fastify-openapi-autoload-example", + "description": "An example to configure @autotelic/fastify-openapi-autoload", + "main": "index.js", + "type": "module", + "scripts": { + "start": "fastify start -w -l info -P -o index.js" + }, + "dependencies": { + "fastify": "^4.25.2", + "fastify-cli": "^6.0.0", + "fastify-plugin": "^4.5.1" + } +} diff --git a/example/basic/routes/handlers/getFoo/index.js b/example/basic/routes/handlers/getFoo/index.js new file mode 100644 index 0000000..44bfc2b --- /dev/null +++ b/example/basic/routes/handlers/getFoo/index.js @@ -0,0 +1,7 @@ +export default async (fastify, { operationId }) => { + fastify.decorate(operationId, async (req, reply) => { + reply.code(200).send({ foo: 'bar' }) + }) +} + +export const autoConfig = { operationId: 'getFoo' } diff --git a/example/basic/routes/spec/test-spec.yaml b/example/basic/routes/spec/test-spec.yaml new file mode 100644 index 0000000..2f2ba2e --- /dev/null +++ b/example/basic/routes/spec/test-spec.yaml @@ -0,0 +1,27 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: Test Spec + license: + name: MIT + +paths: + /foo: + get: + summary: test GET route /foo + operationId: getFoo + security: + - bearerAuth: [] + tags: + - foo + responses: + '204': + description: test GET route /foo + content: + application/json: + schema: + type: object + properties: + foo: + type: string + diff --git a/example/index.js b/example/jwtJks/index.js similarity index 67% rename from example/index.js rename to example/jwtJks/index.js index d5fbd19..5facf51 100644 --- a/example/index.js +++ b/example/jwtJks/index.js @@ -6,13 +6,44 @@ import { createSigner } from 'fast-jwt' import fastifyPlugin from 'fastify-plugin' import { generateKeyPair, importSPKI, exportJWK } from 'jose' -import openapiAutoload from '../index.js' +import openapiAutoload from '../../index.js' +import { jwtJwksHandler } from '../../lib/jwtJwks.js' const __dirname = dirname(fileURLToPath(import.meta.url)) const fixturesDir = join(__dirname, 'routes') const ISSUER = 'https://autotelic.localhost:3000/' +export default async function app (fastify, opts) { + // mock jwks endpoint and jwt token: + const { jwk, jwtToken } = await generateKeys() + fastify.register(fastifyPlugin(async (fastify, options) => { + const { operationId = 'getJwks' } = options + fastify.decorate(operationId, async () => ({ keys: [jwk] })) + })) + console.log('\n\x1b[33m ========HTTPie test request:========\x1b[0m\n') + console.log(`\x1b[33mhttp https://autotelic.localhost:3000/foo 'Authorization:Bearer ${jwtToken}'\x1b[0m\n\n`) + + // use jwt/jwks as security handler: + const makeSecurityHandlers = jwtJwksHandler({ issuer: ISSUER, authRequestDecorator: auth }) + + // register openapiAutoload: + fastify.register(openapiAutoload, { + handlersDir: join(fixturesDir, 'handlers'), + openapiOpts: { specification: join(fixturesDir, 'spec', 'test-spec.yaml') }, + makeSecurityHandlers + }) +} + +export const options = { + https: { + key: readFileSync(join('local-certs', 'autotelic.localhost-key.pem')), + cert: readFileSync(join('local-certs', 'autotelic.localhost.pem')) + }, + maxParamLength: 500 +} + +// ==== HELPER FUNCTION TO GENERATE JWT TOKEN AND JWK ==== // async function generateKeys () { const JWT_SIGNING_ALGORITHM = 'RS256' const { publicKey, privateKey } = await generateKeyPair(JWT_SIGNING_ALGORITHM) @@ -39,27 +70,24 @@ async function generateKeys () { } } -export default async function app (fastify, opts) { - const { jwk, jwtToken } = await generateKeys() - fastify.register(fastifyPlugin(async (fastify, options) => { - const { operationId = 'getJwks' } = options - fastify.decorate(operationId, async () => ({ keys: [jwk] })) - })) - - fastify.register(openapiAutoload, { - handlersDir: join(fixturesDir, 'handlers'), - jwksOpts: { whiteListedIssuer: ISSUER }, - openapiOpts: { specification: join(fixturesDir, 'spec', 'test-spec.yaml') } - }) - - console.log('\n\x1b[33m ========HTTPie test request:========\x1b[0m\n') - console.log(`\x1b[33mhttp https://autotelic.localhost:3000/foo 'Authorization:Bearer ${jwtToken}'\x1b[0m\n\n`) +// ==== AUTH & SECURITY HANDLERS ==== // +async function auth (request) { + try { + const decodedToken = await request.jwtVerify(request) + const { userId } = decodedToken + return userId + } catch (err) { + return null + } } -export const options = { - https: { - key: readFileSync(join('local-certs', 'autotelic.localhost-key.pem')), - cert: readFileSync(join('local-certs', 'autotelic.localhost.pem')) - }, - maxParamLength: 500 -} +// const defaultHandlers = { +// async bearerAuth (request, reply, params) { +// try { +// const userId = await request.authenticate(request) +// if (userId == null) throw new Error('no user id') +// } catch (e) { +// throw new Error(e) +// } +// } +// } diff --git a/example/local-certs/.gitignore b/example/jwtJks/local-certs/.gitignore similarity index 100% rename from example/local-certs/.gitignore rename to example/jwtJks/local-certs/.gitignore diff --git a/example/package.json b/example/jwtJks/package.json similarity index 92% rename from example/package.json rename to example/jwtJks/package.json index 984ffc7..61e92b6 100644 --- a/example/package.json +++ b/example/jwtJks/package.json @@ -1,6 +1,6 @@ { "name": "fastify-openapi-autoload-example", - "description": "An example to configure @autotelic/fastify-openapi-autoload", + "description": "An example to configure @autotelic/fastify-openapi-autoload with jwt/jwks", "main": "index.js", "type": "module", "scripts": { diff --git a/example/routes/handlers/getFoo/index.js b/example/jwtJks/routes/handlers/getFoo/index.js similarity index 100% rename from example/routes/handlers/getFoo/index.js rename to example/jwtJks/routes/handlers/getFoo/index.js diff --git a/example/routes/spec/test-spec.yaml b/example/jwtJks/routes/spec/test-spec.yaml similarity index 100% rename from example/routes/spec/test-spec.yaml rename to example/jwtJks/routes/spec/test-spec.yaml diff --git a/index.js b/index.js index 38c1177..83d39b1 100644 --- a/index.js +++ b/index.js @@ -1,15 +1,12 @@ import { existsSync } from 'fs' -import https from 'node:https' import { fastifyAutoload } from '@fastify/autoload' -import fastifyJwt from '@fastify/jwt' import openapiGlue from 'fastify-openapi-glue' import fastifyPlugin from 'fastify-plugin' -import buildGetJwks from 'get-jwks' async function openapiAutoload (fastify, options = {}) { - const { handlersDir, jwksOpts = {}, openapiOpts = {} } = options - const { specification, operationResolver = null, securityHandlers = null } = openapiOpts + const { handlersDir, openapiOpts = {}, makeSecurityHandlers, makeOperationResolver } = options + const { specification, operationResolver = null } = openapiOpts // Validate handlers directory if (!handlersDir || !existsSync(handlersDir)) { @@ -22,31 +19,6 @@ async function openapiAutoload (fastify, options = {}) { } try { - // If whiteListed is provided, register fastifyJwt and decorate request with `authenticate` - if (jwksOpts.whiteListedIssuer) { - const getJwks = makeJwks(jwksOpts) - // Register JWT verify - fastify.register(fastifyJwt, { - decode: { complete: true }, - secret: (request, token, callback) => { - const { header: { kid, alg }, payload: { iss } } = token - return getJwks.getPublicKey({ kid, domain: iss, alg }) - .then(publicKey => callback(null, publicKey), callback) - } - }) - - // Decorate request with authenticate - fastify.decorateRequest('authenticate', async (request) => { - try { - const decodedToken = await request.jwtVerify(request) - const { userId } = decodedToken - return userId - } catch (err) { - return null - } - }) - } - // Register fastifyAutoload for handlers fastify.register(fastifyAutoload, { dir: handlersDir, @@ -55,13 +27,22 @@ async function openapiAutoload (fastify, options = {}) { encapsulate: false }) - // Register openapiGlue for OpenAPI integration - fastify.register(openapiGlue, { + const openapiGlueOpts = { specification, - operationResolver: operationResolver || makeOperationResolver(fastify), - securityHandlers: securityHandlers || makeSecurityHandlers(fastify), + operationResolver: operationResolver || defaultResolverFactory(fastify), ...openapiOpts - }) + } + + // factory/creator function params for security handlers & operation resolver + if (makeSecurityHandlers) { + openapiGlueOpts.securityHandlers = makeSecurityHandlers(fastify) + } + if (makeOperationResolver) { + openapiGlueOpts.operationResolver = makeOperationResolver(fastify) + } + + // Register openapiGlue for OpenAPI integration + fastify.register(openapiGlue, openapiGlueOpts) } catch (error) { const errorMessage = `fastify-openapi-autoload: Error registering plugins - ${error.message}` fastify.log.error(errorMessage) @@ -69,48 +50,13 @@ async function openapiAutoload (fastify, options = {}) { } } -function makeOperationResolver (fastify) { +function defaultResolverFactory (fastify) { return (operationId) => { fastify.log.info(`fastify-openapi-autoload - has '${operationId}' decorator: ${fastify.hasDecorator(operationId)}`) return fastify[operationId] } } -// TODO(meghein): update securityHandler with reasonable defaults -function makeSecurityHandlers (fastify) { - return { - async bearerAuth (request, reply, params) { - try { - const userId = await request.authenticate(request) - if (userId == null) throw new Error('no user id') - } catch (e) { - throw new Error(e) - } - } - } -} - -function makeJwks ({ - max = 100, - ttl = 60 * 1000, - timeout = 5000, - whiteListedIssuer, - providerDiscovery = false, - agent = new https.Agent({ keepAlive: true }), - ...opts -}) { - return buildGetJwks({ - max, - ttl, - timeout, - issuersWhitelist: [whiteListedIssuer], - checkIssuer: (issuer) => issuer === whiteListedIssuer, - providerDiscovery, - agent, - ...opts - }) -} - const fastifyOpenapiAutoload = fastifyPlugin(openapiAutoload, { name: 'fastify-openapi-autoload' }) diff --git a/lib/jwtJwks.js b/lib/jwtJwks.js new file mode 100644 index 0000000..dc3c58f --- /dev/null +++ b/lib/jwtJwks.js @@ -0,0 +1,69 @@ +import https from 'node:https' + +import fastifyJwt from '@fastify/jwt' +import buildGetJwks from 'get-jwks' + +export function jwtJwksHandler ({ + jwksOpts = {}, + issuer, + authRequestDecorator = defaultAuth, + securityHandlers = defaultHandlers +} = {}) { + const { + max = 100, + ttl = 60 * 1000, + timeout = 5000, + providerDiscovery = false, + agent = new https.Agent({ keepAlive: true }), + ...opts + } = jwksOpts + + const getJwks = buildGetJwks({ + max, + ttl, + timeout, + issuersWhitelist: [issuer], + checkIssuer: (iss) => iss === issuer, + providerDiscovery, + agent, + ...opts + }) + + return function makeSecurityHandler (fastify) { + // Register JWT verify + fastify.register(fastifyJwt, { + decode: { complete: true }, + secret: (_request, token, callback) => { + const { header: { kid, alg }, payload: { iss } } = token + return getJwks.getPublicKey({ kid, domain: iss, alg }) + .then(publicKey => callback(null, publicKey), callback) + } + }) + + // Decorate request with authenticate method + fastify.decorateRequest('authenticate', authRequestDecorator) + + return securityHandlers + } +} + +async function defaultAuth (request) { + try { + const decodedToken = await request.jwtVerify(request) + const { userId } = decodedToken + return userId + } catch (err) { + return null + } +} + +const defaultHandlers = { + async bearerAuth (request, reply, params) { + try { + const userId = await request.authenticate(request) + if (userId == null) throw new Error('no user id') + } catch (e) { + throw new Error(e) + } + } +} diff --git a/package.json b/package.json index a7a115f..6e05fa9 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,8 @@ "homepage": "https://github.com/autotelic/fastify-openapi-autoload#readme", "dependencies": { "@fastify/autoload": "^5.8.0", - "@fastify/jwt": "^8.0.0", "fastify-openapi-glue": "4.4.2", - "fastify-plugin": "^4.5.1", - "get-jwks": "^9.0.0" + "fastify-plugin": "^4.5.1" }, "devDependencies": { "@autotelic/eslint-config-js": "^0.2.1", @@ -52,6 +50,10 @@ "tsd": "^0.30.3", "typescript": "^5.3.3" }, + "peerDependencies": { + "@fastify/jwt": "^8.0.0", + "get-jwks": "^9.0.0" + }, "tsd": { "directory": "types" } diff --git a/test/error.test.js b/test/error.test.js new file mode 100644 index 0000000..0beb92e --- /dev/null +++ b/test/error.test.js @@ -0,0 +1,62 @@ +import { dirname, join } from 'path' +import { fileURLToPath } from 'url' + +import fastifyInjector from '@autotelic/fastify-injector' +import { test } from 'tap' + +import openapiAutoload from '../index.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const fixturesDir = join(__dirname, 'fixtures') + +function buildApp ({ + hasHandlersDir = true, + handlersDir = join(fixturesDir, 'handlers'), + hasSpec = true, + specification = join(fixturesDir, 'test-spec.yaml') +} = {}) { + const fastify = fastifyInjector() + + const pluginOpts = {} + + if (hasHandlersDir) { + pluginOpts.handlersDir = handlersDir + } + + if (hasSpec) { + pluginOpts.openapiOpts = { specification } + } + + fastify.register(openapiAutoload, pluginOpts) + + return fastify +} + +test('fastify-openapi-autoload should throw an error if the `handlersDir` param is missing', async ({ rejects }) => { + const fastify = buildApp({ hasHandlersDir: false }) + + const appError = new Error('fastify-openapi-autoload: Missing or invalid `handlersDir`. Please specify a valid directory where your handlers are located.') + + await rejects(fastify.ready(), appError) +}) + +test('fastify-openapi-autoload should throw an error if the `handlersDir` path is invalid', async ({ rejects }) => { + const fastify = buildApp({ handlersDir: join(fixturesDir, 'invalidPath') }) + + const appError = new Error('fastify-openapi-autoload: Missing or invalid `handlersDir`. Please specify a valid directory where your handlers are located.') + await rejects(fastify.ready(), appError) +}) + +test('fastify-openapi-autoload should throw an error if the `specification` param is missing', async ({ rejects }) => { + const fastify = buildApp({ hasSpec: false }) + + const appError = new Error('fastify-openapi-autoload: Missing or invalid `openapi.specification`. Please provide a valid OpenAPI specification file.') + await rejects(fastify.ready(), appError) +}) + +test('fastify-openapi-autoload should throw an error if the `specification` path is invalid', async ({ rejects }) => { + const fastify = buildApp({ specification: 'invalidPath' }) + + const appError = new Error('fastify-openapi-autoload: Missing or invalid `openapi.specification`. Please provide a valid OpenAPI specification file.') + await rejects(fastify.ready(), appError) +}) diff --git a/test/index.js b/test/index.js deleted file mode 100644 index 87da8f8..0000000 --- a/test/index.js +++ /dev/null @@ -1,130 +0,0 @@ -import { dirname, join } from 'path' -import { fileURLToPath } from 'url' - -import fastifyInjector from '@autotelic/fastify-injector' -import { test } from 'tap' - -import openapiAutoload from '../index.js' - -const __dirname = dirname(fileURLToPath(import.meta.url)) -const fixturesDir = join(__dirname, 'fixtures') - -function buildApp ({ - operationResolver, - hasHandlersDir = true, - handlersDir = join(fixturesDir, 'handlers'), - hasSpec = true, - specification = join(fixturesDir, 'test-spec.yaml'), - hasPluginError = false -} = {}) { - const injectorOpts = {} - - if (hasPluginError) { - injectorOpts.plugins = { 'fastify-openapi-glue': new Error('fastify-openapi-glue error') } - } - const fastify = fastifyInjector(injectorOpts) - - const pluginOpts = {} - - if (hasHandlersDir) { - pluginOpts.handlersDir = handlersDir - } - - if (hasSpec) { - pluginOpts.openapiOpts = { specification } - } - - if (operationResolver) { - pluginOpts.openapiOpts.operationResolver = operationResolver - } - - fastify.register(openapiAutoload, pluginOpts) - - return fastify -} - -test('fastify-openapi-autoload plugin should exist', async ({ ok }) => { - const fastify = buildApp() - await fastify.ready() - - ok(fastify.hasPlugin('fastify-openapi-autoload')) - ok(fastify.hasPlugin('fastify-openapi-glue')) -}) - -test('fastify-openapi-autoload should make operation resolvers', async ({ ok }) => { - const fastify = buildApp() - await fastify.ready() - - ok(fastify.getFoo) - ok(fastify.getBar) - ok(fastify.postBaz) -}) - -test('fastify-openapi-autoload should register routes', async ({ equal }) => { - const fastify = buildApp() - await fastify.ready() - - const expectedRoutes = '└── /\n' + - ' ├── foo (GET, HEAD)\n' + - ' └── ba\n' + - ' ├── r (GET, HEAD)\n' + - ' └── z (POST)\n' - const routes = fastify.printRoutes() - - // OpenAPI doesn't register routes traditionally, but we can print all routes to check: - equal(routes, expectedRoutes) -}) - -test('fastify-openapi-autoload will use a custom operation resolver if provided', async ({ equal }) => { - const fastify = buildApp({ - operationResolver: (fastify) => (operationId) => { - fastify.log.info('custom resolver') - return fastify[operationId] - } - }) - - await fastify.ready() - - fastify.log.info = (msg) => { - equal(msg, 'custom resolver') - } -}) - -test('fastify-openapi-autoload should throw an error if the `dir` param is missing', async ({ rejects }) => { - const fastify = buildApp({ hasHandlersDir: false }) - - const appError = new Error('fastify-openapi-autoload: Missing or invalid `handlersDir`. Please specify a valid directory where your handlers are located.') - - await rejects(fastify.ready(), appError) -}) - -test('fastify-openapi-autoload should throw an error if the `dir` path is invalid', async ({ rejects }) => { - const fastify = buildApp({ handlersDir: join(fixturesDir, 'invalidPath') }) - - const appError = new Error('fastify-openapi-autoload: Missing or invalid `handlersDir`. Please specify a valid directory where your handlers are located.') - await rejects(fastify.ready(), appError) -}) - -test('fastify-openapi-autoload should throw an error if the `spec` param is missing', async ({ rejects }) => { - const fastify = buildApp({ hasSpec: false }) - - const appError = new Error('fastify-openapi-autoload: Missing or invalid `openapi.specification`. Please provide a valid OpenAPI specification file.') - await rejects(fastify.ready(), appError) -}) - -test('fastify-openapi-autoload should throw an error if the `spec` path is invalid', async ({ rejects }) => { - const fastify = buildApp({ specification: 'invalidPath' }) - - const appError = new Error('fastify-openapi-autoload: Missing or invalid `openapi.specification`. Please provide a valid OpenAPI specification file.') - await rejects(fastify.ready(), appError) -}) - -test('fastify-openapi-autoload plugin error handling', async ({ equal, rejects }) => { - const fastify = buildApp({ hasPluginError: true }) - - fastify.log.error = (msg) => { - equal(msg, 'fastify-openapi-autoload: Error registering plugins - fastify-plugin expects a function, instead got a \'object\'') - } - - await rejects(fastify.ready(), new Error('fastify-openapi-autoload: Error registering plugins - fastify-plugin expects a function, instead got a \'object\'')) -}) diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 0000000..bfadde6 --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,175 @@ +import { dirname, join } from 'path' +import { fileURLToPath } from 'url' + +import fastifyInjector from '@autotelic/fastify-injector' +import { test } from 'tap' + +import openapiAutoload from '../index.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const fixturesDir = join(__dirname, 'fixtures') + +function buildApp ({ + operationResolver, + makeOperationResolver, + makeSecurityHandlers, + hasPluginError = false +} = {}) { + const injectorOpts = {} + + if (hasPluginError) { + injectorOpts.plugins = { 'fastify-openapi-glue': new Error('fastify-openapi-glue error') } + } + const fastify = fastifyInjector(injectorOpts) + + const pluginOpts = { + handlersDir: join(fixturesDir, 'handlers'), + openapiOpts: { + specification: join(fixturesDir, 'test-spec.yaml') + } + } + + if (operationResolver) { + pluginOpts.openapiOpts.operationResolver = operationResolver + } + + if (makeOperationResolver) { + pluginOpts.makeOperationResolver = makeOperationResolver + } + if (makeSecurityHandlers) { + pluginOpts.makeSecurityHandlers = makeSecurityHandlers + } + + fastify.register(openapiAutoload, pluginOpts) + + return fastify +} + +test('plugin should exist', async ({ ok, teardown }) => { + teardown(async () => fastify.close()) + + const fastify = buildApp() + await fastify.ready() + + ok(fastify.hasPlugin('fastify-openapi-autoload')) + ok(fastify.hasPlugin('fastify-openapi-glue')) +}) + +test('should register routes', async ({ equal, teardown }) => { + teardown(async () => fastify.close()) + + const fastify = buildApp() + await fastify.ready() + + const expectedRoutes = '└── /\n' + + ' ├── foo (GET, HEAD)\n' + + ' └── ba\n' + + ' ├── r (GET, HEAD)\n' + + ' └── z (POST)\n' + const routes = fastify.printRoutes() + + // OpenAPI doesn't register routes traditionally, but we can print all routes to check: + equal(routes, expectedRoutes) +}) + +test('should use default operation resolvers', async ({ ok, teardown }) => { + teardown(async () => fastify.close()) + + const fastify = buildApp() + await fastify.ready() + + ok(fastify.getFoo) + ok(fastify.getBar) + ok(fastify.postBaz) +}) + +test('should use a custom operation resolver if provided', async ({ equal }) => { + const fastify = buildApp({ + operationResolver: (operationId) => { + if (operationId === 'getFoo') { + return async (_req, reply) => { + reply.code(200).send('this is a test') + } + } + } + }) + + await fastify.ready() + + const response = await fastify.inject({ + method: 'GET', + url: '/foo' + }) + + equal(response.statusCode, 200) + equal(response.body, 'this is a test') +}) + +test('should use a custom operation resolver factory if provided', async ({ equal }) => { + const fastify = buildApp({ + makeOperationResolver: (fastify) => (operationId) => { + if (operationId === 'getFoo') { + return async (_req, reply) => { + reply.code(200).send('this is a test') + } + } + return fastify[operationId] + } + }) + + await fastify.ready() + + const fooResponse = await fastify.inject({ + method: 'GET', + url: '/foo' + }) + + equal(fooResponse.statusCode, 200) + equal(fooResponse.body, 'this is a test') + + const barResponse = await fastify.inject({ + method: 'GET', + url: '/bar' + }) + + equal(barResponse.statusCode, 200) + equal(barResponse.body, 'bar') +}) + +test('should use a custom security handlers factory if provided', async ({ equal, same }) => { + const fastify = buildApp({ + makeSecurityHandlers: (fastify) => { + fastify.decorateRequest('authenticate', async () => 123) + return { + async bearerAuth (request, reply, params) { + try { + const userId = await request.authenticate(request) + if (userId == null) throw new Error('no user id') + } catch (e) { + throw new Error(e) + } + } + } + } + }) + + await fastify.ready() + + const fooResponse = await fastify.inject({ + method: 'GET', + url: '/foo' + }) + + equal(fooResponse.statusCode, 200) + same(fooResponse.json(), { userId: 123 }) +}) + +test('plugin error handling', async ({ equal, rejects }) => { + const fastify = buildApp({ hasPluginError: true }) + + fastify.log.error = (msg) => { + equal(msg, 'fastify-openapi-autoload: Error registering plugins - fastify-plugin expects a function, instead got a \'object\'') + } + + await rejects(fastify.ready(), new Error('fastify-openapi-autoload: Error registering plugins - fastify-plugin expects a function, instead got a \'object\'')) +}) diff --git a/test/withSecurityHandlers.test.js b/test/jwtJwks.test.js similarity index 81% rename from test/withSecurityHandlers.test.js rename to test/jwtJwks.test.js index bcf203e..73031f0 100644 --- a/test/withSecurityHandlers.test.js +++ b/test/jwtJwks.test.js @@ -8,6 +8,7 @@ import nock from 'nock' import { test, beforeEach, afterEach } from 'tap' import openapiAutoload from '../index.js' +import { jwtJwksHandler } from '../lib/jwtJwks.js' beforeEach(() => { nock.disableNetConnect() @@ -51,25 +52,25 @@ async function buildApp () { // nock endpoint for jwks nock(ISSUER).get('/.well-known/jwks.json').reply(200, { keys: [jwk] }) + const makeSecurityHandlers = jwtJwksHandler({ issuer: ISSUER }) + fastify.register(openapiAutoload, { handlersDir: join(fixtureDir, 'handlers'), openapiOpts: { specification: join(fixtureDir, 'spec-with-security.yaml') }, - jwksOpts: { - whiteListedIssuer: ISSUER - } + makeSecurityHandlers }) return { fastify, jwtToken } } -test('fastify-openapi-autoload will register @fastify/jwt', async ({ ok }) => { +test('fastify-openapi-autoload with jwtJwks handlers should register @fastify/jwt', async ({ ok }) => { const { fastify } = await buildApp() await fastify.ready() ok(fastify.hasPlugin('@fastify/jwt')) }) -test('fastify-openapi-autoload will reject secure routes without a bearer token', async ({ same }) => { +test('fastify-openapi-autoload with jwtJwks handlers should reject secure routes without a bearer token', async ({ same }) => { const { fastify } = await buildApp() const results = await fastify.inject({ @@ -84,7 +85,7 @@ test('fastify-openapi-autoload will reject secure routes without a bearer token' }) }) -test('fastify-openapi-autoload will handle secure routes with a bearer token', async ({ same }) => { +test('fastify-openapi-autoload with jwtJwks handlers should pass secure routes with a bearer token', async ({ same }) => { const { fastify, jwtToken } = await buildApp() await fastify.ready()