Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,217 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { IdempotencyMismatchError } from "../errors/idempotency-error.js";
import { MemoryIdempotencyStore } from "../store/memory-store.js";
import { createIdempotencyMiddleware } from "./idempotency-middleware.js";
interface TestRequest {
headers: Record<string, string | undefined>;
body: unknown;
operation?: string;
}
interface TestResponse {
status: number;
data: unknown;
}
function makeMiddleware(store = new MemoryIdempotencyStore()) {
return createIdempotencyMiddleware<TestRequest, TestResponse>({
store,
extractKey: (req) => req.headers["idempotency-key"],
extractInput: (req) => req.body,
extractOperation: (req) => req.operation,
ttlMs: 60_000,
ttlOverrides: { "send-email": 3_600_000 },
});
}
function req(
key?: string,
body: unknown = {},
operation?: string
): TestRequest {
return { headers: { "idempotency-key": key }, body, operation };
}
describe("createIdempotencyMiddleware", () => {
it("executes handler when no cached result", {
timeout: 30000,
}, async () => {
const middleware = makeMiddleware();
const result = await middleware(req("k1"), async () => ({
status: 200,
data: "created",
}));
expect(result.response.data).toBe("created");
expect(result.cached).toBe(false);
expect(result.key).toBe("k1");
});
it("returns cached response on second call", {
timeout: 30000,
}, async () => {
const middleware = makeMiddleware();
let calls = 0;
const handler = async () => {
calls++;
return { status: 200, data: calls };
};
await middleware(req("dup"), handler);
const r2 = await middleware(req("dup"), handler);
expect(r2.cached).toBe(true);
expect(r2.response.data).toBe(1); // same as first call
expect(calls).toBe(1);
});
it("bypasses cache when no key extracted", { timeout: 30000 }, async () => {
const middleware = makeMiddleware();
let calls = 0;
const r1 = await middleware(req(undefined), async () => {
calls++;
return { status: 200, data: calls };
});
const r2 = await middleware(req(undefined), async () => {
calls++;
return { status: 200, data: calls };
});
expect(r1.cached).toBe(false);
expect(r2.cached).toBe(false);
expect(calls).toBe(2);
expect(r1.key).toBe("");
});
it("throws IdempotencyMismatchError on fingerprint conflict", {
timeout: 30000,
}, async () => {
const middleware = makeMiddleware();
await middleware(req("fp", { a: 1 }), async () => ({
status: 200,
data: 1,
}));
await expect(
middleware(req("fp", { a: 2 }), async () => ({
status: 200,
data: 2,
}))
).rejects.toBeInstanceOf(IdempotencyMismatchError);
});
it("stores failed status when handler throws", {
timeout: 30000,
}, async () => {
const store = new MemoryIdempotencyStore();
const middleware = makeMiddleware(store);
await expect(
middleware(req("err"), async () => {
throw new Error("boom");
})
).rejects.toThrow("boom");
const rec = await store.get("err");
expect(rec?.status).toBe("failed");
});
it("applies transformResponse on cache hit", {
timeout: 30000,
}, async () => {
const store = new MemoryIdempotencyStore();
const middleware = createIdempotencyMiddleware<
TestRequest,
TestResponse
>({
store,
extractKey: (r) => r.headers["idempotency-key"],
transformResponse: (res) => ({ ...res, status: 304 }),
});
await middleware(req("tr"), async () => ({ status: 200, data: "x" }));
const r2 = await middleware(req("tr"), async () => ({
status: 200,
data: "x",
}));
expect(r2.response.status).toBe(304);
});
it("applies transformResponse on fresh execution", {
timeout: 30000,
}, async () => {
const store = new MemoryIdempotencyStore();
const middleware = createIdempotencyMiddleware<
TestRequest,
TestResponse
>({
store,
extractKey: (r) => r.headers["idempotency-key"],
transformResponse: (res) => ({ ...res, data: "transformed" }),
});
const r = await middleware(req("tf2"), async () => ({
status: 200,
data: "original",
}));
expect(r.response.data).toBe("transformed");
});
it("uses ttlOverrides for known operations", {
timeout: 30000,
}, async () => {
const store = new MemoryIdempotencyStore();
const middleware = makeMiddleware(store);
// Just verify it doesn't throw with a known operation
const r = await middleware(req("op", {}, "send-email"), async () => ({
status: 200,
data: "sent",
}));
expect(r.response.data).toBe("sent");
});
it("falls back to default TTL for unknown operations", {
timeout: 30000,
}, async () => {
const store = new MemoryIdempotencyStore();
const middleware = makeMiddleware(store);
const r = await middleware(req("uop", {}, "unknown-op"), async () => ({
status: 200,
data: "ok",
}));
expect(r.cached).toBe(false);
});
it("stores failed status when handler throws a non-Error value", {
timeout: 30000,
}, async () => {
const store = new MemoryIdempotencyStore();
const middleware = makeMiddleware(store);
await expect(
middleware(req("non-err"), async () => {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw "string-error";
})
).rejects.toBe("string-error");
const rec = await store.get("non-err");
expect(rec?.status).toBe("failed");
expect(rec?.error).toBe("string-error");
});
it("uses defaultTtlMs when ttlOverrides entry is undefined", {
timeout: 30000,
}, async () => {
const store = new MemoryIdempotencyStore();
// Pass a ttlOverrides map where the matched key has an undefined value
// so the `?? defaultTtlMs` fallback on line 68 is exercised
const middleware = createIdempotencyMiddleware<
TestRequest,
TestResponse
>({
store,
extractKey: (r) => r.headers["idempotency-key"],
extractOperation: (r) => r.operation,
ttlMs: 60_000,
ttlOverrides: {
"undef-op": undefined as unknown as number,
},
});
const r = await middleware(
req("undef-ttl", {}, "undef-op"),
async () => ({ status: 200, data: "ok" })
);
expect(r.cached).toBe(false);
});
});