import { MemoryStorageAdapter, MemoryStorageConfig } from '../../../../src/adapters/storage/MemoryStorageAdapter'; import { QueryCondition, SortOption } from '../../../../src/interfaces/StorageAdapter'; interface TestItem { id?: string; name: string; value: number; tags: string[]; status: 'active' | 'inactive' | 'pending'; createdAt?: number; updatedAt?: number; description?: string; } const testData: Omit[] = [ { name: 'Item A', value: 10, tags: ['tag1', 'tag2'], status: 'active' }, { name: 'Item B', value: 20, tags: ['tag2', 'tag3'], status: 'inactive' }, { name: 'Item C', value: 30, tags: ['tag1', 'tag3'], status: 'pending' }, { name: 'Item D', value: 15, tags: ['tag1'], status: 'active' }, { name: 'Item E', value: 25, tags: ['tag3'], status: 'inactive' }, { name: 'Item F', value: 5, tags: ['tag2'], status: 'pending' }, { name: 'Special Item', value: 50, tags: ['special', 'tag1'], status: 'active' }, ]; describe('MemoryStorageAdapter Unit Tests', () => { let adapter: MemoryStorageAdapter; beforeEach(() => { adapter = new MemoryStorageAdapter(); }); afterEach(async () => { // 关闭适配器,释放资源 await adapter.close(); }); describe('Basic Operations', () => { test('should initialize correctly with default config', async () => { await adapter.initialize(); expect(await adapter.isAvailable()).toBe(true); await adapter.close(); }); test('should close correctly and clear data', async () => { await adapter.initialize(); await adapter.create('testItems', testData[0]); await adapter.close(); expect(await adapter.isAvailable()).toBe(false); }); test('should throw error when used before initialization', async () => { await expect(adapter.create('testItems', testData[0])).rejects.toThrow('MemoryStorageAdapter is not initialized'); await expect(adapter.getById('testItems', '1')).rejects.toThrow('MemoryStorageAdapter is not initialized'); await expect(adapter.find('testItems')).rejects.toThrow('MemoryStorageAdapter is not initialized'); await expect(adapter.update('testItems', '1', {})).rejects.toThrow('MemoryStorageAdapter is not initialized'); await expect(adapter.delete('testItems', '1')).rejects.toThrow('MemoryStorageAdapter is not initialized'); await expect(adapter.bulkCreate('testItems', [testData[0]])).rejects.toThrow('MemoryStorageAdapter is not initialized'); }); }); describe('Create and Retrieve Operations', () => { beforeEach(async () => { await adapter.initialize(); }); test('should create and retrieve items with generated id', async () => { // Create an item without providing an id const createdItem = await adapter.create('testItems', testData[0]); // Verify the item was created with an id expect(createdItem).toBeDefined(); expect(createdItem.id).toBeDefined(); expect(createdItem.name).toBe(testData[0].name); expect(createdItem.createdAt).toBeDefined(); expect(createdItem.updatedAt).toBeDefined(); expect(createdItem.createdAt).toBe(createdItem.updatedAt); // Retrieve the item by id const retrievedItem = await adapter.getById('testItems', createdItem.id); // Verify the retrieved item matches the created item expect(retrievedItem).toEqual(createdItem); }); test('should use provided id when creating items', async () => { // Create an item with a provided id const customId = 'custom-123'; const createdItem = await adapter.create('testItems', { ...testData[0], id: customId }); // Verify the item was created with the provided id expect(createdItem.id).toBe(customId); // Retrieve the item by the provided id const retrievedItem = await adapter.getById('testItems', customId); // Verify the retrieved item matches the created item expect(retrievedItem).toBeDefined(); expect(retrievedItem?.id).toBe(customId); }); test('should return null for non-existent items', async () => { // Try to get an item with a non-existent id const retrievedItem = await adapter.getById('testItems', 'non-existent-id'); // Verify null is returned expect(retrievedItem).toBeNull(); }); test('should handle special characters in names and values', async () => { // Create an item with special characters const specialItem = { name: 'Special@Item#123', value: 100, tags: ['special', 'test'], status: 'active' as const, description: 'Item with special characters: !@#$%^&*()_+-=[]{}|;:,.<>?' }; const createdItem = await adapter.create('testItems', specialItem); // Verify the item was created correctly expect(createdItem).toBeDefined(); expect(createdItem.name).toBe(specialItem.name); expect(createdItem.description).toBe(specialItem.description); // Retrieve the item const retrievedItem = await adapter.getById('testItems', createdItem.id); // Verify the retrieved item matches expect(retrievedItem).toEqual(createdItem); }); test('should handle empty values correctly', async () => { // Create an item with empty values const emptyItem = { name: '', value: 0, tags: [], status: 'active' as const, description: '' }; const createdItem = await adapter.create('testItems', emptyItem); // Verify the item was created correctly expect(createdItem).toBeDefined(); expect(createdItem.name).toBe(emptyItem.name); expect(createdItem.value).toBe(emptyItem.value); expect(createdItem.tags).toEqual(emptyItem.tags); expect(createdItem.description).toBe(emptyItem.description); // Retrieve the item const retrievedItem = await adapter.getById('testItems', createdItem.id); // Verify the retrieved item matches expect(retrievedItem).toEqual(createdItem); }); }); describe('Update and Delete Operations', () => { beforeEach(async () => { await adapter.initialize(); }); test('should update existing items', async () => { // Create an item const createdItem = await adapter.create('testItems', testData[0]); // Update the item const updateData = { name: 'Updated Item', value: 999, status: 'inactive' as const }; const updatedItem = await adapter.update('testItems', createdItem.id, updateData); // Verify the item was updated correctly expect(updatedItem).toBeDefined(); expect(updatedItem?.id).toBe(createdItem.id); expect(updatedItem?.name).toBe(updateData.name); expect(updatedItem?.value).toBe(updateData.value); expect(updatedItem?.status).toBe(updateData.status); expect(updatedItem?.updatedAt).toBeGreaterThanOrEqual(updatedItem?.createdAt || 0); // Verify the update is persisted const retrievedItem = await adapter.getById('testItems', createdItem.id); expect(retrievedItem).toEqual(updatedItem); }); test('should return null when updating non-existent items', async () => { // Try to update a non-existent item const updatedItem = await adapter.update('testItems', 'non-existent-id', { name: 'Updated' }); // Verify null is returned expect(updatedItem).toBeNull(); }); test('should delete existing items', async () => { // Create an item const createdItem = await adapter.create('testItems', testData[0]); // Verify the item exists const beforeDelete = await adapter.getById('testItems', createdItem.id); expect(beforeDelete).toBeDefined(); // Delete the item const deleted = await adapter.delete('testItems', createdItem.id); // Verify the item was deleted expect(deleted).toBe(true); // Verify the item no longer exists const afterDelete = await adapter.getById('testItems', createdItem.id); expect(afterDelete).toBeNull(); }); test('should return false when deleting non-existent items', async () => { // Try to delete a non-existent item const deleted = await adapter.delete('testItems', 'non-existent-id'); // Verify false is returned expect(deleted).toBe(false); }); }); describe('Bulk Operations', () => { beforeEach(async () => { await adapter.initialize(); }); test('should create multiple items in bulk', async () => { // Create multiple items in bulk const createdItems = await adapter.bulkCreate('testItems', testData); // Verify all items were created expect(createdItems.length).toBe(testData.length); createdItems.forEach((item, index) => { expect(item.id).toBeDefined(); expect(item.name).toBe(testData[index].name); expect(item.value).toBe(testData[index].value); expect(item.status).toBe(testData[index].status); }); // Verify all items can be retrieved const result = await adapter.find('testItems'); expect(result.total).toBe(testData.length); expect(result.data.length).toBe(testData.length); }); test('should handle empty bulk create', async () => { // Try to create an empty array of items const createdItems = await adapter.bulkCreate('testItems', []); // Verify an empty array is returned expect(createdItems).toEqual([]); // Verify no items were created const result = await adapter.find('testItems'); expect(result.total).toBe(0); }); }); describe('Query Operations', () => { beforeEach(async () => { await adapter.initialize(); // Create test data await adapter.bulkCreate('testItems', testData); }); test('should find all items with no query', async () => { // Find all items with no query const result = await adapter.find('testItems'); // Verify all items are returned expect(result.total).toBe(testData.length); expect(result.data.length).toBe(testData.length); }); test('should find items with exact match query', async () => { // Query for items with status 'active' const query: QueryCondition = { status: 'active' }; const result = await adapter.find('testItems', query); // Verify only active items are returned expect(result.data.length).toBeGreaterThan(0); result.data.forEach(item => { expect(item.status).toBe('active'); }); }); test('should find items with $eq operator', async () => { // Query for items with value equal to 10 const query: QueryCondition = { value: { $eq: 10 } }; const result = await adapter.find('testItems', query); // Verify only items with value 10 are returned expect(result.data.length).toBe(1); result.data.forEach(item => { expect(item.value).toBe(10); }); }); test('should find items with $neq operator', async () => { // Query for items with value not equal to 10 const query: QueryCondition = { value: { $neq: 10 } }; const result = await adapter.find('testItems', query); // Verify items with value 10 are excluded expect(result.data.length).toBe(testData.length - 1); result.data.forEach(item => { expect(item.value).not.toBe(10); }); }); test('should find items with $gt and $lt operators', async () => { // Query for items with value between 15 and 30 (exclusive) const query: QueryCondition = { value: { $gt: 15, $lt: 30 } }; const result = await adapter.find('testItems', query); // Verify only items with value between 15 and 30 are returned result.data.forEach(item => { expect(item.value).toBeGreaterThan(15); expect(item.value).toBeLessThan(30); }); }); test('should find items with $gte and $lte operators', async () => { // Query for items with value between 10 and 20 (inclusive) const query: QueryCondition = { value: { $gte: 10, $lte: 20 } }; const result = await adapter.find('testItems', query); // Verify only items with value between 10 and 20 are returned result.data.forEach(item => { expect(item.value).toBeGreaterThanOrEqual(10); expect(item.value).toBeLessThanOrEqual(20); }); }); test('should find items with $in operator', async () => { // Query for items with status 'active' or 'pending' const query: QueryCondition = { status: { $in: ['active', 'pending'] } }; const result = await adapter.find('testItems', query); // Verify only items with status 'active' or 'pending' are returned result.data.forEach(item => { expect(['active', 'pending']).toContain(item.status); }); }); test('should find items with $nin operator', async () => { // Query for items with status not 'inactive' const query: QueryCondition = { status: { $nin: ['inactive'] } }; const result = await adapter.find('testItems', query); // Verify items with status 'inactive' are excluded result.data.forEach(item => { expect(item.status).not.toBe('inactive'); }); }); test('should find items with $like operator', async () => { // Query for items with name containing 'Item' const query: QueryCondition = { name: { $like: '%Item%' } }; const result = await adapter.find('testItems', query); // Verify only items with name containing 'Item' are returned result.data.forEach(item => { expect(item.name).toContain('Item'); }); }); test('should sort items correctly', async () => { // Sort by value in ascending order const sortAsc: SortOption = { value: 'asc' }; const resultAsc = await adapter.find('testItems', {}, 0, 100, sortAsc); // Verify items are sorted in ascending order for (let i = 1; i < resultAsc.data.length; i++) { expect(resultAsc.data[i].value).toBeGreaterThanOrEqual(resultAsc.data[i - 1].value); } // Sort by value in descending order const sortDesc: SortOption = { value: 'desc' }; const resultDesc = await adapter.find('testItems', {}, 0, 100, sortDesc); // Verify items are sorted in descending order for (let i = 1; i < resultDesc.data.length; i++) { expect(resultDesc.data[i].value).toBeLessThanOrEqual(resultDesc.data[i - 1].value); } }); test('should support pagination', async () => { // Set page size to 3 const pageSize = 3; // Get first page const page1 = await adapter.find('testItems', {}, 0, pageSize); // Get second page const page2 = await adapter.find('testItems', {}, pageSize, pageSize); // Get third page const page3 = await adapter.find('testItems', {}, pageSize * 2, pageSize); // Verify page sizes expect(page1.data.length).toBe(pageSize); expect(page2.data.length).toBe(pageSize); expect(page3.data.length).toBe(testData.length - pageSize * 2); // Verify total count is consistent expect(page1.total).toBe(testData.length); expect(page2.total).toBe(testData.length); expect(page3.total).toBe(testData.length); // Verify no duplicate items across pages const allIds = [...page1.data, ...page2.data, ...page3.data].map(item => item.id); const uniqueIds = new Set(allIds); expect(uniqueIds.size).toBe(testData.length); }); test('should combine query, sort and pagination', async () => { // Query for active items, sort by value descending, get first 2 const query: QueryCondition = { status: 'active' }; const sort: SortOption = { value: 'desc' }; const result = await adapter.find('testItems', query, 0, 2, sort); // Verify results expect(result.data.length).toBe(2); expect(result.data[0].name).toBe('Special Item'); // value: 50 expect(result.data[1].name).toBe('Item D'); // value: 15 }); }); describe('Configuration Options', () => { test('should respect clearOnInitialize config', async () => { // Create an adapter and add some data const config1: MemoryStorageConfig = { type: 'memory', clearOnInitialize: false }; await adapter.initialize(config1); await adapter.create('testItems', testData[0]); // Verify data exists const result1 = await adapter.find('testItems'); expect(result1.total).toBe(1); // Re-initialize with clearOnInitialize: true const config2: MemoryStorageConfig = { type: 'memory', clearOnInitialize: true }; await adapter.initialize(config2); // Verify data was cleared const result2 = await adapter.find('testItems'); expect(result2.total).toBe(0); // Re-initialize with clearOnInitialize: false await adapter.initialize(config1); await adapter.create('testItems', testData[0]); // Re-initialize again with clearOnInitialize: false await adapter.initialize(config1); // Verify data still exists const result3 = await adapter.find('testItems'); expect(result3.total).toBe(1); }); }); describe('TTL Functionality', () => { test('should handle items with TTL', async () => { // Create an adapter with defaultTTL of 100ms const config: MemoryStorageConfig = { type: 'memory', defaultTTL: 100, enableTTLCheck: true }; await adapter.initialize(config); // Create an item const createdItem = await adapter.create('testItems', testData[0]); // Verify the item exists let retrievedItem = await adapter.getById('testItems', createdItem.id); expect(retrievedItem).toBeDefined(); // Wait for the item to expire await new Promise(resolve => setTimeout(resolve, 150)); // Verify the item is no longer accessible retrievedItem = await adapter.getById('testItems', createdItem.id); expect(retrievedItem).toBeNull(); // Verify the item is excluded from find results const result = await adapter.find('testItems'); expect(result.total).toBe(0); }); test('should not expire items with TTL disabled', async () => { // Create an adapter with defaultTTL but TTL disabled const config: MemoryStorageConfig = { type: 'memory', defaultTTL: 100, enableTTLCheck: false }; await adapter.initialize(config); // Create an item const createdItem = await adapter.create('testItems', testData[0]); // Verify the item exists let retrievedItem = await adapter.getById('testItems', createdItem.id); expect(retrievedItem).toBeDefined(); // Wait for the item would have expired await new Promise(resolve => setTimeout(resolve, 150)); // Verify the item is still accessible (TTL check disabled) retrievedItem = await adapter.getById('testItems', createdItem.id); expect(retrievedItem).toBeDefined(); }); }); describe('Namespaces', () => { beforeEach(async () => { await adapter.initialize(); }); test('should isolate data between namespaces', async () => { // Create items in different namespaces await adapter.create('namespace1', testData[0]); await adapter.create('namespace2', testData[1]); await adapter.create('namespace1', testData[2]); // Verify data is isolated const result1 = await adapter.find('namespace1'); const result2 = await adapter.find('namespace2'); expect(result1.total).toBe(2); expect(result2.total).toBe(1); // Verify items in namespace1 are correct const names1 = result1.data.map(item => item.name); expect(names1).toContain(testData[0].name); expect(names1).toContain(testData[2].name); // Verify items in namespace2 are correct const names2 = result2.data.map(item => item.name); expect(names2).toContain(testData[1].name); }); test('should handle empty and special namespace names', async () => { // Create items with special namespace names await adapter.create('', testData[0]); await adapter.create('special-namespace_123', testData[1]); await adapter.create('namespace with spaces', testData[2]); // Verify all items can be retrieved expect((await adapter.find('')).total).toBe(1); expect((await adapter.find('special-namespace_123')).total).toBe(1); expect((await adapter.find('namespace with spaces')).total).toBe(1); }); }); describe('Edge Cases', () => { beforeEach(async () => { await adapter.initialize(); }); test('should handle very large values', async () => { // Create an item with a very large value const largeItem = { name: 'Large Value Item', value: Number.MAX_SAFE_INTEGER, tags: ['large'], status: 'active' as const }; const createdItem = await adapter.create('testItems', largeItem); // Verify the item was created correctly expect(createdItem).toBeDefined(); expect(createdItem.value).toBe(Number.MAX_SAFE_INTEGER); // Retrieve the item const retrievedItem = await adapter.getById('testItems', createdItem.id); // Verify the retrieved item matches expect(retrievedItem).toEqual(createdItem); }); test('should handle null and undefined values', async () => { // Create an item with null and undefined values const nullUndefinedItem = { name: 'Null Undefined Item', value: 0, tags: ['test'], status: 'active' as const, description: null as any }; const createdItem = await adapter.create('testItems', nullUndefinedItem); // Verify the item was created correctly expect(createdItem).toBeDefined(); expect(createdItem.description).toBeNull(); // Update with undefined value const updatedItem = await adapter.update('testItems', createdItem.id, { description: undefined as any }); // Verify the update was applied expect(updatedItem?.description).toBeUndefined(); }); }); });