manicanldes-backend/test/checkout-concurrency.spec.ts

119 lines
3.6 KiB
TypeScript

import { randomUUID } from 'node:crypto';
import { identityApp, type IdentityApp } from './helpers/identity-app';
import { checkoutFixture, couponInput } from './helpers/checkout';
import { secondActor, addressInput } from './helpers/commerce';
const native = process.env.TEST_DATABASE_URL ? describe : describe.skip;
native('native PostgreSQL checkout concurrency', () => {
let ctx: IdentityApp;
beforeAll(async () => {
ctx = await identityApp();
}, 60000);
afterAll(async () => {
await ctx.close();
});
beforeEach(async () => {
await ctx.clearLimits();
});
it('creates one order for two simultaneous identical requests', async () => {
const f = await checkoutFixture(ctx);
const responses = await Promise.all(
[1, 2].map(() =>
ctx
.api()
.post('/api/v1/checkout')
.auth(f.actor.token, { type: 'bearer' })
.send(f.input),
),
);
expect(responses.map((response) => response.status)).toEqual([201, 201]);
expect(responses[0]!.body.id).toBe(responses[1]!.body.id);
expect(
await ctx.db.stockReservation.count({
where: { userId: f.actor.userId },
}),
).toBe(1);
});
it('lets only one competing checkout claim the final stock and coupon use', async () => {
const f = await checkoutFixture(ctx, 2);
const other = await secondActor(ctx);
const address = await ctx
.api()
.post('/api/v1/addresses')
.auth(other.token, { type: 'bearer' })
.send(addressInput)
.expect(201);
await ctx
.api()
.put('/api/v1/cart/lines/' + f.variant.id)
.auth(other.token, { type: 'bearer' })
.send({ quantity: 2, version: 0 })
.expect(200);
const coupon = await ctx
.api()
.post('/api/v1/coupons')
.auth(ctx.token, { type: 'bearer' })
.send({ ...couponInput(), maxUses: 1 })
.expect(201);
const commands = [
{ actor: f.actor, input: { ...f.input, couponCode: coupon.body.code } },
{
actor: other,
input: {
...f.input,
addressId: address.body.id,
idempotencyKey: randomUUID(),
couponCode: coupon.body.code,
},
},
];
const responses = await Promise.all(
commands.map(({ actor, input }) =>
ctx
.api()
.post('/api/v1/checkout')
.auth(actor.token, { type: 'bearer' })
.send(input),
),
);
expect(responses.map((response) => response.status).sort()).toEqual([
201, 409,
]);
expect(
await ctx.db.order.count({ where: { couponId: coupon.body.id } }),
).toBe(1);
const reserved = await ctx.db.stockReservation.aggregate({
where: { stockItemId: f.stock.id },
_sum: { quantity: true },
});
expect(reserved._sum.quantity).toBe(2);
});
it('coordinates checkout with a standalone inventory reservation', async () => {
const f = await checkoutFixture(ctx, 2);
const responses = await Promise.all([
ctx
.api()
.post('/api/v1/checkout')
.auth(f.actor.token, { type: 'bearer' })
.send(f.input),
ctx
.api()
.post('/api/v1/inventory/reservations')
.auth(ctx.token, { type: 'bearer' })
.send({
stockItemId: f.stock.id,
quantity: 2,
idempotencyKey: randomUUID(),
}),
]);
expect(responses.map((response) => response.status).sort()).toEqual([
201, 409,
]);
const held = await ctx.db.stockReservation.aggregate({
where: { stockItemId: f.stock.id },
_sum: { quantity: true },
});
expect(held._sum.quantity).toBe(2);
});
});