import { describe, it, expect } from 'vitest'; import { createMiddlewareChain, type MiddlewareChain } from '@/utils/middleware'; describe('createMiddlewareChain', () => { describe('basic execution', () => { it('should return context when no middlewares and no fallback', async () => { const chain = createMiddlewareChain<{ value: number }>(); const result = await chain.execute({ value: 42 }); expect(result).toEqual({ value: 42 }); }); it('should call fallback when no middlewares', async () => { const chain = createMiddlewareChain<{ value: number }, string>( async ctx => `value is ${ctx.value}` ); const result = await chain.execute({ value: 42 }); expect(result).toBe('value is 42'); }); it('should pass context to fallback', async () => { const chain = createMiddlewareChain<{ a: number; b: number }, number>( async ctx => ctx.a + ctx.b ); const result = await chain.execute({ a: 3, b: 7 }); expect(result).toBe(10); }); }); describe('single middleware', () => { it('should execute a single middleware', async () => { const chain = createMiddlewareChain<{ count: number }>(); chain.use(async (ctx, next) => { ctx.count *= 2; return next(); }); const result = await chain.execute({ count: 5 }); expect(result.count).toBe(10); }); it('should allow middleware to modify return value', async () => { const chain = createMiddlewareChain<{ value: number }, number>( async ctx => ctx.value ); chain.use(async (ctx, next) => { const result = await next(); return result * 2; }); const result = await chain.execute({ value: 21 }); expect(result).toBe(42); }); it('should allow middleware to short-circuit without calling next', async () => { const chain = createMiddlewareChain<{ value: number }>(); chain.use(async (_ctx, _next) => { return { value: 999 }; }); const result = await chain.execute({ value: 1 }); expect(result.value).toBe(999); }); }); describe('multiple middlewares', () => { it('should execute middlewares in order', async () => { const order: number[] = []; const chain = createMiddlewareChain<{ value: number }>(); chain.use(async (_ctx, next) => { order.push(1); const result = await next(); order.push(4); return result; }); chain.use(async (_ctx, next) => { order.push(2); const result = await next(); order.push(3); return result; }); await chain.execute({ value: 0 }); expect(order).toEqual([1, 2, 3, 4]); }); it('should accumulate modifications through the chain', async () => { const chain = createMiddlewareChain<{ value: number }>(); chain.use(async (ctx, next) => { ctx.value += 1; return next(); }); chain.use(async (ctx, next) => { ctx.value *= 2; return next(); }); chain.use(async (ctx, next) => { ctx.value += 3; return next(); }); const result = await chain.execute({ value: 0 }); expect(result.value).toBe(5); }); it('should allow middleware to modify result on the way back', async () => { const chain = createMiddlewareChain<{ base: number }, number>( async ctx => ctx.base ); chain.use(async (_ctx, next) => { const result = await next(); return result + 10; }); chain.use(async (_ctx, next) => { const result = await next(); return result * 2; }); const result = await chain.execute({ base: 5 }); expect(result).toBe(20); }); it('should allow middleware to short-circuit in the middle', async () => { const executed: number[] = []; const chain = createMiddlewareChain<{ value: number }>(); chain.use(async (_ctx, next) => { executed.push(1); return next(); }); chain.use(async () => { executed.push(2); return { value: -1 }; }); chain.use(async (_ctx, next) => { executed.push(3); return next(); }); const result = await chain.execute({ value: 100 }); expect(result.value).toBe(-1); expect(executed).toEqual([1, 2]); }); }); describe('nested next calls', () => { it('should advance index on each next call, skipping remaining middlewares', async () => { const chain = createMiddlewareChain<{ counter: number }>(); chain.use(async (_ctx, next) => { await next(); await next(); }); const result = await chain.execute({ counter: 0 }); expect(result).toBeUndefined(); }); it('should allow middleware to call next conditionally', async () => { const chain = createMiddlewareChain<{ skip: boolean; value: number }>(); chain.use(async (ctx, next) => { if (ctx.skip) { return { value: -1 }; } return next(); }); chain.use(async (ctx, next) => { ctx.value += 10; return next(); }); const resultA = await chain.execute({ skip: true, value: 0 }); const resultB = await chain.execute({ skip: false, value: 0 }); expect(resultA.value).toBe(-1); expect(resultB.value).toBe(10); }); it('should handle middleware that awaits next multiple times with a fallback', async () => { const log: string[] = []; const chain = createMiddlewareChain<{ value: number }, string[]>( async _ctx => log ); chain.use(async (_ctx, next) => { log.push('before'); await next(); log.push('after-first'); await next(); log.push('after-second'); return log; }); chain.use(async (_ctx, next) => { log.push('mw2'); return next(); }); const result = await chain.execute({ value: 0 }); expect(result).toEqual(['before', 'mw2', 'after-first', 'after-second']); }); it('should return fallback result on second next call when no more middlewares remain', async () => { const chain = createMiddlewareChain<{ value: number }, number>( async ctx => ctx.value * 10 ); chain.use(async (_ctx, next) => { await next(); return await next(); }); chain.use(async (_ctx, next) => { return next(); }); const result = await chain.execute({ value: 5 }); expect(result).toBe(50); }); it('should return fallback result on second next call when no more middlewares remain', async () => { const chain = createMiddlewareChain<{ value: number }, number>( async ctx => ctx.value * 10 ); chain.use(async (_ctx, next) => { await next(); return await next(); }); chain.use(async (_ctx, next) => { return next(); }); const result = await chain.execute({ value: 5 }); expect(result).toBe(50); }); }); describe('async behavior', () => { it('should handle async middlewares', async () => { const chain = createMiddlewareChain<{ value: number }>(); chain.use(async (ctx, next) => { await new Promise(resolve => setTimeout(resolve, 10)); ctx.value += 1; return next(); }); chain.use(async (ctx, next) => { await new Promise(resolve => setTimeout(resolve, 10)); ctx.value += 2; return next(); }); const result = await chain.execute({ value: 0 }); expect(result.value).toBe(3); }); it('should handle async fallback', async () => { const chain = createMiddlewareChain<{ value: number }, number>( async ctx => { await new Promise(resolve => setTimeout(resolve, 10)); return ctx.value * 10; } ); const result = await chain.execute({ value: 5 }); expect(result).toBe(50); }); }); describe('error handling', () => { it('should propagate errors from middleware', async () => { const chain = createMiddlewareChain<{ value: number }>(); chain.use(async () => { throw new Error('middleware error'); }); await expect(chain.execute({ value: 1 })).rejects.toThrow('middleware error'); }); it('should propagate errors from fallback', async () => { const chain = createMiddlewareChain<{ value: number }, number>( async () => { throw new Error('fallback error'); } ); await expect(chain.execute({ value: 1 })).rejects.toThrow('fallback error'); }); it('should allow middleware to catch errors from downstream', async () => { const chain = createMiddlewareChain<{ value: number }>(); chain.use(async (_ctx, next) => { try { return await next(); } catch { return { value: -1 }; } }); chain.use(async () => { throw new Error('downstream error'); }); const result = await chain.execute({ value: 1 }); expect(result.value).toBe(-1); }); }); describe('return type override', () => { it('should support different TReturn type than TContext', async () => { const chain = createMiddlewareChain<{ name: string }, string>( async ctx => `Hello, ${ctx.name}!` ); const result = await chain.execute({ name: 'World' }); expect(result).toBe('Hello, World!'); }); it('should allow middleware to transform return type', async () => { const chain = createMiddlewareChain<{ items: number[] }, number>( async ctx => ctx.items.reduce((a, b) => a + b, 0) ); chain.use(async (_ctx, next) => { const sum = await next(); return sum * 2; }); const result = await chain.execute({ items: [1, 2, 3] }); expect(result).toBe(12); }); }); describe('reusability', () => { it('should reset index on each execute call', async () => { const chain = createMiddlewareChain<{ count: number }>(); chain.use(async (ctx, next) => { ctx.count += 1; return next(); }); const resultA = await chain.execute({ count: 0 }); const resultB = await chain.execute({ count: 0 }); expect(resultA.count).toBe(1); expect(resultB.count).toBe(1); }); it('should share middlewares across execute calls', async () => { const chain = createMiddlewareChain<{ log: string[] }>(); chain.use(async (ctx, next) => { ctx.log.push('always'); return next(); }); await chain.execute({ log: [] }); await chain.execute({ log: [] }); expect(chain).toBeDefined(); }); }); describe('edge cases', () => { it('should handle empty context object', async () => { const chain = createMiddlewareChain>(); const result = await chain.execute({}); expect(result).toEqual({}); }); it('should handle middleware that returns a completely different object', async () => { const chain = createMiddlewareChain<{ x: number }, { y: string }>( async () => ({ y: 'default' }) ); chain.use(async (_ctx, next) => { return next(); }); const result = await chain.execute({ x: 42 }); expect(result).toEqual({ y: 'default' }); }); it('should handle middleware that mutates context without returning', async () => { const chain = createMiddlewareChain<{ value: number }>( async ctx => ctx ); chain.use(async (ctx, next) => { ctx.value = 100; return next(); }); const result = await chain.execute({ value: 0 }); expect(result.value).toBe(100); }); it('should return undefined when middleware does not call next or return', async () => { const chain = createMiddlewareChain<{ value: number }>(); chain.use(async (ctx) => { ctx.value = 100; }); const result = await chain.execute({ value: 0 }); expect(result).toBeUndefined(); }); }); });