kth-node-express-routing
Version:
Define named routes using express routing syntax. Allows exporting route defs javascript object
264 lines (225 loc) • 9.29 kB
JavaScript
jest.mock('express')
const { _routerMockup: ExpressRouterMockup } = jest.requireMock('express')
const { Router: ApiRouter } = require('./ApiRouter')
const { bold, green, EXPECTS, IS_ACCESSIBLE, RETURNS, THROWS, FAILS, WORKS } = require('../test')
function _expectIsApiScopeHandler({ handler, expectedScopes }) {
expect(handler).toBeFunction()
expect(handler).toHaveLength(3)
const [req, res, next] = [{}, {}, jest.fn()]
const result = handler(req, res, next)
expect(result).toBeUndefined()
expect(req).toEqual({ scope: expectedScopes })
expect(res).toEqual({})
expect(next).toHaveBeenCalledTimes(1)
expect(next).toHaveBeenCalledWith()
}
function runTestsAboutApiRouter() {
describe(`Exported function ${bold('ApiRouter()')}`, () => {
it(IS_ACCESSIBLE, () => expect(ApiRouter).toBeFunction())
it(`${EXPECTS} one argument (checkApiKeyMiddleware)`, () => expect(ApiRouter.length).toBe(1))
const _runBasicTestsOfApiRouter = middleware => {
const _text = middleware
? `- when passing a middleware - ${RETURNS} an object that`
: `- when used w/o arguments - ${RETURNS} an object that`
describe(_text, () => {
const uri = '/test'
const method = 'GET'
const openid = { scope_required: true, scopes: { api_key: ['read'] } }
it(`is an instance of "Router"`, () => {
const router = ApiRouter(middleware)
expect(router).toBeObject()
expect(router.constructor.name).toBe('Router')
})
it(`has ${bold('getRouter()')} to directly access the underlying Express.js Router`, () => {
const router = ApiRouter(middleware)
expect(router.getRouter()).toBe(ExpressRouterMockup)
})
it(`${green('allows')} adding an unsecured endpoint with ${bold('register()')}`, () => {
const router = ApiRouter(middleware)
const result = router.register({ uri, method }, jest.fn())
expect(result).toBe(ExpressRouterMockup)
})
if (middleware) {
it(`${green('allows')} adding a secure endpoint with ${bold('register()')}`, () => {
const router = ApiRouter(middleware)
const result = router.register({ uri, method, openid }, jest.fn())
expect(result).toBe(ExpressRouterMockup)
})
} else {
it(`${THROWS} an error when adding a secured endpoint with ${bold('register()')}`, () => {
const router = ApiRouter()
expect(() => router.register({ uri, method, openid }, jest.fn())).toThrow(
'Missing middleware to check api key scopes'
)
})
}
})
}
_runBasicTestsOfApiRouter()
_runBasicTestsOfApiRouter(jest.fn())
})
}
function runTestsAboutRegisterOfApiRouter() {
describe(`The function ${bold('register()')} delivered by "ApiRouter(middleware)"`, () => {
const middleware = jest.fn()
const uri = '/test'
const method = 'GET'
const openid = { scope_required: true, scopes: { api_key: ['read'] } }
let router = null
beforeAll(() => {
router = ApiRouter(middleware)
})
it(IS_ACCESSIBLE, () => expect(router.register).toBeFunction())
it(`${EXPECTS} one argument (apiPathObj)`, () => {
expect(router.register).toHaveLength(1)
})
it(`${green('accepts')} additional arguments (...routeHandlers)`, () => {
const handlers = [jest.fn(), jest.fn(), jest.fn()]
router.register({ uri, method, openid }, ...handlers)
expect(ExpressRouterMockup.get).toHaveBeenCalledTimes(1)
const [call] = ExpressRouterMockup.get.mock.calls
expect(call[1]).toBeFunction()
call[1] = 'scopeHandler'
expect(call).toEqual([uri, 'scopeHandler', middleware, ...handlers])
})
it(`${RETURNS} the underlying Express.js Router`, () => {
const result = router.register({ uri, method }, jest.fn())
expect(result).toBe(ExpressRouterMockup)
})
it(`- when registering an unsecure GET-endpoint - ${WORKS} as expected`, () => {
const handlers = [jest.fn(), jest.fn()]
router.register({ uri, method: 'GET' }, ...handlers)
const { get } = ExpressRouterMockup
expect(get).toHaveBeenCalledTimes(1)
expect(get).toHaveBeenCalledWith(uri, handlers[0], handlers[1])
})
it.each([['POST'], ['PUT'], ['PATCH'], ['DELETE']])(
`- when registering an unsecure %s-endpoint - ${FAILS} as expected`,
_method => {
const callback = () => router.register({ uri, method: _method }, jest.fn())
expect(callback).toThrow(`Missing prop "openid" in input, which is mandatory with method "${_method}"`)
}
)
it.each([['GET'], ['POST'], ['PUT'], ['PATCH'], ['DELETE']])(
`- when registering a secure %s-endpoint - ${WORKS} as expected`,
_method => {
const handlers = [jest.fn(), jest.fn()]
router.register({ uri, method: _method, openid }, ...handlers)
const verb = _method.toLowerCase()
const func = ExpressRouterMockup[verb]
expect(func).toHaveBeenCalledTimes(1)
const [call] = func.mock.calls
_expectIsApiScopeHandler({ handler: call[1], expectedScopes: openid.scopes.api_key })
call[1] = 'scopeHandler'
expect(call).toEqual([uri, 'scopeHandler', middleware, ...handlers])
}
)
const testDataConfigurations = [
{
apiPathObj: '/test',
errorText: 'this must be a `object` type',
},
{
apiPathObj: { uri },
errorText: 'method is a required field',
},
{
apiPathObj: { method },
errorText: 'uri is a required field',
},
{
apiPathObj: { uri, method: {} },
errorText: 'method must be a `string` type',
},
{
apiPathObj: { uri, method: 'test' },
errorText: 'method must match the following:',
},
{
apiPathObj: { uri, method: 'HEAD' },
errorText: 'method must match the following:',
},
{
apiPathObj: { uri, method: 'GET' },
errorText: null,
},
{
apiPathObj: { uri, method: 'gEt' },
errorText: null,
},
{
apiPathObj: { uri, method: 'get' },
errorText: null,
},
{
apiPathObj: { uri, method, openid: 179 },
errorText: 'openid must be a `object` type',
},
{
apiPathObj: { uri, method, openid: {} },
errorText: 'openid.scopes is a required field',
},
{
apiPathObj: { uri, method, openid: { scope_required: false } },
errorText: 'openid.scopes is a required field',
},
{
apiPathObj: { uri, method, openid: { scopes: {} } },
errorText: 'openid.scopes must contain exactly one strategy',
},
{
apiPathObj: { uri, method, openid: { scopes: { api_key: {} } } },
errorText: 'openid.scopes.api_key must be a `array` type',
},
{
apiPathObj: { uri, method, openid: { scopes: { api_key: [] } } },
errorText: 'openid.scopes.api_key field must have at least 1 item',
},
{
apiPathObj: { uri, method, openid: { scopes: { api_key: [179, 'read', 'write'] } } },
errorText: 'openid.scopes.api_key[0] must be a `string` type',
},
{
apiPathObj: { uri, method, openid: { scopes: { api_key: ['read'], connect: ['write'] } } },
errorText: 'openid.scopes field has unspecified keys: connect',
},
{
apiPathObj: { uri, method, openid: { scopes: { api_key: ['read'] } } },
errorText: 'openid.scope_required is a required field',
},
{
apiPathObj: { uri, method, openid: { scope_required: {}, scopes: { api_key: ['read'] } } },
errorText: 'openid.scope_required must be a `boolean` type',
},
{
apiPathObj: { uri, method, openid: { scope_required: false, scopes: { api_key: ['read'] } } },
errorText: 'openid.scope_required must be one of the following values: true',
},
{
apiPathObj: { uri, method, openid: { scope_required: true, scopes: { connect: ['write'] } } },
errorText: 'openid.scopes field has unspecified keys: connect',
},
{
apiPathObj: { uri, method, openid: { scope_required: true, scopes: { api_key: ['read', 'write', 'remove'] } } },
errorText: null,
},
]
const validConfigurations = testDataConfigurations.filter(item => item.errorText == null)
it.each(validConfigurations.map(item => [JSON.stringify(item.apiPathObj), item]))(
`- when used with valid config %s - ${WORKS} as expected`,
(caption1, { apiPathObj }) => {
router.register(apiPathObj, jest.fn())
}
)
const invalidConfigurations = testDataConfigurations.filter(item => item.errorText != null)
it.each(invalidConfigurations.map(item => [JSON.stringify(item.apiPathObj), item]))(
`- when used with invalid config %s - ${FAILS} as expected`,
(caption1, { apiPathObj, errorText }) => {
const callback = () => router.register(apiPathObj, jest.fn())
expect(callback).toThrow(errorText)
}
)
})
}
runTestsAboutApiRouter()
runTestsAboutRegisterOfApiRouter()