0
0
mirror of https://github.com/honojs/hono.git synced 2024-11-27 16:06:24 +01:00
hono/src/compose.test.ts
Yusuke Wada 5f78e4e1b3
chore(lint): ESLint v9 (#3393)
* chore(lint): ESLint v9

* run prettier
2024-09-12 12:05:41 +09:00

740 lines
19 KiB
TypeScript

import { compose } from './compose'
import { Context } from './context'
import type { Params } from './router'
type C = {
req: Record<string, string>
res: Record<string, string>
finalized: boolean
}
type MiddlewareTuple = [[Function, unknown], Params]
class ExpectedError extends Error {}
function buildMiddlewareTuple(fn: Function, params?: Params): MiddlewareTuple {
return [[fn, undefined], params || {}]
}
describe('compose', () => {
const middleware: MiddlewareTuple[] = []
const a = async (c: C, next: Function) => {
c.req['log'] = 'log'
await next()
}
const b = async (c: C, next: Function) => {
await next()
c.res['headers'] = 'custom-header'
}
const c = async (c: C, next: Function) => {
c.req['xxx'] = 'yyy'
await next()
c.res['zzz'] = c.req['xxx']
}
const handler = async (c: C, next: Function) => {
c.req['log'] = `${c.req.log} message`
await next()
c.res = { message: 'new response' }
}
middleware.push(buildMiddlewareTuple(a))
middleware.push(buildMiddlewareTuple(b))
middleware.push(buildMiddlewareTuple(c))
middleware.push(buildMiddlewareTuple(handler))
it('Request', async () => {
const c: C = { req: {}, res: {}, finalized: false }
const composed = compose<C>(middleware)
const context = await composed(c)
expect(context.req['log']).not.toBeNull()
expect(context.req['log']).toBe('log message')
expect(context.req['xxx']).toBe('yyy')
})
it('Response', async () => {
const c: C = { req: {}, res: {}, finalized: false }
const composed = compose<C>(middleware)
const context = await composed(c)
expect(context.res['headers']).not.toBeNull()
expect(context.res['headers']).toBe('custom-header')
expect(context.res['message']).toBe('new response')
expect(context.res['zzz']).toBe('yyy')
})
})
describe('compose with returning a promise, non-async function', () => {
const handlers: MiddlewareTuple[] = [
buildMiddlewareTuple(() => {
return new Promise((resolve) =>
setTimeout(() => {
resolve({ message: 'new response' })
}, 1)
)
}),
]
it('Response', async () => {
const c: C = { req: {}, res: {}, finalized: false }
const composed = compose<C>(handlers)
const context = await composed(c)
expect(context.res['message']).toBe('new response')
})
})
describe('Handler and middlewares', () => {
const middleware: MiddlewareTuple[] = []
const req = new Request('http://localhost/')
const c: Context = new Context(req)
const mHandlerFoo = async (c: Context, next: Function) => {
c.req.raw.headers.append('x-header-foo', 'foo')
await next()
}
const mHandlerBar = async (c: Context, next: Function) => {
await next()
c.header('x-header-bar', 'bar')
}
const handler = (c: Context) => {
const foo = c.req.header('x-header-foo') || ''
return c.text(foo)
}
middleware.push(buildMiddlewareTuple(mHandlerFoo))
middleware.push(buildMiddlewareTuple(mHandlerBar))
middleware.push(buildMiddlewareTuple(handler))
it('Should return 200 Response', async () => {
const composed = compose<Context>(middleware)
const context = await composed(c)
const res = context.res
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(await res.text()).toBe('foo')
expect(res.headers.get('x-header-bar')).toBe('bar')
})
})
describe('compose with Context - 200 success', () => {
const middleware: MiddlewareTuple[] = []
const req = new Request('http://localhost/')
const c: Context = new Context(req)
const handler = (c: Context) => {
return c.text('Hello')
}
const mHandler = async (_c: Context, next: Function) => {
await next()
}
middleware.push(buildMiddlewareTuple(handler))
middleware.push(buildMiddlewareTuple(mHandler))
it('Should return 200 Response', async () => {
const composed = compose<Context>(middleware)
const context = await composed(c)
expect(context.res).not.toBeNull()
expect(context.res.status).toBe(200)
expect(await context.res.text()).toBe('Hello')
})
})
describe('compose with Context - 404 not found', () => {
const middleware: MiddlewareTuple[] = []
const req = new Request('http://localhost/')
const onNotFound = (c: Context) => {
return c.text('onNotFound', 404)
}
const onNotFoundAsync = async (c: Context) => {
return c.text('onNotFoundAsync', 404)
}
const mHandler = async (_c: Context, next: Function) => {
await next()
}
middleware.push(buildMiddlewareTuple(mHandler))
it('Should return 404 Response', async () => {
const c: Context = new Context(req)
const composed = compose<Context>(middleware, undefined, onNotFound)
const context = await composed(c)
expect(context.res).not.toBeNull()
expect(context.res.status).toBe(404)
expect(await context.res.text()).toBe('onNotFound')
expect(context.finalized).toBe(true)
})
it('Should return 404 Response - async handler', async () => {
const c: Context = new Context(req)
const composed = compose<Context>(middleware, undefined, onNotFoundAsync)
const context = await composed(c)
expect(context.res).not.toBeNull()
expect(context.res.status).toBe(404)
expect(await context.res.text()).toBe('onNotFoundAsync')
expect(context.finalized).toBe(true)
})
})
describe('compose with Context - 401 not authorized', () => {
const middleware: MiddlewareTuple[] = []
const req = new Request('http://localhost/')
const c: Context = new Context(req)
const handler = (c: Context) => {
return c.text('Hello')
}
const mHandler = async (c: Context, next: Function) => {
await next()
c.res = new Response('Not authorized', { status: 401 })
}
middleware.push(buildMiddlewareTuple(mHandler))
middleware.push(buildMiddlewareTuple(handler))
it('Should return 401 Response', async () => {
const composed = compose<Context>(middleware)
const context = await composed(c)
expect(context.res).not.toBeNull()
expect(context.res.status).toBe(401)
expect(await context.res.text()).toBe('Not authorized')
expect(context.finalized).toBe(true)
})
})
describe('compose with Context - next() below', () => {
const middleware: MiddlewareTuple[] = []
const req = new Request('http://localhost/')
const c: Context = new Context(req)
const handler = (c: Context) => {
const message = c.req.header('x-custom') || 'blank'
return c.text(message)
}
const mHandler = async (c: Context, next: Function) => {
c.req.raw.headers.append('x-custom', 'foo')
await next()
}
middleware.push(buildMiddlewareTuple(mHandler))
middleware.push(buildMiddlewareTuple(handler))
it('Should return 200 Response', async () => {
const composed = compose<Context>(middleware)
const context = await composed(c)
expect(context.res).not.toBeNull()
expect(context.res.status).toBe(200)
expect(await context.res.text()).toBe('foo')
expect(context.finalized).toBe(true)
})
})
describe('compose with Context - 500 error', () => {
const middleware: MiddlewareTuple[] = []
const req = new Request('http://localhost/')
const c: Context = new Context(req)
it('Error on handler', async () => {
const handler = () => {
throw new Error()
}
const mHandler = async (_c: Context, next: Function) => {
await next()
}
middleware.push(buildMiddlewareTuple(mHandler))
middleware.push(buildMiddlewareTuple(handler))
const onNotFound = (c: Context) => c.text('NotFound', 404)
const onError = (_error: Error, c: Context) => c.text('onError', 500)
const composed = compose<Context>(middleware, onError, onNotFound)
const context = await composed(c)
expect(context.res).not.toBeNull()
expect(context.res.status).toBe(500)
expect(await context.res.text()).toBe('onError')
expect(context.finalized).toBe(true)
})
it('Error on handler - async', async () => {
const handler = () => {
throw new Error()
}
middleware.push(buildMiddlewareTuple(handler))
const onError = async (_error: Error, c: Context) => c.text('onError', 500)
const composed = compose<Context>(middleware, onError)
const context = await composed(c)
expect(context.res).not.toBeNull()
expect(context.res.status).toBe(500)
expect(await context.res.text()).toBe('onError')
expect(context.finalized).toBe(true)
})
it('Run all the middlewares', async () => {
const ctx: C = { req: {}, res: {}, finalized: false }
const stack: number[] = []
const middlewares = [
async (_ctx: C, next: Function) => {
stack.push(0)
await next()
},
async (_ctx: C, next: Function) => {
stack.push(1)
await next()
},
async (_ctx: C, next: Function) => {
stack.push(2)
await next()
},
].map((h) => buildMiddlewareTuple(h))
const composed = compose(middlewares)
await composed(ctx)
expect(stack).toEqual([0, 1, 2])
})
})
describe('compose with Context - not finalized', () => {
const req = new Request('http://localhost/')
const c: Context = new Context(req)
const onNotFound = (c: Context) => {
return c.text('onNotFound', 404)
}
it('Should not be finalized - lack `next()`', async () => {
const middleware: MiddlewareTuple[] = []
const mHandler = async (_c: Context, next: Function) => {
await next()
}
const mHandler2 = async () => {}
middleware.push(buildMiddlewareTuple(mHandler))
middleware.push(buildMiddlewareTuple(mHandler2))
const composed = compose<Context>(middleware, undefined, onNotFound)
const context = await composed(c)
expect(context.finalized).toBe(false)
})
it('Should not be finalized - lack `return Response`', async () => {
const middleware2: MiddlewareTuple[] = []
const mHandler3 = async (_c: Context, next: Function) => {
await next()
}
const handler = async () => {}
middleware2.push(buildMiddlewareTuple(mHandler3))
middleware2.push(buildMiddlewareTuple(handler))
const composed = compose<Context>(middleware2, undefined, onNotFound)
const context = await composed(c)
expect(context.finalized).toBe(false)
})
})
describe('Compose', function () {
it('should get executed order one by one', async () => {
const arr: number[] = []
const stack = []
const called: boolean[] = []
stack.push(
buildMiddlewareTuple(async (_context: C, next: Function) => {
called.push(true)
arr.push(1)
await next()
arr.push(6)
})
)
stack.push(
buildMiddlewareTuple(async (_context: C, next: Function) => {
called.push(true)
arr.push(2)
await next()
arr.push(5)
})
)
stack.push(
buildMiddlewareTuple(async (_context: C, next: Function) => {
called.push(true)
arr.push(3)
await next()
arr.push(4)
})
)
await compose(stack)({ res: null, finalized: false })
expect(called).toEqual([true, true, true])
expect(arr).toEqual([1, 2, 3, 4, 5, 6])
})
it('should not get executed if previous next() not triggered', async () => {
const arr: number[] = []
const stack = []
const called: boolean[] = []
stack.push(
buildMiddlewareTuple(async (_context: C, next: Function) => {
called.push(true)
arr.push(1)
await next()
arr.push(6)
})
)
stack.push(
buildMiddlewareTuple(async () => {
called.push(true)
arr.push(2)
})
)
stack.push(
buildMiddlewareTuple(async (_context: C, next: Function) => {
called.push(true)
arr.push(3)
await next()
arr.push(4)
})
)
await compose(stack)({ res: null, finalized: false })
expect(called).toEqual([true, true])
expect(arr).toEqual([1, 2, 6])
})
it('should be able to be called twice', async () => {
type C = {
arr: number[]
}
const stack = []
stack.push(
buildMiddlewareTuple(async (context: C, next: Function) => {
context.arr.push(1)
await next()
context.arr.push(6)
})
)
stack.push(
buildMiddlewareTuple(async (context: C, next: Function) => {
context.arr.push(2)
await next()
context.arr.push(5)
})
)
stack.push(
buildMiddlewareTuple(async (context: C, next: Function) => {
context.arr.push(3)
await next()
context.arr.push(4)
})
)
const fn = compose(stack)
const ctx1 = { arr: [] as number[], res: null, finalized: false }
const ctx2 = { arr: [] as number[], res: null, finalized: false }
const out = [1, 2, 3, 4, 5, 6]
await fn(ctx1)
expect(out).toEqual(ctx1.arr)
await fn(ctx2)
expect(out).toEqual(ctx2.arr)
})
it('should create next functions that return a Promise', async () => {
const stack = []
const arr: unknown[] = []
for (let i = 0; i < 5; i++) {
stack.push(
buildMiddlewareTuple((_context: C, next: Function) => {
arr.push(next())
})
)
}
await compose(stack)({ res: null, finalized: false })
for (const next of arr) {
const isPromise = !!(next as { then?: Function })?.then
expect(isPromise).toBe(true)
}
})
it('should work with 0 middleware', async () => {
await compose([])({ res: null, finalized: false })
})
it('should work when yielding at the end of the stack', async () => {
const stack = []
let called = false
stack.push(
buildMiddlewareTuple(async (_ctx: C, next: Function) => {
await next()
called = true
})
)
await compose(stack)({ res: null, finalized: false })
expect(called).toBe(true)
})
it('should reject on errors in middleware', async () => {
const stack = []
stack.push(
buildMiddlewareTuple(() => {
throw new ExpectedError()
})
)
try {
await compose(stack)({ res: null, finalized: false })
throw new Error('promise was not rejected')
} catch (e) {
expect(e).toBeInstanceOf(ExpectedError)
}
})
it('should keep the context', async () => {
const ctx = { res: null, finalized: false }
const stack = []
stack.push(
buildMiddlewareTuple(async (ctx2: C, next: Function) => {
await next()
expect(ctx2).toEqual(ctx)
})
)
stack.push(
buildMiddlewareTuple(async (ctx2: C, next: Function) => {
await next()
expect(ctx2).toEqual(ctx)
})
)
stack.push(
buildMiddlewareTuple(async (ctx2: C, next: Function) => {
await next()
expect(ctx2).toEqual(ctx)
})
)
await compose(stack)(ctx)
})
it('should catch downstream errors', async () => {
const arr: number[] = []
const stack = []
stack.push(
buildMiddlewareTuple(async (_ctx: C, next: Function) => {
arr.push(1)
try {
arr.push(6)
await next()
arr.push(7)
} catch {
arr.push(2)
}
arr.push(3)
})
)
stack.push(
buildMiddlewareTuple(async () => {
arr.push(4)
throw new Error()
})
)
await compose(stack)({ res: null, finalized: false })
expect(arr).toEqual([1, 6, 4, 2, 3])
})
it('should compose w/ next', async () => {
let called = false
await compose([])({ res: null, finalized: false }, async () => {
called = true
})
expect(called).toBe(true)
})
it('should handle errors in wrapped non-async functions', async () => {
const stack = []
stack.push(
buildMiddlewareTuple(function () {
throw new ExpectedError()
})
)
try {
await compose(stack)({ res: null, finalized: false })
throw new Error('promise was not rejected')
} catch (e) {
expect(e).toBeInstanceOf(ExpectedError)
}
})
// https://github.com/koajs/compose/pull/27#issuecomment-143109739
it('should compose w/ other compositions', async () => {
const called: number[] = []
await compose([
buildMiddlewareTuple(
compose([
buildMiddlewareTuple((_ctx: C, next: Function) => {
called.push(1)
return next()
}),
buildMiddlewareTuple((_ctx: C, next: Function) => {
called.push(2)
return next()
}),
])
),
buildMiddlewareTuple((_ctx: C, next: Function) => {
called.push(3)
return next()
}),
])({ res: null, finalized: false })
expect(called).toEqual([1, 2, 3])
})
it('should throw if next() is called multiple times', async () => {
try {
await compose([
buildMiddlewareTuple(async (_ctx: C, next: Function) => {
await next()
await next()
}),
])({ res: null, finalized: false })
throw new Error('boom')
} catch (err) {
expect(err instanceof Error && /multiple times/.test(err.message)).toBe(true)
}
})
it('should return a valid middleware', async () => {
let val = 0
await compose([
buildMiddlewareTuple(
compose([
buildMiddlewareTuple((_ctx: C, next: Function) => {
val++
return next()
}),
buildMiddlewareTuple((_ctx: C, next: Function) => {
val++
return next()
}),
])
),
buildMiddlewareTuple((_ctx: C, next: Function) => {
val++
return next()
}),
])({ res: null, finalized: false })
expect(val).toEqual(3)
})
it('should return last return value', async () => {
type C = {
val: number
finalized: boolean
res: unknown
}
const stack = []
stack.push(
buildMiddlewareTuple(async (ctx: C, next: Function) => {
await next()
expect(ctx.val).toEqual(2)
ctx.val = 1
})
)
stack.push(
buildMiddlewareTuple(async (ctx: C, next: Function) => {
ctx.val = 2
await next()
expect(ctx.val).toEqual(2)
})
)
const res = await compose<C>(stack)({ val: 0, res: null, finalized: false })
expect(res.val).toEqual(1)
})
it('should not affect the original middleware array', () => {
const middleware: MiddlewareTuple[] = []
const fn1 = (_ctx: C, next: Function) => {
return next()
}
middleware.push(buildMiddlewareTuple(fn1))
for (const [[fn]] of middleware) {
expect(fn).toEqual(fn1)
}
compose(middleware)
for (const [[fn]] of middleware) {
expect(fn).toEqual(fn1)
}
})
it('should not get stuck on the passed in next', async () => {
type C = {
middleware: number
next: number
finalized: boolean
res: unknown
}
const middleware = [
buildMiddlewareTuple((ctx: C, next: Function) => {
ctx.middleware++
return next()
}),
]
const ctx = {
middleware: 0,
next: 0,
finalized: false,
res: null,
}
await compose<C>(middleware)(ctx, (ctx: C, next: Function) => {
ctx.next++
return next()
})
expect(ctx.middleware).toEqual(1)
expect(ctx.next).toEqual(1)
})
})