1import { Test } from "@nestjs/testing";
2import { INestApplication, ValidationPipe } from "@nestjs/common";
3import request from "supertest";
4import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
5import { AppModule } from "../src/app.module";
6import { PrismaService } from "../src/prisma/prisma.service";
7import { resetDb, seedProduct, seedUser, tokenFor } from "./helpers";
8
9describe("POST /orders (real HTTP, real PostgreSQL)", () => {
10 let app: INestApplication;
11 let prisma: PrismaService;
12
13 const post = (user: { id: string; role: string } | null, body: unknown) => {
14 const req = request(app.getHttpServer()).post("/orders");
15 return (user ? req.set("Authorization", "Bearer " + tokenFor(user)) : req).send(body);
16 };
17 const stockOf = async (id: string) => (await prisma.product.findUniqueOrThrow({ where: { id } })).stock;
18
19 beforeAll(async () => {
20 const mod = await Test.createTestingModule({ imports: [AppModule] }).compile();
21 app = mod.createNestApplication();
22 app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
23 await app.init();
24 prisma = app.get(PrismaService);
25 });
26 afterAll(() => app.close());
27 beforeEach(() => resetDb(prisma));
28
29 it("creates the order, ignores a fake total and reduces stock", async () => {
30 const user = await seedUser(prisma);
31 const lamp = await seedProduct(prisma, { priceCents: 1250, stock: 5 });
32
33 const res = await post(user, { items: [{ productId: lamp.id, quantity: 2 }], total: 1 }).expect(201);
34
35 // 1. the API contract
36 expect(res.body).toMatchObject({
37 status: "CONFIRMED",
38 totalCents: 2500,
39 items: [{ productId: lamp.id, quantity: 2, unitPriceCents: 1250 }],
40 });
41 // 2. the database truth
42 const saved = await prisma.order.findUniqueOrThrow({ where: { id: res.body.id } });
43 expect(saved).toMatchObject({ userId: user.id, totalCents: 2500 });
44 expect(await stockOf(lamp.id)).toBe(3);
45 });
46
47 it("returns 401 without a token and writes nothing", async () => {
48 const lamp = await seedProduct(prisma, { stock: 5 });
49 await post(null, { items: [{ productId: lamp.id, quantity: 1 }] }).expect(401);
50 expect(await prisma.order.count()).toBe(0);
51 expect(await stockOf(lamp.id)).toBe(5);
52 });
53
54 it("returns 404 for an unknown product and writes nothing", async () => {
55 const user = await seedUser(prisma);
56 await post(user, { items: [{ productId: "does-not-exist", quantity: 1 }] }).expect(404);
57 expect(await prisma.order.count()).toBe(0);
58 });
59
60 it.each([
61 ["an inactive product", { active: false }, 1, 422],
62 ["quantity 0", {}, 0, 400],
63 ["quantity above stock", { stock: 2 }, 3, 409],
64 ])("rejects %s and leaves the database untouched", async (_name, overrides, quantity, status) => {
65 const user = await seedUser(prisma);
66 const lamp = await seedProduct(prisma, { stock: 5, ...overrides });
67
68 await post(user, { items: [{ productId: lamp.id, quantity }] }).expect(status);
69
70 expect(await prisma.order.count()).toBe(0);
71 expect(await stockOf(lamp.id)).toBe(lamp.stock); // unchanged
72 });
73
74 it("allows exactly the remaining stock", async () => {
75 const user = await seedUser(prisma);
76 const lamp = await seedProduct(prisma, { stock: 2 });
77 await post(user, { items: [{ productId: lamp.id, quantity: 2 }] }).expect(201);
78 expect(await stockOf(lamp.id)).toBe(0);
79 });
80
81 it("rolls back everything when the second line fails", async () => {
82 const user = await seedUser(prisma);
83 const lamp = await seedProduct(prisma, { stock: 5 });
84 const scarce = await seedProduct(prisma, { stock: 1 });
85
86 await post(user, { items: [
87 { productId: lamp.id, quantity: 2 },
88 { productId: scarce.id, quantity: 9 },
89 ] }).expect(409);
90
91 expect(await stockOf(lamp.id)).toBe(5); // the first line was rolled back
92 expect(await prisma.order.count()).toBe(0);
93 });
94
95 it("sells the last unit exactly once under concurrent requests", async () => {
96 const [a, b] = [await seedUser(prisma), await seedUser(prisma)];
97 const lamp = await seedProduct(prisma, { stock: 1 });
98 const body = { items: [{ productId: lamp.id, quantity: 1 }] };
99
100 const results = await Promise.all([post(a, body), post(b, body)]);
101
102 expect(results.map((r) => r.status).sort()).toEqual([201, 409]);
103 expect(await stockOf(lamp.id)).toBe(0);
104 });
105});