| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571 |
- 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<string, number>; // productId -> newStock
- error?: string;
- }
- /**
- * 纯函数:模拟订单创建逻辑
- * 用于属性测试验证业务逻辑的正确性
- */
- function simulateOrderCreation(
- products: Map<string, MockProduct>,
- 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<string, number>();
- 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<string, MockProduct>();
- 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<string, MockProduct>();
- 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<string, MockProduct>();
- 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<string, MockProduct>();
- 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<MockOrder> = 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<string, number>,
- operations: StockOperation[]
- ): Map<string, number> {
- 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<string, number>,
- 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<string, number>();
- 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<string, number>();
- 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<string, number>();
- 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<string, number>();
- 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 }
- );
- });
- });
|