Files
0pt/tests/lib/orders-idempotency.test.ts
T

85 lines
4.1 KiB
TypeScript

/**
* P014: lib/orders-store.ts previously had ZERO test coverage and no
* idempotency mechanism -- a retried POST /api/orders created a second,
* indistinguishable row every time (recon: "Duplicate delivery idempotent:
* NOT MET"). These tests exercise the in-memory fallback path directly
* (no DATABASE_URL is set in this test environment, matching every other
* test in this repo) and prove: a retry under the same Idempotency-Key
* replays the original order; a reused key with a different submission is
* refused as a conflict, not silently merged; and omitting the key
* preserves the exact pre-P014 "always create a new order" behavior.
*/
import { describe, it, expect, beforeEach } from "vitest";
import {
createOrder,
findOrderByIdempotencyKey,
IdempotencyKeyConflictError,
type OrderConfig,
} from "@/lib/orders-store";
const CONFIG_A: OrderConfig = { markets: ["SE"], type: "Bolag", fieldIds: ["identity"], volume: 1000 };
const CONFIG_B: OrderConfig = { markets: ["NO"], type: "Bolag", fieldIds: ["identity"], volume: 5000 };
describe("createOrder idempotency (in-memory fallback)", () => {
it("no Idempotency-Key: every call creates a distinct order (unchanged pre-P014 behavior)", async () => {
const first = await createOrder("custom", CONFIG_A);
const second = await createOrder("custom", CONFIG_A);
expect(first.order.order_id).not.toBe(second.order.order_id);
expect(first.replayed).toBe(false);
expect(second.replayed).toBe(false);
});
it("same Idempotency-Key + same (flow, config): the second call replays the first order, no duplicate row", async () => {
const key = `idem_${crypto.randomUUID()}`;
const first = await createOrder("custom", CONFIG_A, key);
const second = await createOrder("custom", CONFIG_A, key);
expect(first.replayed).toBe(false);
expect(second.replayed).toBe(true);
expect(second.order.order_id).toBe(first.order.order_id);
expect(second.order.created_at).toBe(first.order.created_at);
});
it("retrying the SAME key three times still yields exactly one underlying order", async () => {
const key = `idem_${crypto.randomUUID()}`;
const a = await createOrder("custom", CONFIG_A, key);
const b = await createOrder("custom", CONFIG_A, key);
const c = await createOrder("custom", CONFIG_A, key);
const ids = new Set([a.order.order_id, b.order.order_id, c.order.order_id]);
expect(ids.size).toBe(1);
});
it("same Idempotency-Key reused with a DIFFERENT config: refused as a conflict, not silently merged", async () => {
const key = `idem_${crypto.randomUUID()}`;
await createOrder("custom", CONFIG_A, key);
await expect(createOrder("custom", CONFIG_B, key)).rejects.toBeInstanceOf(IdempotencyKeyConflictError);
});
it("same Idempotency-Key reused with a DIFFERENT flow: refused as a conflict", async () => {
const key = `idem_${crypto.randomUUID()}`;
await createOrder("custom", CONFIG_A, key);
await expect(createOrder("subscription", CONFIG_A, key)).rejects.toBeInstanceOf(IdempotencyKeyConflictError);
});
it("a conflicting retry does not mutate or replace the original order", async () => {
const key = `idem_${crypto.randomUUID()}`;
const original = await createOrder("custom", CONFIG_A, key);
await expect(createOrder("custom", CONFIG_B, key)).rejects.toThrow();
const stillThere = await findOrderByIdempotencyKey(key);
expect(stillThere?.order_id).toBe(original.order.order_id);
expect(stillThere?.config).toEqual(CONFIG_A);
});
it("different Idempotency-Keys for the same config create independent orders", async () => {
const a = await createOrder("custom", CONFIG_A, `idem_${crypto.randomUUID()}`);
const b = await createOrder("custom", CONFIG_A, `idem_${crypto.randomUUID()}`);
expect(a.order.order_id).not.toBe(b.order.order_id);
});
it("an empty-string Idempotency-Key is treated as absent (no dedup, no crash)", async () => {
const a = await createOrder("custom", CONFIG_A, "");
const b = await createOrder("custom", CONFIG_A, " ");
expect(a.order.order_id).not.toBe(b.order.order_id);
});
});