import * as fc from 'fast-check'; /** * **Feature: backend-frontend-integration, Property 9: MongoDB-Elasticsearch Sync** * **Validates: Requirements 9.1** * * *For any* product update operation, the data in Elasticsearch SHALL eventually * match the data in MongoDB within a bounded time. */ // 模拟商品数据结构 interface MockProduct { id: string; name: string; description: string; price: number; stock: number; status: 'published' | 'draft' | 'archived'; sellerId: string; category: string; brand?: string; tags?: string[]; } // 同步操作类型 type SyncOperation = 'index' | 'update' | 'delete'; // 同步结果 interface SyncResult { success: boolean; productId: string; operation: SyncOperation; retries: number; error?: string; } // 模拟ES存储 interface MockElasticsearchStore { products: Map; } // 模拟MongoDB存储 interface MockMongoDBStore { products: Map; } /** * 纯函数:模拟同步操作 * 将MongoDB中的商品数据同步到Elasticsearch */ function simulateSync( _mongoStore: MockMongoDBStore, esStore: MockElasticsearchStore, operation: SyncOperation, productId: string, productData?: MockProduct, shouldFail: boolean = false ): SyncResult { if (shouldFail) { return { success: false, productId, operation, retries: 3, error: 'Simulated sync failure' }; } switch (operation) { case 'index': if (productData) { esStore.products.set(productId, { ...productData }); } break; case 'update': if (productData && esStore.products.has(productId)) { esStore.products.set(productId, { ...productData }); } else if (productData) { // 如果ES中不存在,则创建 esStore.products.set(productId, { ...productData }); } break; case 'delete': esStore.products.delete(productId); break; } return { success: true, productId, operation, retries: 0 }; } /** * 纯函数:验证MongoDB和ES数据一致性 */ function verifyDataConsistency( mongoStore: MockMongoDBStore, esStore: MockElasticsearchStore ): { consistent: boolean; differences: string[] } { const differences: string[] = []; // 检查MongoDB中的每个商品是否在ES中存在且数据一致 for (const [productId, mongoProduct] of mongoStore.products) { const esProduct = esStore.products.get(productId); if (!esProduct) { differences.push(`Product ${productId} exists in MongoDB but not in ES`); continue; } // 比较关键字段 if (mongoProduct.name !== esProduct.name) { differences.push(`Product ${productId}: name mismatch (MongoDB: ${mongoProduct.name}, ES: ${esProduct.name})`); } if (mongoProduct.price !== esProduct.price) { differences.push(`Product ${productId}: price mismatch (MongoDB: ${mongoProduct.price}, ES: ${esProduct.price})`); } if (mongoProduct.stock !== esProduct.stock) { differences.push(`Product ${productId}: stock mismatch (MongoDB: ${mongoProduct.stock}, ES: ${esProduct.stock})`); } if (mongoProduct.status !== esProduct.status) { differences.push(`Product ${productId}: status mismatch (MongoDB: ${mongoProduct.status}, ES: ${esProduct.status})`); } } // 检查ES中是否有MongoDB中不存在的商品(已删除但未同步) for (const [productId] of esStore.products) { if (!mongoStore.products.has(productId)) { differences.push(`Product ${productId} exists in ES but not in MongoDB (should be deleted)`); } } return { consistent: differences.length === 0, differences }; } /** * 纯函数:模拟带重试的同步 */ function simulateSyncWithRetry( mongoStore: MockMongoDBStore, esStore: MockElasticsearchStore, operation: SyncOperation, productId: string, productData?: MockProduct, maxRetries: number = 3, failureRate: number = 0 ): SyncResult { let retries = 0; for (let attempt = 0; attempt <= maxRetries; attempt++) { // 模拟随机失败 const shouldFail = Math.random() < failureRate && attempt < maxRetries; if (!shouldFail) { const result = simulateSync(mongoStore, esStore, operation, productId, productData, false); result.retries = retries; return result; } retries++; } return { success: false, productId, operation, retries: maxRetries, error: 'Max retries exceeded' }; } describe('SearchSyncService Property Tests', () => { // 生成有效商品的arbitrary const productArb: fc.Arbitrary = fc.record({ id: fc.uuid(), name: fc.string({ minLength: 1, maxLength: 100 }), description: fc.string({ minLength: 0, maxLength: 500 }), price: fc.integer({ min: 1, max: 1000000 }).map(cents => cents / 100), stock: fc.integer({ min: 0, max: 10000 }), status: fc.constantFrom('published' as const, 'draft' as const, 'archived' as const), sellerId: fc.uuid(), category: fc.string({ minLength: 1, maxLength: 50 }), brand: fc.option(fc.string({ minLength: 1, maxLength: 50 }), { nil: undefined }), tags: fc.option(fc.array(fc.string({ minLength: 1, maxLength: 20 }), { minLength: 0, maxLength: 5 }), { nil: undefined }) }); describe('Property 9: MongoDB-Elasticsearch Sync', () => { it('创建商品后,ES中应包含相同的数据', () => { fc.assert( fc.property( productArb, (product) => { const mongoStore: MockMongoDBStore = { products: new Map() }; const esStore: MockElasticsearchStore = { products: new Map() }; // 在MongoDB中创建商品 mongoStore.products.set(product.id, product); // 同步到ES const syncResult = simulateSync(mongoStore, esStore, 'index', product.id, product); if (!syncResult.success) { return true; // 同步失败时跳过验证 } // 验证数据一致性 const consistency = verifyDataConsistency(mongoStore, esStore); return consistency.consistent; } ), { numRuns: 100 } ); }); it('更新商品后,ES中的数据应与MongoDB一致', () => { fc.assert( fc.property( productArb, fc.record({ name: fc.option(fc.string({ minLength: 1, maxLength: 100 }), { nil: undefined }), price: fc.option(fc.integer({ min: 1, max: 1000000 }).map(cents => cents / 100), { nil: undefined }), stock: fc.option(fc.integer({ min: 0, max: 10000 }), { nil: undefined }), status: fc.option(fc.constantFrom('published' as const, 'draft' as const, 'archived' as const), { nil: undefined }) }), (product, updates) => { const mongoStore: MockMongoDBStore = { products: new Map() }; const esStore: MockElasticsearchStore = { products: new Map() }; // 初始创建 mongoStore.products.set(product.id, product); esStore.products.set(product.id, { ...product }); // 应用更新到MongoDB const updatedProduct = { ...product }; if (updates.name !== undefined) updatedProduct.name = updates.name; if (updates.price !== undefined) updatedProduct.price = updates.price; if (updates.stock !== undefined) updatedProduct.stock = updates.stock; if (updates.status !== undefined) updatedProduct.status = updates.status; mongoStore.products.set(product.id, updatedProduct); // 同步更新到ES const syncResult = simulateSync(mongoStore, esStore, 'update', product.id, updatedProduct); if (!syncResult.success) { return true; // 同步失败时跳过验证 } // 验证数据一致性 const consistency = verifyDataConsistency(mongoStore, esStore); return consistency.consistent; } ), { numRuns: 100 } ); }); it('删除商品后,ES中不应存在该商品', () => { fc.assert( fc.property( productArb, (product) => { const mongoStore: MockMongoDBStore = { products: new Map() }; const esStore: MockElasticsearchStore = { products: new Map() }; // 初始创建 mongoStore.products.set(product.id, product); esStore.products.set(product.id, { ...product }); // 从MongoDB删除 mongoStore.products.delete(product.id); // 同步删除到ES const syncResult = simulateSync(mongoStore, esStore, 'delete', product.id); if (!syncResult.success) { return true; // 同步失败时跳过验证 } // 验证数据一致性 const consistency = verifyDataConsistency(mongoStore, esStore); return consistency.consistent; } ), { numRuns: 100 } ); }); it('批量操作后,所有商品数据应保持一致', () => { fc.assert( fc.property( fc.array(productArb, { minLength: 1, maxLength: 10 }), (products) => { const mongoStore: MockMongoDBStore = { products: new Map() }; const esStore: MockElasticsearchStore = { products: new Map() }; // 批量创建 for (const product of products) { mongoStore.products.set(product.id, product); simulateSync(mongoStore, esStore, 'index', product.id, product); } // 验证数据一致性 const consistency = verifyDataConsistency(mongoStore, esStore); return consistency.consistent; } ), { numRuns: 100 } ); }); it('重试机制应最终保证数据同步成功', () => { fc.assert( fc.property( productArb, fc.integer({ min: 0, max: 2 }), // 模拟0-2次失败 (product, _failCount) => { const mongoStore: MockMongoDBStore = { products: new Map() }; const esStore: MockElasticsearchStore = { products: new Map() }; // 在MongoDB中创建商品 mongoStore.products.set(product.id, product); // 使用带重试的同步(failureRate=0表示不会失败) const syncResult = simulateSyncWithRetry( mongoStore, esStore, 'index', product.id, product, 3, // maxRetries 0 // failureRate = 0,确保最终成功 ); // 验证同步成功 if (!syncResult.success) { return false; } // 验证数据一致性 const consistency = verifyDataConsistency(mongoStore, esStore); return consistency.consistent; } ), { numRuns: 100 } ); }); it('混合操作序列后数据应保持一致', () => { fc.assert( fc.property( fc.array(productArb, { minLength: 2, maxLength: 5 }), fc.array( fc.record({ operationType: fc.constantFrom('create' as const, 'update' as const, 'delete' as const), productIndex: fc.nat() }), { minLength: 1, maxLength: 10 } ), (products, operations) => { const mongoStore: MockMongoDBStore = { products: new Map() }; const esStore: MockElasticsearchStore = { products: new Map() }; // 执行操作序列 for (const op of operations) { const productIndex = op.productIndex % products.length; const product = products[productIndex]; switch (op.operationType) { case 'create': mongoStore.products.set(product.id, product); simulateSync(mongoStore, esStore, 'index', product.id, product); break; case 'update': if (mongoStore.products.has(product.id)) { const updated = { ...product, stock: product.stock + 1 }; mongoStore.products.set(product.id, updated); simulateSync(mongoStore, esStore, 'update', product.id, updated); } break; case 'delete': mongoStore.products.delete(product.id); simulateSync(mongoStore, esStore, 'delete', product.id); break; } } // 验证最终数据一致性 const consistency = verifyDataConsistency(mongoStore, esStore); return consistency.consistent; } ), { numRuns: 100 } ); }); it('同步结果应正确反映操作类型', () => { fc.assert( fc.property( productArb, fc.constantFrom('index' as const, 'update' as const, 'delete' as const), (product, operation) => { const mongoStore: MockMongoDBStore = { products: new Map() }; const esStore: MockElasticsearchStore = { products: new Map() }; // 对于update和delete,先创建商品 if (operation === 'update' || operation === 'delete') { mongoStore.products.set(product.id, product); esStore.products.set(product.id, { ...product }); } const syncResult = simulateSync( mongoStore, esStore, operation, product.id, operation !== 'delete' ? product : undefined ); // 验证返回的操作类型正确 return syncResult.operation === operation && syncResult.productId === product.id; } ), { numRuns: 100 } ); }); }); });