446 lines
12 KiB
TypeScript
446 lines
12 KiB
TypeScript
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<Record<string, never>>();
|
|
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();
|
|
});
|
|
});
|
|
});
|