/** * # 测试 商城数据层集成测试(真实 PostgreSQL) * 覆盖:加购物车 → 建订单 → 库存扣减 → 订单号唯一 + 购物车清理。 * 直接调用 src/server/commerce.ts 的核心函数(ensureCart / addProductToCart / placeOrderCore), * 不经过 Cookie/会话层。测试数据用唯一标记,结束时全部清理。 */ import { describe, test, expect, afterAll, beforeAll } from "bun:test"; import { SQL } from "bun"; import { drizzle } from "drizzle-orm/bun-sql"; import { eq, inArray } from "drizzle-orm"; import * as schema from "~/db/schema"; import { ensureCart, addProductToCart, placeOrderCore } from "~/server/commerce"; /** # 成员变量 测试专用数据库连接(结束后 end() 释放连接池) **/ import { initDb } from "../../src/db"; const sql = new SQL(process.env.DATABASE_URL || "postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot"); const db = drizzle(sql, { schema }); /** # 成员变量 测试数据标记(避免与真实数据冲突) **/ const TAG = `itest_${Date.now().toString(36)}`; beforeAll(async () => { await initDb(); }); /** # 成员变量 夹具序号(同一运行内多次建夹具时保证邮箱/slug 唯一) **/ let fixtureSeq = 0; /** # 成员变量 清理目标 ID 集合 **/ const cleanupIds = { userIds: [] as number[], productIds: [] as number[], cartIds: [] as number[], orderIds: [] as number[] }; afterAll(async () => { // 清理测试数据:订单明细 → 订单 → 购物车明细 → 购物车 → 产品 → 用户 if (cleanupIds.orderIds.length) { await db.delete(schema.orderItems).where(inArray(schema.orderItems.orderId, cleanupIds.orderIds)); await db.delete(schema.orders).where(inArray(schema.orders.id, cleanupIds.orderIds)); } if (cleanupIds.cartIds.length) { await db.delete(schema.cartItems).where(inArray(schema.cartItems.cartId, cleanupIds.cartIds)); await db.delete(schema.carts).where(inArray(schema.carts.id, cleanupIds.cartIds)); } if (cleanupIds.productIds.length) { await db.delete(schema.products).where(inArray(schema.products.id, cleanupIds.productIds)); } if (cleanupIds.userIds.length) { await db.delete(schema.users).where(inArray(schema.users.id, cleanupIds.userIds)); } await sql.end(); }); describe("commerce 数据层", () => { /** # 函数 测试夹具:建测试用户 + 两个测试产品(一个带库存一个无 stock 字段) */ async function setupFixture() { const tag = `${TAG}_${++fixtureSeq}`; const [user] = await db .insert(schema.users) .values({ email: `${tag}@example.com`, displayName: tag, passwordHash: "x", role: "customer" }) .returning(); cleanupIds.userIds.push(user!.id); const [p1] = await db .insert(schema.products) .values({ slug: `${tag}-p1`, title: `Test P1 ${tag}`, price: "19.90", stock: 10, stockStatus: "instock", status: "published" }) .returning(); const [p2] = await db .insert(schema.products) .values({ slug: `${tag}-p2`, title: `Test P2 ${tag}`, price: "5.00", stock: null, stockStatus: "onbackorder", status: "published" }) .returning(); const [p3] = await db .insert(schema.products) .values({ slug: `${tag}-p3`, title: `Test P3 ${tag}`, price: "1.00", stock: 3, stockStatus: "outofstock", status: "published" }) .returning(); cleanupIds.productIds.push(p1!.id, p2!.id, p3!.id); return { userId: user!.id, p1: p1!, p2: p2!, p3: p3! }; } test("加购物车 → 建订单 → 库存扣减 → 订单号唯一", async () => { const fx = await setupFixture(); // 1. 建购物车并加商品 const cart1 = await ensureCart({ userId: fx.userId }); cleanupIds.cartIds.push(cart1.cartId); const add1 = await addProductToCart(cart1.cartId, fx.p1.id, 2); expect(add1.ok).toBe(true); // 同产品重复加 → 数量累加 const add2 = await addProductToCart(cart1.cartId, fx.p1.id, 1); expect(add2.ok).toBe(true); expect((add2 as { count: number }).count).toBe(3); // 售罄产品不可加 const add3 = await addProductToCart(cart1.cartId, fx.p3.id, 1); expect(add3.ok).toBe(false); // 2. 下单(带账单信息) const orderNumber = await placeOrderCore(fx.userId, cart1.cartId, { billing: { firstName: "Test", lastName: "User", email: `${TAG}@example.com`, address: "1 Test St", city: "Shenzhen", country: "CN", postcode: "518000" }, shippingSame: true, note: "integration test", }); expect(orderNumber).toMatch(/^DS/); const [order] = await db.select().from(schema.orders).where(eq(schema.orders.orderNumber, orderNumber)); cleanupIds.orderIds.push(order!.id); expect(order!.userId).toBe(fx.userId); expect(order!.paymentMethod).toBe("bank_transfer"); expect(order!.paymentStatus).toBe("unpaid"); // 小计 = 19.90 * 3 = 59.70 expect(Number(order!.subtotal)).toBeCloseTo(59.7, 2); // 3. 明细快照 const items = await db.select().from(schema.orderItems).where(eq(schema.orderItems.orderId, order!.id)); expect(items.length).toBe(1); expect(items[0]!.qty).toBe(3); expect(Number(items[0]!.price)).toBeCloseTo(19.9, 2); // 4. 库存扣减(10 - 3 = 7) const [p1After] = await db.select().from(schema.products).where(eq(schema.products.id, fx.p1.id)); expect(p1After!.stock).toBe(7); // 5. 购物车已清空 const lines = await db.select().from(schema.cartItems).where(eq(schema.cartItems.cartId, cart1.cartId)); expect(lines.length).toBe(0); // 6. 订单号唯一:连续下两单(新购物车加 p2),订单号必须不同 const cart2 = await ensureCart({ userId: fx.userId }); cleanupIds.cartIds.push(cart2.cartId); await addProductToCart(cart2.cartId, fx.p2.id, 1); const orderNumber2 = await placeOrderCore(fx.userId, cart2.cartId, { billing: { firstName: "Test", lastName: "User", email: `${TAG}@example.com`, address: "1 Test St", city: "Shenzhen", country: "CN" }, shippingSame: true, }); expect(orderNumber2).not.toBe(orderNumber); const [order2] = await db.select().from(schema.orders).where(eq(schema.orders.orderNumber, orderNumber2)); cleanupIds.orderIds.push(order2!.id); // p2 无 stock 字段 → 扣库存逻辑跳过,stock 仍为 NULL const [p2After] = await db.select().from(schema.products).where(eq(schema.products.id, fx.p2.id)); expect(p2After!.stock).toBeNull(); }); test("ensureCart:匿名车过户 + 合并", async () => { const fx = await setupFixture(); // 匿名车加商品 const anonCart = await ensureCart({ sessionKey: `${TAG}-anon` }); cleanupIds.cartIds.push(anonCart.cartId); await addProductToCart(anonCart.cartId, fx.p1.id, 1); // 用户无购物车 → 匿名车过户给用户(同一条记录) const merged = await ensureCart({ userId: fx.userId, sessionKey: `${TAG}-anon` }); const [transferred] = await db.select().from(schema.carts).where(eq(schema.carts.id, anonCart.cartId)); expect(merged.cartId).toBe(anonCart.cartId); expect(transferred!.userId).toBe(fx.userId); expect(transferred!.sessionKey).toBeNull(); // 用户已有购物车 + 第二个匿名车 → 合并后匿名车删除 const anon2 = await ensureCart({ sessionKey: `${TAG}-anon2` }); cleanupIds.cartIds.push(anon2.cartId); await addProductToCart(anon2.cartId, fx.p1.id, 2); const merged2 = await ensureCart({ userId: fx.userId, sessionKey: `${TAG}-anon2` }); expect(merged2.cartId).toBe(anonCart.cartId); const [gone] = await db.select().from(schema.carts).where(eq(schema.carts.id, anon2.cartId)); expect(gone).toBeUndefined(); const lines = await db.select().from(schema.cartItems).where(eq(schema.cartItems.cartId, anonCart.cartId)); expect(lines.length).toBe(1); expect(lines[0]!.qty).toBe(3); // 1 + 2 累加 }); test("售罄商品下单被事务拒绝", async () => { const fx = await setupFixture(); const cart = await ensureCart({ userId: fx.userId }); cleanupIds.cartIds.push(cart.cartId); // 直接插入售罄商品行绕过加车校验,验证下单事务兜底 await db.insert(schema.cartItems).values({ cartId: cart.cartId, productId: fx.p3.id, qty: 1 }); await expect( placeOrderCore(fx.userId, cart.cartId, { billing: { firstName: "T", lastName: "U", email: `${TAG}@example.com`, address: "a", city: "b", country: "c" }, shippingSame: true, }), ).rejects.toThrow("已售罄"); }); });