order.service.property.test.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. import * as fc from 'fast-check';
  2. /**
  3. * **Feature: backend-frontend-integration, Property 2: Order Creation Atomicity**
  4. * **Validates: Requirements 2.1, 9.2**
  5. *
  6. * *For any* order creation request with valid items, the system SHALL atomically
  7. * create the order record AND deduct stock from all products, such that either
  8. * both operations succeed or neither does.
  9. */
  10. // 模拟商品数据结构
  11. interface MockProduct {
  12. id: string;
  13. name: string;
  14. price: number;
  15. stock: number;
  16. status: 'published' | 'draft' | 'archived';
  17. sellerId: string;
  18. }
  19. // 模拟订单项
  20. interface MockOrderItem {
  21. productId: string;
  22. quantity: number;
  23. }
  24. // 模拟订单创建结果
  25. interface OrderCreationResult {
  26. success: boolean;
  27. order?: {
  28. orderNo: string;
  29. items: Array<{
  30. productId: string;
  31. quantity: number;
  32. subtotal: number;
  33. }>;
  34. totalAmount: number;
  35. };
  36. updatedProducts?: Map<string, number>; // productId -> newStock
  37. error?: string;
  38. }
  39. /**
  40. * 纯函数:模拟订单创建逻辑
  41. * 用于属性测试验证业务逻辑的正确性
  42. */
  43. function simulateOrderCreation(
  44. products: Map<string, MockProduct>,
  45. orderItems: MockOrderItem[]
  46. ): OrderCreationResult {
  47. // 1. 验证所有商品
  48. for (const item of orderItems) {
  49. const product = products.get(item.productId);
  50. if (!product) {
  51. return { success: false, error: `商品不存在: ${item.productId}` };
  52. }
  53. if (product.status !== 'published') {
  54. return { success: false, error: `商品已下架: ${product.name}` };
  55. }
  56. if (product.stock < item.quantity) {
  57. return { success: false, error: `库存不足: ${product.name}` };
  58. }
  59. }
  60. // 2. 计算订单金额并创建订单项
  61. const orderItemsResult: Array<{ productId: string; quantity: number; subtotal: number }> = [];
  62. let totalAmount = 0;
  63. for (const item of orderItems) {
  64. const product = products.get(item.productId)!;
  65. const subtotal = product.price * item.quantity;
  66. orderItemsResult.push({
  67. productId: item.productId,
  68. quantity: item.quantity,
  69. subtotal
  70. });
  71. totalAmount += subtotal;
  72. }
  73. // 3. 扣减库存
  74. const updatedProducts = new Map<string, number>();
  75. for (const item of orderItems) {
  76. const product = products.get(item.productId)!;
  77. updatedProducts.set(item.productId, product.stock - item.quantity);
  78. }
  79. // 4. 生成订单号
  80. const orderNo = `TEST${Date.now()}${Math.floor(Math.random() * 1000000)}`;
  81. return {
  82. success: true,
  83. order: {
  84. orderNo,
  85. items: orderItemsResult,
  86. totalAmount
  87. },
  88. updatedProducts
  89. };
  90. }
  91. describe('OrderService Property Tests', () => {
  92. // 生成有效商品的arbitrary
  93. const productArb = fc.record({
  94. id: fc.uuid(),
  95. name: fc.string({ minLength: 1, maxLength: 50 }),
  96. // 使用integer生成价格(以分为单位),然后转换为元
  97. price: fc.integer({ min: 1, max: 1000000 }).map(cents => cents / 100),
  98. stock: fc.integer({ min: 0, max: 1000 }),
  99. status: fc.constantFrom('published' as const, 'draft' as const, 'archived' as const),
  100. sellerId: fc.uuid()
  101. });
  102. describe('Property 2: Order Creation Atomicity', () => {
  103. it('订单创建成功时,库存扣减量等于订单商品数量', () => {
  104. fc.assert(
  105. fc.property(
  106. // 生成1-5个已发布且有库存的商品
  107. fc.array(
  108. productArb.filter(p => p.status === 'published' && p.stock > 0),
  109. { minLength: 1, maxLength: 5 }
  110. ),
  111. (products) => {
  112. // 创建商品映射
  113. const productMap = new Map<string, MockProduct>();
  114. products.forEach(p => productMap.set(p.id, p));
  115. // 为每个商品创建一个订单项,数量不超过库存
  116. const orderItems: MockOrderItem[] = products.map(p => ({
  117. productId: p.id,
  118. quantity: Math.min(p.stock, Math.floor(Math.random() * 5) + 1)
  119. }));
  120. const result = simulateOrderCreation(productMap, orderItems);
  121. if (result.success) {
  122. // 验证:每个商品的库存扣减量等于订单中的数量
  123. for (const item of orderItems) {
  124. const originalStock = productMap.get(item.productId)!.stock;
  125. const newStock = result.updatedProducts!.get(item.productId)!;
  126. if (originalStock - newStock !== item.quantity) {
  127. return false;
  128. }
  129. }
  130. return true;
  131. }
  132. // 如果失败,库存不应该变化(原子性)
  133. return result.updatedProducts === undefined;
  134. }
  135. ),
  136. { numRuns: 100 }
  137. );
  138. });
  139. it('订单创建失败时,库存不应该变化', () => {
  140. fc.assert(
  141. fc.property(
  142. // 生成包含下架商品或库存不足商品的场景
  143. fc.array(productArb, { minLength: 1, maxLength: 5 }),
  144. (products) => {
  145. const productMap = new Map<string, MockProduct>();
  146. products.forEach(p => productMap.set(p.id, p));
  147. // 创建可能导致失败的订单项
  148. const orderItems: MockOrderItem[] = products.map(p => ({
  149. productId: p.id,
  150. quantity: p.stock + 1 // 故意超过库存
  151. }));
  152. const result = simulateOrderCreation(productMap, orderItems);
  153. // 如果失败,不应该有库存更新
  154. if (!result.success) {
  155. return result.updatedProducts === undefined;
  156. }
  157. return true;
  158. }
  159. ),
  160. { numRuns: 100 }
  161. );
  162. });
  163. it('订单总金额等于所有商品小计之和', () => {
  164. fc.assert(
  165. fc.property(
  166. fc.array(
  167. productArb.filter(p => p.status === 'published' && p.stock > 0),
  168. { minLength: 1, maxLength: 5 }
  169. ),
  170. (products) => {
  171. const productMap = new Map<string, MockProduct>();
  172. products.forEach(p => productMap.set(p.id, p));
  173. const orderItems: MockOrderItem[] = products.map(p => ({
  174. productId: p.id,
  175. quantity: Math.min(p.stock, 1)
  176. }));
  177. const result = simulateOrderCreation(productMap, orderItems);
  178. if (result.success && result.order) {
  179. const calculatedTotal = result.order.items.reduce(
  180. (sum, item) => sum + item.subtotal,
  181. 0
  182. );
  183. // 使用近似比较处理浮点数精度问题
  184. return Math.abs(result.order.totalAmount - calculatedTotal) < 0.01;
  185. }
  186. return true;
  187. }
  188. ),
  189. { numRuns: 100 }
  190. );
  191. });
  192. it('商品小计等于单价乘以数量', () => {
  193. fc.assert(
  194. fc.property(
  195. fc.array(
  196. productArb.filter(p => p.status === 'published' && p.stock > 0),
  197. { minLength: 1, maxLength: 5 }
  198. ),
  199. (products) => {
  200. const productMap = new Map<string, MockProduct>();
  201. products.forEach(p => productMap.set(p.id, p));
  202. const orderItems: MockOrderItem[] = products.map(p => ({
  203. productId: p.id,
  204. quantity: Math.min(p.stock, 1)
  205. }));
  206. const result = simulateOrderCreation(productMap, orderItems);
  207. if (result.success && result.order) {
  208. for (const orderItem of result.order.items) {
  209. const product = productMap.get(orderItem.productId)!;
  210. const expectedSubtotal = product.price * orderItem.quantity;
  211. // 使用近似比较处理浮点数精度问题
  212. if (Math.abs(orderItem.subtotal - expectedSubtotal) >= 0.01) {
  213. return false;
  214. }
  215. }
  216. return true;
  217. }
  218. return true;
  219. }
  220. ),
  221. { numRuns: 100 }
  222. );
  223. });
  224. });
  225. });
  226. /**
  227. * **Feature: backend-frontend-integration, Property 1: Seller Data Isolation**
  228. * **Validates: Requirements 1.1, 3.2, 4.2, 10.3**
  229. *
  230. * *For any* seller querying products, orders, or refunds, the returned data
  231. * SHALL only contain records where sellerId matches the authenticated seller's ID.
  232. */
  233. // 模拟订单数据
  234. interface MockOrder {
  235. id: string;
  236. orderNo: string;
  237. userId: string;
  238. sellerId: string;
  239. totalAmount: number;
  240. status: string;
  241. }
  242. /**
  243. * 纯函数:模拟按卖家ID过滤订单
  244. */
  245. function filterOrdersBySeller(orders: MockOrder[], sellerId: string): MockOrder[] {
  246. return orders.filter(order => order.sellerId === sellerId);
  247. }
  248. /**
  249. * 纯函数:模拟按用户ID过滤订单
  250. */
  251. function filterOrdersByUser(orders: MockOrder[], userId: string): MockOrder[] {
  252. return orders.filter(order => order.userId === userId);
  253. }
  254. describe('Property 1: Seller Data Isolation', () => {
  255. // 生成订单的arbitrary
  256. const orderArb: fc.Arbitrary<MockOrder> = fc.record({
  257. id: fc.uuid(),
  258. orderNo: fc.nat().map(n => `ORDER${String(n).padStart(14, '0')}`),
  259. userId: fc.uuid(),
  260. sellerId: fc.uuid(),
  261. totalAmount: fc.integer({ min: 1, max: 100000 }).map(cents => cents / 100),
  262. status: fc.constantFrom('PendingPayment', 'PendingShipment', 'Shipped', 'Completed', 'Cancelled')
  263. });
  264. it('商家查询订单时只能看到自己店铺的订单', () => {
  265. fc.assert(
  266. fc.property(
  267. // 生成多个订单
  268. fc.array(orderArb, { minLength: 1, maxLength: 20 }),
  269. // 生成查询的卖家ID
  270. fc.uuid(),
  271. (orders, querySellerId) => {
  272. const filteredOrders = filterOrdersBySeller(orders, querySellerId);
  273. // 验证:所有返回的订单都属于该卖家
  274. return filteredOrders.every(order => order.sellerId === querySellerId);
  275. }
  276. ),
  277. { numRuns: 100 }
  278. );
  279. });
  280. it('用户查询订单时只能看到自己的订单', () => {
  281. fc.assert(
  282. fc.property(
  283. fc.array(orderArb, { minLength: 1, maxLength: 20 }),
  284. fc.uuid(),
  285. (orders, queryUserId) => {
  286. const filteredOrders = filterOrdersByUser(orders, queryUserId);
  287. // 验证:所有返回的订单都属于该用户
  288. return filteredOrders.every(order => order.userId === queryUserId);
  289. }
  290. ),
  291. { numRuns: 100 }
  292. );
  293. });
  294. it('不同卖家的订单数据完全隔离', () => {
  295. fc.assert(
  296. fc.property(
  297. fc.array(orderArb, { minLength: 2, maxLength: 20 }),
  298. fc.uuid(),
  299. fc.uuid(),
  300. (orders, sellerId1, sellerId2) => {
  301. // 确保两个卖家ID不同
  302. if (sellerId1 === sellerId2) return true;
  303. const seller1Orders = filterOrdersBySeller(orders, sellerId1);
  304. const seller2Orders = filterOrdersBySeller(orders, sellerId2);
  305. // 验证:两个卖家的订单集合没有交集
  306. const seller1OrderIds = new Set(seller1Orders.map(o => o.id));
  307. const hasOverlap = seller2Orders.some(o => seller1OrderIds.has(o.id));
  308. return !hasOverlap;
  309. }
  310. ),
  311. { numRuns: 100 }
  312. );
  313. });
  314. it('过滤后的订单数量不超过原始订单数量', () => {
  315. fc.assert(
  316. fc.property(
  317. fc.array(orderArb, { minLength: 0, maxLength: 20 }),
  318. fc.uuid(),
  319. (orders, sellerId) => {
  320. const filteredOrders = filterOrdersBySeller(orders, sellerId);
  321. return filteredOrders.length <= orders.length;
  322. }
  323. ),
  324. { numRuns: 100 }
  325. );
  326. });
  327. });
  328. /**
  329. * **Feature: backend-frontend-integration, Property 4: Stock Consistency**
  330. * **Validates: Requirements 2.1, 2.5, 4.5**
  331. *
  332. * *For any* sequence of order creations and cancellations, the final stock of each product
  333. * SHALL equal initial stock minus sum of ordered quantities plus sum of cancelled quantities.
  334. */
  335. // 模拟库存操作
  336. interface StockOperation {
  337. type: 'create' | 'cancel';
  338. productId: string;
  339. quantity: number;
  340. }
  341. /**
  342. * 纯函数:计算库存变化
  343. */
  344. function calculateFinalStock(
  345. initialStock: Map<string, number>,
  346. operations: StockOperation[]
  347. ): Map<string, number> {
  348. const finalStock = new Map(initialStock);
  349. for (const op of operations) {
  350. const currentStock = finalStock.get(op.productId) || 0;
  351. if (op.type === 'create') {
  352. // 创建订单:扣减库存
  353. finalStock.set(op.productId, currentStock - op.quantity);
  354. } else {
  355. // 取消订单:恢复库存
  356. finalStock.set(op.productId, currentStock + op.quantity);
  357. }
  358. }
  359. return finalStock;
  360. }
  361. /**
  362. * 纯函数:验证库存一致性公式
  363. * 最终库存 = 初始库存 - 订单数量 + 取消数量
  364. */
  365. function verifyStockConsistency(
  366. initialStock: Map<string, number>,
  367. operations: StockOperation[]
  368. ): boolean {
  369. const finalStock = calculateFinalStock(initialStock, operations);
  370. // 按商品ID分组计算
  371. const productIds = new Set([...initialStock.keys(), ...operations.map(op => op.productId)]);
  372. for (const productId of productIds) {
  373. const initial = initialStock.get(productId) || 0;
  374. // 计算该商品的订单总量和取消总量
  375. let orderedQuantity = 0;
  376. let cancelledQuantity = 0;
  377. for (const op of operations) {
  378. if (op.productId === productId) {
  379. if (op.type === 'create') {
  380. orderedQuantity += op.quantity;
  381. } else {
  382. cancelledQuantity += op.quantity;
  383. }
  384. }
  385. }
  386. const expectedFinal = initial - orderedQuantity + cancelledQuantity;
  387. const actualFinal = finalStock.get(productId) || 0;
  388. if (expectedFinal !== actualFinal) {
  389. return false;
  390. }
  391. }
  392. return true;
  393. }
  394. describe('Property 4: Stock Consistency', () => {
  395. it('库存变化遵循公式:最终库存 = 初始库存 - 订单数量 + 取消数量', () => {
  396. fc.assert(
  397. fc.property(
  398. // 生成1-5个商品ID
  399. fc.array(fc.uuid(), { minLength: 1, maxLength: 5 }),
  400. // 生成初始库存(100-1000)
  401. fc.integer({ min: 100, max: 1000 }),
  402. (productIds, baseStock) => {
  403. // 创建初始库存映射
  404. const initialStock = new Map<string, number>();
  405. productIds.forEach(id => initialStock.set(id, baseStock));
  406. // 生成随机操作序列
  407. const operations: StockOperation[] = [];
  408. const numOps = Math.floor(Math.random() * 10) + 1;
  409. for (let i = 0; i < numOps; i++) {
  410. operations.push({
  411. type: Math.random() > 0.5 ? 'create' : 'cancel',
  412. productId: productIds[Math.floor(Math.random() * productIds.length)],
  413. quantity: Math.floor(Math.random() * 10) + 1
  414. });
  415. }
  416. return verifyStockConsistency(initialStock, operations);
  417. }
  418. ),
  419. { numRuns: 100 }
  420. );
  421. });
  422. it('创建订单后取消应恢复原始库存', () => {
  423. fc.assert(
  424. fc.property(
  425. fc.uuid(),
  426. fc.integer({ min: 100, max: 1000 }),
  427. fc.integer({ min: 1, max: 50 }),
  428. (productId, initialStockValue, quantity) => {
  429. const initialStock = new Map<string, number>();
  430. initialStock.set(productId, initialStockValue);
  431. // 先创建订单,再取消
  432. const operations: StockOperation[] = [
  433. { type: 'create', productId, quantity },
  434. { type: 'cancel', productId, quantity }
  435. ];
  436. const finalStock = calculateFinalStock(initialStock, operations);
  437. // 最终库存应等于初始库存
  438. return finalStock.get(productId) === initialStockValue;
  439. }
  440. ),
  441. { numRuns: 100 }
  442. );
  443. });
  444. it('多次创建订单的库存扣减是累加的', () => {
  445. fc.assert(
  446. fc.property(
  447. fc.uuid(),
  448. fc.integer({ min: 100, max: 1000 }),
  449. fc.array(fc.integer({ min: 1, max: 10 }), { minLength: 1, maxLength: 5 }),
  450. (productId, initialStockValue, quantities) => {
  451. const initialStock = new Map<string, number>();
  452. initialStock.set(productId, initialStockValue);
  453. // 创建多个订单
  454. const operations: StockOperation[] = quantities.map(q => ({
  455. type: 'create' as const,
  456. productId,
  457. quantity: q
  458. }));
  459. const finalStock = calculateFinalStock(initialStock, operations);
  460. const totalOrdered = quantities.reduce((sum, q) => sum + q, 0);
  461. // 最终库存 = 初始库存 - 总订单量
  462. return finalStock.get(productId) === initialStockValue - totalOrdered;
  463. }
  464. ),
  465. { numRuns: 100 }
  466. );
  467. });
  468. it('空操作序列不改变库存', () => {
  469. fc.assert(
  470. fc.property(
  471. fc.array(fc.uuid(), { minLength: 1, maxLength: 5 }),
  472. fc.integer({ min: 100, max: 1000 }),
  473. (productIds, baseStock) => {
  474. const initialStock = new Map<string, number>();
  475. productIds.forEach(id => initialStock.set(id, baseStock));
  476. const finalStock = calculateFinalStock(initialStock, []);
  477. // 所有商品库存应保持不变
  478. for (const [id, stock] of initialStock) {
  479. if (finalStock.get(id) !== stock) {
  480. return false;
  481. }
  482. }
  483. return true;
  484. }
  485. ),
  486. { numRuns: 100 }
  487. );
  488. });
  489. });