85 lines
4.1 KiB
TypeScript
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);
|
|
});
|
|
});
|