import * as fc from 'fast-check'; /** * **Feature: backend-frontend-integration, Property 2: Order Creation Atomicity** * **Validates: Requirements 2.1, 9.2** * * *For any* order creation request with valid items, the system SHALL atomically * create the order record AND deduct stock from all products, such that either * both operations succeed or neither does. */ // 模拟商品数据结构 interface MockProduct { id: string; name: string; price: number; stock: number; status: 'published' | 'draft' | 'archived'; sellerId: string; } // 模拟订单项 interface MockOrderItem { productId: string; quantity: number; } // 模拟订单创建结果 interface OrderCreationResult { success: boolean; order?: { orderNo: string; items: Array<{ productId: string; quantity: number; subtotal: number; }>; totalAmount: number; }; updatedProducts?: Map; // productId -> newStock error?: string; } /** * 纯函数:模拟订单创建逻辑 * 用于属性测试验证业务逻辑的正确性 */ function simulateOrderCreation( products: Map, orderItems: MockOrderItem[] ): OrderCreationResult { // 1. 验证所有商品 for (const item of orderItems) { const product = products.get(item.productId); if (!product) { return { success: false, error: `商品不存在: ${item.productId}` }; } if (product.status !== 'published') { return { success: false, error: `商品已下架: ${product.name}` }; } if (product.stock < item.quantity) { return { success: false, error: `库存不足: ${product.name}` }; } } // 2. 计算订单金额并创建订单项 const orderItemsResult: Array<{ productId: string; quantity: number; subtotal: number }> = []; let totalAmount = 0; for (const item of orderItems) { const product = products.get(item.productId)!; const subtotal = product.price * item.quantity; orderItemsResult.push({ productId: item.productId, quantity: item.quantity, subtotal }); totalAmount += subtotal; } // 3. 扣减库存 const updatedProducts = new Map(); for (const item of orderItems) { const product = products.get(item.productId)!; updatedProducts.set(item.productId, product.stock - item.quantity); } // 4. 生成订单号 const orderNo = `TEST${Date.now()}${Math.floor(Math.random() * 1000000)}`; return { success: true, order: { orderNo, items: orderItemsResult, totalAmount }, updatedProducts }; } describe('OrderService Property Tests', () => { // 生成有效商品的arbitrary const productArb = fc.record({ id: fc.uuid(), name: fc.string({ minLength: 1, maxLength: 50 }), // 使用integer生成价格(以分为单位),然后转换为元 price: fc.integer({ min: 1, max: 1000000 }).map(cents => cents / 100), stock: fc.integer({ min: 0, max: 1000 }), status: fc.constantFrom('published' as const, 'draft' as const, 'archived' as const), sellerId: fc.uuid() }); describe('Property 2: Order Creation Atomicity', () => { it('订单创建成功时,库存扣减量等于订单商品数量', () => { fc.assert( fc.property( // 生成1-5个已发布且有库存的商品 fc.array( productArb.filter(p => p.status === 'published' && p.stock > 0), { minLength: 1, maxLength: 5 } ), (products) => { // 创建商品映射 const productMap = new Map(); products.forEach(p => productMap.set(p.id, p)); // 为每个商品创建一个订单项,数量不超过库存 const orderItems: MockOrderItem[] = products.map(p => ({ productId: p.id, quantity: Math.min(p.stock, Math.floor(Math.random() * 5) + 1) })); const result = simulateOrderCreation(productMap, orderItems); if (result.success) { // 验证:每个商品的库存扣减量等于订单中的数量 for (const item of orderItems) { const originalStock = productMap.get(item.productId)!.stock; const newStock = result.updatedProducts!.get(item.productId)!; if (originalStock - newStock !== item.quantity) { return false; } } return true; } // 如果失败,库存不应该变化(原子性) return result.updatedProducts === undefined; } ), { numRuns: 100 } ); }); it('订单创建失败时,库存不应该变化', () => { fc.assert( fc.property( // 生成包含下架商品或库存不足商品的场景 fc.array(productArb, { minLength: 1, maxLength: 5 }), (products) => { const productMap = new Map(); products.forEach(p => productMap.set(p.id, p)); // 创建可能导致失败的订单项 const orderItems: MockOrderItem[] = products.map(p => ({ productId: p.id, quantity: p.stock + 1 // 故意超过库存 })); const result = simulateOrderCreation(productMap, orderItems); // 如果失败,不应该有库存更新 if (!result.success) { return result.updatedProducts === undefined; } return true; } ), { numRuns: 100 } ); }); it('订单总金额等于所有商品小计之和', () => { fc.assert( fc.property( fc.array( productArb.filter(p => p.status === 'published' && p.stock > 0), { minLength: 1, maxLength: 5 } ), (products) => { const productMap = new Map(); products.forEach(p => productMap.set(p.id, p)); const orderItems: MockOrderItem[] = products.map(p => ({ productId: p.id, quantity: Math.min(p.stock, 1) })); const result = simulateOrderCreation(productMap, orderItems); if (result.success && result.order) { const calculatedTotal = result.order.items.reduce( (sum, item) => sum + item.subtotal, 0 ); // 使用近似比较处理浮点数精度问题 return Math.abs(result.order.totalAmount - calculatedTotal) < 0.01; } return true; } ), { numRuns: 100 } ); }); it('商品小计等于单价乘以数量', () => { fc.assert( fc.property( fc.array( productArb.filter(p => p.status === 'published' && p.stock > 0), { minLength: 1, maxLength: 5 } ), (products) => { const productMap = new Map(); products.forEach(p => productMap.set(p.id, p)); const orderItems: MockOrderItem[] = products.map(p => ({ productId: p.id, quantity: Math.min(p.stock, 1) })); const result = simulateOrderCreation(productMap, orderItems); if (result.success && result.order) { for (const orderItem of result.order.items) { const product = productMap.get(orderItem.productId)!; const expectedSubtotal = product.price * orderItem.quantity; // 使用近似比较处理浮点数精度问题 if (Math.abs(orderItem.subtotal - expectedSubtotal) >= 0.01) { return false; } } return true; } return true; } ), { numRuns: 100 } ); }); }); }); /** * **Feature: backend-frontend-integration, Property 1: Seller Data Isolation** * **Validates: Requirements 1.1, 3.2, 4.2, 10.3** * * *For any* seller querying products, orders, or refunds, the returned data * SHALL only contain records where sellerId matches the authenticated seller's ID. */ // 模拟订单数据 interface MockOrder { id: string; orderNo: string; userId: string; sellerId: string; totalAmount: number; status: string; } /** * 纯函数:模拟按卖家ID过滤订单 */ function filterOrdersBySeller(orders: MockOrder[], sellerId: string): MockOrder[] { return orders.filter(order => order.sellerId === sellerId); } /** * 纯函数:模拟按用户ID过滤订单 */ function filterOrdersByUser(orders: MockOrder[], userId: string): MockOrder[] { return orders.filter(order => order.userId === userId); } describe('Property 1: Seller Data Isolation', () => { // 生成订单的arbitrary const orderArb: fc.Arbitrary = fc.record({ id: fc.uuid(), orderNo: fc.nat().map(n => `ORDER${String(n).padStart(14, '0')}`), userId: fc.uuid(), sellerId: fc.uuid(), totalAmount: fc.integer({ min: 1, max: 100000 }).map(cents => cents / 100), status: fc.constantFrom('PendingPayment', 'PendingShipment', 'Shipped', 'Completed', 'Cancelled') }); it('商家查询订单时只能看到自己店铺的订单', () => { fc.assert( fc.property( // 生成多个订单 fc.array(orderArb, { minLength: 1, maxLength: 20 }), // 生成查询的卖家ID fc.uuid(), (orders, querySellerId) => { const filteredOrders = filterOrdersBySeller(orders, querySellerId); // 验证:所有返回的订单都属于该卖家 return filteredOrders.every(order => order.sellerId === querySellerId); } ), { numRuns: 100 } ); }); it('用户查询订单时只能看到自己的订单', () => { fc.assert( fc.property( fc.array(orderArb, { minLength: 1, maxLength: 20 }), fc.uuid(), (orders, queryUserId) => { const filteredOrders = filterOrdersByUser(orders, queryUserId); // 验证:所有返回的订单都属于该用户 return filteredOrders.every(order => order.userId === queryUserId); } ), { numRuns: 100 } ); }); it('不同卖家的订单数据完全隔离', () => { fc.assert( fc.property( fc.array(orderArb, { minLength: 2, maxLength: 20 }), fc.uuid(), fc.uuid(), (orders, sellerId1, sellerId2) => { // 确保两个卖家ID不同 if (sellerId1 === sellerId2) return true; const seller1Orders = filterOrdersBySeller(orders, sellerId1); const seller2Orders = filterOrdersBySeller(orders, sellerId2); // 验证:两个卖家的订单集合没有交集 const seller1OrderIds = new Set(seller1Orders.map(o => o.id)); const hasOverlap = seller2Orders.some(o => seller1OrderIds.has(o.id)); return !hasOverlap; } ), { numRuns: 100 } ); }); it('过滤后的订单数量不超过原始订单数量', () => { fc.assert( fc.property( fc.array(orderArb, { minLength: 0, maxLength: 20 }), fc.uuid(), (orders, sellerId) => { const filteredOrders = filterOrdersBySeller(orders, sellerId); return filteredOrders.length <= orders.length; } ), { numRuns: 100 } ); }); }); /** * **Feature: backend-frontend-integration, Property 4: Stock Consistency** * **Validates: Requirements 2.1, 2.5, 4.5** * * *For any* sequence of order creations and cancellations, the final stock of each product * SHALL equal initial stock minus sum of ordered quantities plus sum of cancelled quantities. */ // 模拟库存操作 interface StockOperation { type: 'create' | 'cancel'; productId: string; quantity: number; } /** * 纯函数:计算库存变化 */ function calculateFinalStock( initialStock: Map, operations: StockOperation[] ): Map { const finalStock = new Map(initialStock); for (const op of operations) { const currentStock = finalStock.get(op.productId) || 0; if (op.type === 'create') { // 创建订单:扣减库存 finalStock.set(op.productId, currentStock - op.quantity); } else { // 取消订单:恢复库存 finalStock.set(op.productId, currentStock + op.quantity); } } return finalStock; } /** * 纯函数:验证库存一致性公式 * 最终库存 = 初始库存 - 订单数量 + 取消数量 */ function verifyStockConsistency( initialStock: Map, operations: StockOperation[] ): boolean { const finalStock = calculateFinalStock(initialStock, operations); // 按商品ID分组计算 const productIds = new Set([...initialStock.keys(), ...operations.map(op => op.productId)]); for (const productId of productIds) { const initial = initialStock.get(productId) || 0; // 计算该商品的订单总量和取消总量 let orderedQuantity = 0; let cancelledQuantity = 0; for (const op of operations) { if (op.productId === productId) { if (op.type === 'create') { orderedQuantity += op.quantity; } else { cancelledQuantity += op.quantity; } } } const expectedFinal = initial - orderedQuantity + cancelledQuantity; const actualFinal = finalStock.get(productId) || 0; if (expectedFinal !== actualFinal) { return false; } } return true; } describe('Property 4: Stock Consistency', () => { it('库存变化遵循公式:最终库存 = 初始库存 - 订单数量 + 取消数量', () => { fc.assert( fc.property( // 生成1-5个商品ID fc.array(fc.uuid(), { minLength: 1, maxLength: 5 }), // 生成初始库存(100-1000) fc.integer({ min: 100, max: 1000 }), (productIds, baseStock) => { // 创建初始库存映射 const initialStock = new Map(); productIds.forEach(id => initialStock.set(id, baseStock)); // 生成随机操作序列 const operations: StockOperation[] = []; const numOps = Math.floor(Math.random() * 10) + 1; for (let i = 0; i < numOps; i++) { operations.push({ type: Math.random() > 0.5 ? 'create' : 'cancel', productId: productIds[Math.floor(Math.random() * productIds.length)], quantity: Math.floor(Math.random() * 10) + 1 }); } return verifyStockConsistency(initialStock, operations); } ), { numRuns: 100 } ); }); it('创建订单后取消应恢复原始库存', () => { fc.assert( fc.property( fc.uuid(), fc.integer({ min: 100, max: 1000 }), fc.integer({ min: 1, max: 50 }), (productId, initialStockValue, quantity) => { const initialStock = new Map(); initialStock.set(productId, initialStockValue); // 先创建订单,再取消 const operations: StockOperation[] = [ { type: 'create', productId, quantity }, { type: 'cancel', productId, quantity } ]; const finalStock = calculateFinalStock(initialStock, operations); // 最终库存应等于初始库存 return finalStock.get(productId) === initialStockValue; } ), { numRuns: 100 } ); }); it('多次创建订单的库存扣减是累加的', () => { fc.assert( fc.property( fc.uuid(), fc.integer({ min: 100, max: 1000 }), fc.array(fc.integer({ min: 1, max: 10 }), { minLength: 1, maxLength: 5 }), (productId, initialStockValue, quantities) => { const initialStock = new Map(); initialStock.set(productId, initialStockValue); // 创建多个订单 const operations: StockOperation[] = quantities.map(q => ({ type: 'create' as const, productId, quantity: q })); const finalStock = calculateFinalStock(initialStock, operations); const totalOrdered = quantities.reduce((sum, q) => sum + q, 0); // 最终库存 = 初始库存 - 总订单量 return finalStock.get(productId) === initialStockValue - totalOrdered; } ), { numRuns: 100 } ); }); it('空操作序列不改变库存', () => { fc.assert( fc.property( fc.array(fc.uuid(), { minLength: 1, maxLength: 5 }), fc.integer({ min: 100, max: 1000 }), (productIds, baseStock) => { const initialStock = new Map(); productIds.forEach(id => initialStock.set(id, baseStock)); const finalStock = calculateFinalStock(initialStock, []); // 所有商品库存应保持不变 for (const [id, stock] of initialStock) { if (finalStock.get(id) !== stock) { return false; } } return true; } ), { numRuns: 100 } ); }); });