Files
dusuniot/tests/integration/commerce.test.ts
T

173 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* # 测试 商城数据层集成测试(真实 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("已售罄");
});
});