sync.service.property.test.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. import * as fc from 'fast-check';
  2. /**
  3. * **Feature: backend-frontend-integration, Property 9: MongoDB-Elasticsearch Sync**
  4. * **Validates: Requirements 9.1**
  5. *
  6. * *For any* product update operation, the data in Elasticsearch SHALL eventually
  7. * match the data in MongoDB within a bounded time.
  8. */
  9. // 模拟商品数据结构
  10. interface MockProduct {
  11. id: string;
  12. name: string;
  13. description: string;
  14. price: number;
  15. stock: number;
  16. status: 'published' | 'draft' | 'archived';
  17. sellerId: string;
  18. category: string;
  19. brand?: string;
  20. tags?: string[];
  21. }
  22. // 同步操作类型
  23. type SyncOperation = 'index' | 'update' | 'delete';
  24. // 同步结果
  25. interface SyncResult {
  26. success: boolean;
  27. productId: string;
  28. operation: SyncOperation;
  29. retries: number;
  30. error?: string;
  31. }
  32. // 模拟ES存储
  33. interface MockElasticsearchStore {
  34. products: Map<string, MockProduct>;
  35. }
  36. // 模拟MongoDB存储
  37. interface MockMongoDBStore {
  38. products: Map<string, MockProduct>;
  39. }
  40. /**
  41. * 纯函数:模拟同步操作
  42. * 将MongoDB中的商品数据同步到Elasticsearch
  43. */
  44. function simulateSync(
  45. _mongoStore: MockMongoDBStore,
  46. esStore: MockElasticsearchStore,
  47. operation: SyncOperation,
  48. productId: string,
  49. productData?: MockProduct,
  50. shouldFail: boolean = false
  51. ): SyncResult {
  52. if (shouldFail) {
  53. return {
  54. success: false,
  55. productId,
  56. operation,
  57. retries: 3,
  58. error: 'Simulated sync failure'
  59. };
  60. }
  61. switch (operation) {
  62. case 'index':
  63. if (productData) {
  64. esStore.products.set(productId, { ...productData });
  65. }
  66. break;
  67. case 'update':
  68. if (productData && esStore.products.has(productId)) {
  69. esStore.products.set(productId, { ...productData });
  70. } else if (productData) {
  71. // 如果ES中不存在,则创建
  72. esStore.products.set(productId, { ...productData });
  73. }
  74. break;
  75. case 'delete':
  76. esStore.products.delete(productId);
  77. break;
  78. }
  79. return {
  80. success: true,
  81. productId,
  82. operation,
  83. retries: 0
  84. };
  85. }
  86. /**
  87. * 纯函数:验证MongoDB和ES数据一致性
  88. */
  89. function verifyDataConsistency(
  90. mongoStore: MockMongoDBStore,
  91. esStore: MockElasticsearchStore
  92. ): { consistent: boolean; differences: string[] } {
  93. const differences: string[] = [];
  94. // 检查MongoDB中的每个商品是否在ES中存在且数据一致
  95. for (const [productId, mongoProduct] of mongoStore.products) {
  96. const esProduct = esStore.products.get(productId);
  97. if (!esProduct) {
  98. differences.push(`Product ${productId} exists in MongoDB but not in ES`);
  99. continue;
  100. }
  101. // 比较关键字段
  102. if (mongoProduct.name !== esProduct.name) {
  103. differences.push(`Product ${productId}: name mismatch (MongoDB: ${mongoProduct.name}, ES: ${esProduct.name})`);
  104. }
  105. if (mongoProduct.price !== esProduct.price) {
  106. differences.push(`Product ${productId}: price mismatch (MongoDB: ${mongoProduct.price}, ES: ${esProduct.price})`);
  107. }
  108. if (mongoProduct.stock !== esProduct.stock) {
  109. differences.push(`Product ${productId}: stock mismatch (MongoDB: ${mongoProduct.stock}, ES: ${esProduct.stock})`);
  110. }
  111. if (mongoProduct.status !== esProduct.status) {
  112. differences.push(`Product ${productId}: status mismatch (MongoDB: ${mongoProduct.status}, ES: ${esProduct.status})`);
  113. }
  114. }
  115. // 检查ES中是否有MongoDB中不存在的商品(已删除但未同步)
  116. for (const [productId] of esStore.products) {
  117. if (!mongoStore.products.has(productId)) {
  118. differences.push(`Product ${productId} exists in ES but not in MongoDB (should be deleted)`);
  119. }
  120. }
  121. return {
  122. consistent: differences.length === 0,
  123. differences
  124. };
  125. }
  126. /**
  127. * 纯函数:模拟带重试的同步
  128. */
  129. function simulateSyncWithRetry(
  130. mongoStore: MockMongoDBStore,
  131. esStore: MockElasticsearchStore,
  132. operation: SyncOperation,
  133. productId: string,
  134. productData?: MockProduct,
  135. maxRetries: number = 3,
  136. failureRate: number = 0
  137. ): SyncResult {
  138. let retries = 0;
  139. for (let attempt = 0; attempt <= maxRetries; attempt++) {
  140. // 模拟随机失败
  141. const shouldFail = Math.random() < failureRate && attempt < maxRetries;
  142. if (!shouldFail) {
  143. const result = simulateSync(mongoStore, esStore, operation, productId, productData, false);
  144. result.retries = retries;
  145. return result;
  146. }
  147. retries++;
  148. }
  149. return {
  150. success: false,
  151. productId,
  152. operation,
  153. retries: maxRetries,
  154. error: 'Max retries exceeded'
  155. };
  156. }
  157. describe('SearchSyncService Property Tests', () => {
  158. // 生成有效商品的arbitrary
  159. const productArb: fc.Arbitrary<MockProduct> = fc.record({
  160. id: fc.uuid(),
  161. name: fc.string({ minLength: 1, maxLength: 100 }),
  162. description: fc.string({ minLength: 0, maxLength: 500 }),
  163. price: fc.integer({ min: 1, max: 1000000 }).map(cents => cents / 100),
  164. stock: fc.integer({ min: 0, max: 10000 }),
  165. status: fc.constantFrom('published' as const, 'draft' as const, 'archived' as const),
  166. sellerId: fc.uuid(),
  167. category: fc.string({ minLength: 1, maxLength: 50 }),
  168. brand: fc.option(fc.string({ minLength: 1, maxLength: 50 }), { nil: undefined }),
  169. tags: fc.option(fc.array(fc.string({ minLength: 1, maxLength: 20 }), { minLength: 0, maxLength: 5 }), { nil: undefined })
  170. });
  171. describe('Property 9: MongoDB-Elasticsearch Sync', () => {
  172. it('创建商品后,ES中应包含相同的数据', () => {
  173. fc.assert(
  174. fc.property(
  175. productArb,
  176. (product) => {
  177. const mongoStore: MockMongoDBStore = { products: new Map() };
  178. const esStore: MockElasticsearchStore = { products: new Map() };
  179. // 在MongoDB中创建商品
  180. mongoStore.products.set(product.id, product);
  181. // 同步到ES
  182. const syncResult = simulateSync(mongoStore, esStore, 'index', product.id, product);
  183. if (!syncResult.success) {
  184. return true; // 同步失败时跳过验证
  185. }
  186. // 验证数据一致性
  187. const consistency = verifyDataConsistency(mongoStore, esStore);
  188. return consistency.consistent;
  189. }
  190. ),
  191. { numRuns: 100 }
  192. );
  193. });
  194. it('更新商品后,ES中的数据应与MongoDB一致', () => {
  195. fc.assert(
  196. fc.property(
  197. productArb,
  198. fc.record({
  199. name: fc.option(fc.string({ minLength: 1, maxLength: 100 }), { nil: undefined }),
  200. price: fc.option(fc.integer({ min: 1, max: 1000000 }).map(cents => cents / 100), { nil: undefined }),
  201. stock: fc.option(fc.integer({ min: 0, max: 10000 }), { nil: undefined }),
  202. status: fc.option(fc.constantFrom('published' as const, 'draft' as const, 'archived' as const), { nil: undefined })
  203. }),
  204. (product, updates) => {
  205. const mongoStore: MockMongoDBStore = { products: new Map() };
  206. const esStore: MockElasticsearchStore = { products: new Map() };
  207. // 初始创建
  208. mongoStore.products.set(product.id, product);
  209. esStore.products.set(product.id, { ...product });
  210. // 应用更新到MongoDB
  211. const updatedProduct = { ...product };
  212. if (updates.name !== undefined) updatedProduct.name = updates.name;
  213. if (updates.price !== undefined) updatedProduct.price = updates.price;
  214. if (updates.stock !== undefined) updatedProduct.stock = updates.stock;
  215. if (updates.status !== undefined) updatedProduct.status = updates.status;
  216. mongoStore.products.set(product.id, updatedProduct);
  217. // 同步更新到ES
  218. const syncResult = simulateSync(mongoStore, esStore, 'update', product.id, updatedProduct);
  219. if (!syncResult.success) {
  220. return true; // 同步失败时跳过验证
  221. }
  222. // 验证数据一致性
  223. const consistency = verifyDataConsistency(mongoStore, esStore);
  224. return consistency.consistent;
  225. }
  226. ),
  227. { numRuns: 100 }
  228. );
  229. });
  230. it('删除商品后,ES中不应存在该商品', () => {
  231. fc.assert(
  232. fc.property(
  233. productArb,
  234. (product) => {
  235. const mongoStore: MockMongoDBStore = { products: new Map() };
  236. const esStore: MockElasticsearchStore = { products: new Map() };
  237. // 初始创建
  238. mongoStore.products.set(product.id, product);
  239. esStore.products.set(product.id, { ...product });
  240. // 从MongoDB删除
  241. mongoStore.products.delete(product.id);
  242. // 同步删除到ES
  243. const syncResult = simulateSync(mongoStore, esStore, 'delete', product.id);
  244. if (!syncResult.success) {
  245. return true; // 同步失败时跳过验证
  246. }
  247. // 验证数据一致性
  248. const consistency = verifyDataConsistency(mongoStore, esStore);
  249. return consistency.consistent;
  250. }
  251. ),
  252. { numRuns: 100 }
  253. );
  254. });
  255. it('批量操作后,所有商品数据应保持一致', () => {
  256. fc.assert(
  257. fc.property(
  258. fc.array(productArb, { minLength: 1, maxLength: 10 }),
  259. (products) => {
  260. const mongoStore: MockMongoDBStore = { products: new Map() };
  261. const esStore: MockElasticsearchStore = { products: new Map() };
  262. // 批量创建
  263. for (const product of products) {
  264. mongoStore.products.set(product.id, product);
  265. simulateSync(mongoStore, esStore, 'index', product.id, product);
  266. }
  267. // 验证数据一致性
  268. const consistency = verifyDataConsistency(mongoStore, esStore);
  269. return consistency.consistent;
  270. }
  271. ),
  272. { numRuns: 100 }
  273. );
  274. });
  275. it('重试机制应最终保证数据同步成功', () => {
  276. fc.assert(
  277. fc.property(
  278. productArb,
  279. fc.integer({ min: 0, max: 2 }), // 模拟0-2次失败
  280. (product, _failCount) => {
  281. const mongoStore: MockMongoDBStore = { products: new Map() };
  282. const esStore: MockElasticsearchStore = { products: new Map() };
  283. // 在MongoDB中创建商品
  284. mongoStore.products.set(product.id, product);
  285. // 使用带重试的同步(failureRate=0表示不会失败)
  286. const syncResult = simulateSyncWithRetry(
  287. mongoStore,
  288. esStore,
  289. 'index',
  290. product.id,
  291. product,
  292. 3, // maxRetries
  293. 0 // failureRate = 0,确保最终成功
  294. );
  295. // 验证同步成功
  296. if (!syncResult.success) {
  297. return false;
  298. }
  299. // 验证数据一致性
  300. const consistency = verifyDataConsistency(mongoStore, esStore);
  301. return consistency.consistent;
  302. }
  303. ),
  304. { numRuns: 100 }
  305. );
  306. });
  307. it('混合操作序列后数据应保持一致', () => {
  308. fc.assert(
  309. fc.property(
  310. fc.array(productArb, { minLength: 2, maxLength: 5 }),
  311. fc.array(
  312. fc.record({
  313. operationType: fc.constantFrom('create' as const, 'update' as const, 'delete' as const),
  314. productIndex: fc.nat()
  315. }),
  316. { minLength: 1, maxLength: 10 }
  317. ),
  318. (products, operations) => {
  319. const mongoStore: MockMongoDBStore = { products: new Map() };
  320. const esStore: MockElasticsearchStore = { products: new Map() };
  321. // 执行操作序列
  322. for (const op of operations) {
  323. const productIndex = op.productIndex % products.length;
  324. const product = products[productIndex];
  325. switch (op.operationType) {
  326. case 'create':
  327. mongoStore.products.set(product.id, product);
  328. simulateSync(mongoStore, esStore, 'index', product.id, product);
  329. break;
  330. case 'update':
  331. if (mongoStore.products.has(product.id)) {
  332. const updated = { ...product, stock: product.stock + 1 };
  333. mongoStore.products.set(product.id, updated);
  334. simulateSync(mongoStore, esStore, 'update', product.id, updated);
  335. }
  336. break;
  337. case 'delete':
  338. mongoStore.products.delete(product.id);
  339. simulateSync(mongoStore, esStore, 'delete', product.id);
  340. break;
  341. }
  342. }
  343. // 验证最终数据一致性
  344. const consistency = verifyDataConsistency(mongoStore, esStore);
  345. return consistency.consistent;
  346. }
  347. ),
  348. { numRuns: 100 }
  349. );
  350. });
  351. it('同步结果应正确反映操作类型', () => {
  352. fc.assert(
  353. fc.property(
  354. productArb,
  355. fc.constantFrom('index' as const, 'update' as const, 'delete' as const),
  356. (product, operation) => {
  357. const mongoStore: MockMongoDBStore = { products: new Map() };
  358. const esStore: MockElasticsearchStore = { products: new Map() };
  359. // 对于update和delete,先创建商品
  360. if (operation === 'update' || operation === 'delete') {
  361. mongoStore.products.set(product.id, product);
  362. esStore.products.set(product.id, { ...product });
  363. }
  364. const syncResult = simulateSync(
  365. mongoStore,
  366. esStore,
  367. operation,
  368. product.id,
  369. operation !== 'delete' ? product : undefined
  370. );
  371. // 验证返回的操作类型正确
  372. return syncResult.operation === operation && syncResult.productId === product.id;
  373. }
  374. ),
  375. { numRuns: 100 }
  376. );
  377. });
  378. });
  379. });