onion-compose
Version:
Onion style function composition just like koa-compose, but with any number of arguments
372 lines (294 loc) • 7.96 kB
text/typescript
/* eslint-env jest */
/* NOTE: These are original tests from from koa-compose
updated to use Jest, await syntax and TypeScirpt
*/
import compose, { type NextFn } from '../index'
function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms || 1))
}
function isPromise(x: any) {
return x && typeof x.then === 'function'
}
describe('Onion compose', function () {
it('should work', async () => {
const arr: number[] = []
const stack: NextFn<[ctx: Record<string, any>]>[] = []
void stack.push(async (_ctx, next) => {
arr.push(1)
await wait(1)
await next()
await wait(1)
arr.push(6)
})
stack.push(async (_ctx, next) => {
arr.push(2)
await wait(1)
await next()
await wait(1)
arr.push(5)
})
stack.push(async (_ctx, next) => {
arr.push(3)
await wait(1)
await next()
await wait(1)
arr.push(4)
})
await compose(stack)([{}])
expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
})
it('should work with synchronous', async () => {
const arr: number[] = []
const stack: NextFn<[ctx: Record<string, any>]>[] = []
void stack.push((_ctx, next) => {
arr.push(1)
next()
arr.push(6)
})
stack.push((_ctx, next) => {
arr.push(2)
next()
arr.push(5)
})
stack.push((_ctx, next) => {
arr.push(3)
next()
arr.push(4)
})
await compose(stack)([{}])
expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
})
it('should be able to be called twice', () => {
const stack: NextFn<[ctx: { arr: number[] }]>[] = []
stack.push(async (context, next) => {
context.arr.push(1)
await wait(1)
await next()
await wait(1)
context.arr.push(6)
})
stack.push(async (context, next) => {
context.arr.push(2)
await wait(1)
await next()
await wait(1)
context.arr.push(5)
})
stack.push(async (context, next) => {
context.arr.push(3)
await wait(1)
await next()
await wait(1)
context.arr.push(4)
})
const fn = compose(stack)
const ctx1 = { arr: [] }
const ctx2 = { arr: [] }
const out = [1, 2, 3, 4, 5, 6]
return fn([ctx1])
.then(() => {
expect(out).toEqual(ctx1.arr)
return fn([ctx2])
})
.then(() => {
expect(out).toEqual(ctx2.arr)
})
})
it('should only accept an array', () => {
// @ts-expect-error - error on purpose
expect(() => compose()).toThrow(TypeError)
})
it('should create next functions that return a Promise', function () {
const stack: NextFn<[Record<string, any>]>[] = []
const arr: unknown[] = []
for (let i = 0; i < 5; i++) {
stack.push((_context, next) => {
arr.push(next())
})
}
compose(stack)([{}])
for (const next of arr) {
expect(isPromise(next)).toBe(true)
}
})
it('should work with 0 middleware', function () {
expect(() => compose([])([{}])).not.toThrow()
})
it('should only accept middleware as functions', () => {
// @ts-expect-error type mismatch
expect(() => compose([{}])).toThrow(TypeError)
})
it('should work when yielding at the end of the stack', async () => {
const stack: NextFn<[ctx: any]>[] = []
let called = false
stack.push(async (_ctx, next) => {
await next()
called = true
})
await compose(stack)([{}])
expect(called).toBe(true)
})
it('should reject on errors in middleware', async () => {
const stack: NextFn<[Record<string, any>]>[] = []
stack.push(() => {
throw new Error()
})
await expect(compose(stack)([{}])).rejects.toBeInstanceOf(Error)
})
it('should keep the context', () => {
const ctx = {}
const stack: NextFn<[Record<string, any>]>[] = []
stack.push(async (ctx2, next) => {
await next()
expect(ctx2).toEqual(ctx)
})
})
it('should keep all the arguments', async () => {
const arg1 = {}
const arg2 = {}
const arg3 = {}
const stack: NextFn<
[
p1: Record<string, any>,
p2: Record<string, any>,
p3: Record<string, any>
]
>[] = []
stack.push(async (p1, p2, p3, next) => {
await next()
expect(p1).toEqual(arg1)
expect(p2).toEqual(arg2)
expect(p3).toEqual(arg3)
return [p1, p2, p3]
})
stack.push(async (p1, p2, p3, next) => {
await next()
expect(p1).toEqual(arg1)
expect(p2).toEqual(arg2)
expect(p3).toEqual(arg3)
})
stack.push(async (p1, p2, p3, next) => {
await next()
expect(p1).toEqual(arg1)
expect(p2).toEqual(arg2)
expect(p3).toEqual(arg3)
})
const [p1Result, p2Result, p3Result] = await compose(stack)([
arg1,
arg2,
arg3
])
expect(p1Result).toBe(arg1)
expect(p2Result).toBe(arg2)
expect(p3Result).toBe(arg3)
})
it('should catch downstream errors', async () => {
const arr: number[] = []
const stack: NextFn<[ctx: any]>[] = []
stack.push(async (_ctx, next) => {
arr.push(1)
try {
arr.push(6)
await next()
arr.push(7)
} catch (err) {
arr.push(2)
}
arr.push(3)
})
stack.push(async (_ctx) => {
arr.push(4)
throw new Error()
})
await compose(stack)([{}])
expect(arr).toEqual([1, 6, 4, 2, 3])
})
it('should compose w/ next', async () => {
let called = false
const next = async () => {
called = true
}
const run = compose([])
await run([1, 3], next)
expect(called).toBe(true)
})
it('should handle errors in wrapped non-async functions', async () => {
const stack: NextFn<[ctx: Record<string, any>]>[] = []
stack.push(function () {
throw new Error()
})
await expect(compose(stack)([{}])).rejects.toBeInstanceOf(Error)
})
it('should throw if next() is called multiple times', async () => {
const fn = async (_ctx: any, next: () => Promise<void> | void) => {
await next()
await next()
}
expect.assertions(1)
try {
await compose<[ctx: any]>([fn])([{}])
} catch (e) {
/* eslint-disable jest/no-conditional-expect */
expect((e as Error).message).toMatch(/multiple times/)
}
})
it('should return last return value', async () => {
const stack: NextFn<
[ctx: number],
() => Promise<number>,
Promise<number>
>[] = []
stack.push(async (_ctx, next) => {
const val = await next()
expect(val).toEqual(2)
return 1
})
stack.push(async (_ctx, next) => {
const val = await next()
expect(val).toEqual(0)
return 2
})
const next: NextFn<
[ctx: any],
() => Promise<number>,
Promise<number>
> = async (_ctx: any) => 0
const result = await compose<[ctx: number], Promise<number>>(stack)(
[2],
next
)
expect(result).toEqual(1)
})
it('should not affect the original middleware array', () => {
const middleware: NextFn<[ctx: string]>[] = []
const fn1: NextFn<[ctx: string]> = (_ctx, next) => {
return next()
}
middleware.push(fn1)
for (const fn of middleware) {
expect(fn).toBe(fn1)
}
compose(middleware)
for (const fn of middleware) {
expect(fn).toBe(fn1)
}
})
it('should not get stuck on the passed in next', async () => {
const ctx = {
middleware: 0,
next: 0
}
const middleware = (ctx: { middleware: number }, next: () => unknown) => {
ctx.middleware++
return next()
}
await compose<[ctx: { middleware: number; next: number }]>([middleware])(
[ctx],
(ctx, next) => {
ctx.next++
return next()
}
)
expect(ctx).toEqual({ middleware: 1, next: 1 })
})
})