test(orders): add tests for cross-order, filter escape, unique-constraint
Cross-order rejection (Fix #3), PB filter quote escape (Fix #6), and mapping of PB unique-index write errors to OrderNumberCollisionError (Fix #7) get full coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
53b778691e
commit
deb224ac10
1 changed files with 127 additions and 0 deletions
|
|
@ -188,6 +188,88 @@ describe("updateOrderReviewFields", () => {
|
|||
expect(result.order_number).toBe("ORD-2026-07-001");
|
||||
expect(result.customer).toBe("cust-1");
|
||||
});
|
||||
|
||||
it("escapes embedded quotes in order_number before building the collision filter", async () => {
|
||||
// An order_number containing `"` would break the filter
|
||||
// expression unless we escape embedded quotes before
|
||||
// interpolating. Spy on getList to capture the filter
|
||||
// the service actually sends.
|
||||
const order = await pbMock.collection("orders").create({
|
||||
order_number: "ORD-2026-07-001",
|
||||
customer: "cust-1",
|
||||
});
|
||||
|
||||
const originalCollection = pbMock.collection;
|
||||
const captured: string[] = [];
|
||||
pbMock.collection = (name: string) => {
|
||||
const col = originalCollection(name);
|
||||
if (name === "orders") {
|
||||
const originalGetList = col.getList;
|
||||
col.getList = vi.fn(async (...args: unknown[]) => {
|
||||
const opts = (args[2] as { filter?: string } | undefined) || {};
|
||||
if (opts.filter) captured.push(opts.filter);
|
||||
return originalGetList(...(args as Parameters<typeof col.getList>));
|
||||
});
|
||||
}
|
||||
return col;
|
||||
};
|
||||
|
||||
try {
|
||||
// Pick an order_number that contains a literal quote. The
|
||||
// unescaped filter would be `order_number = "ORD "foo" && id != "..."`,
|
||||
// which would either fail to parse or match the wrong rows.
|
||||
await updateOrderReviewFields(order.id, {
|
||||
order_number: 'ORD "weird"',
|
||||
});
|
||||
|
||||
expect(captured.length).toBeGreaterThan(0);
|
||||
// The filter MUST escape the `"` inside the literal — look for
|
||||
// the backslash-escaped quote in the captured string.
|
||||
const filter = captured[captured.length - 1];
|
||||
expect(filter).toContain('ORD \\"weird\\"');
|
||||
} finally {
|
||||
pbMock.collection = originalCollection;
|
||||
}
|
||||
});
|
||||
|
||||
it("maps a PB unique-constraint write error back to OrderNumberCollisionError", async () => {
|
||||
// Two callers race past the pre-write check; the loser's update
|
||||
// then throws on the unique index. The service should catch that
|
||||
// and surface a structured collision error (with the colliding
|
||||
// row's id) rather than letting it bubble up as a 500.
|
||||
const order = await pbMock.collection("orders").create({
|
||||
order_number: "ORD-2026-07-001",
|
||||
customer: "cust-1",
|
||||
});
|
||||
await pbMock.collection("orders").create({
|
||||
order_number: "ORD-2026-07-002",
|
||||
customer: "cust-2",
|
||||
});
|
||||
|
||||
const originalCollection = pbMock.collection;
|
||||
pbMock.collection = (name: string) => {
|
||||
const col = originalCollection(name);
|
||||
if (name === "orders") {
|
||||
col.update = vi.fn(async () => {
|
||||
const err = new Error(
|
||||
"UNIQUE constraint failed: orders.order_number",
|
||||
);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return col;
|
||||
};
|
||||
|
||||
try {
|
||||
await expect(
|
||||
updateOrderReviewFields(order.id, {
|
||||
order_number: "ORD-2026-07-002",
|
||||
}),
|
||||
).rejects.toBeInstanceOf(OrderNumberCollisionError);
|
||||
} finally {
|
||||
pbMock.collection = originalCollection;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateOrderItemsPrices", () => {
|
||||
|
|
@ -394,4 +476,49 @@ describe("updateOrderItemsPrices", () => {
|
|||
// markup_percent must mirror markup — the two fields stay in lock-step.
|
||||
expect(item.markup_percent).toBe(140);
|
||||
});
|
||||
|
||||
it("rejects items that belong to a different order", async () => {
|
||||
// Two orders, each with its own item.
|
||||
const orderA = await pbMock.collection("orders").create({
|
||||
order_number: "ORD-2026-07-001",
|
||||
});
|
||||
const orderB = await pbMock.collection("orders").create({
|
||||
order_number: "ORD-2026-07-002",
|
||||
});
|
||||
await pbMock.collection("order_items").create({
|
||||
id: "item-a",
|
||||
order: orderA.id,
|
||||
description: "A's drill",
|
||||
quantity: 2,
|
||||
selling_price: 50,
|
||||
cost_price: 25,
|
||||
markup: 100,
|
||||
});
|
||||
await pbMock.collection("order_items").create({
|
||||
id: "item-b",
|
||||
order: orderB.id,
|
||||
description: "B's bit",
|
||||
quantity: 1,
|
||||
selling_price: 10,
|
||||
cost_price: 5,
|
||||
markup: 100,
|
||||
});
|
||||
|
||||
// Caller targets order A but slips in order B's item — the cross-
|
||||
// order check must catch it and not modify item-b.
|
||||
const result = await updateOrderItemsPrices(orderA.id, [
|
||||
{ id: "item-a", quantity: 7 },
|
||||
{ id: "item-b", quantity: 99 },
|
||||
]);
|
||||
|
||||
expect(result.updated).toBe(1);
|
||||
expect(result.failed).toEqual([
|
||||
{ id: "item-b", error: "Item does not belong to this order" },
|
||||
]);
|
||||
|
||||
const itemA = await pbMock.collection("order_items").getOne("item-a");
|
||||
expect(itemA.quantity).toBe(7);
|
||||
const itemB = await pbMock.collection("order_items").getOne("item-b");
|
||||
expect(itemB.quantity).toBe(1); // unchanged
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue