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(); return undefined as unknown as { counter: number }; }); 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 { skip: ctx.skip, 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; return undefined as unknown as { value: number }; }); const result = await chain.execute({ value: 0 }); expect(result).toBeUndefined(); }); }); });