| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 |
- 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<string, MockProduct>;
- }
- // 模拟MongoDB存储
- interface MockMongoDBStore {
- products: Map<string, MockProduct>;
- }
- /**
- * 纯函数:模拟同步操作
- * 将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<MockProduct> = 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 }
- );
- });
- });
- });
|