UNPKG

@sailboat-computer/data-storage

Version:

Shared data storage library for sailboat computer v3

457 lines (404 loc) 16.4 kB
/** * Provider Unavailability Tests * * Tests the behavior of the UnifiedStorageManager when storage providers are unavailable. */ import * as path from 'path'; import * as os from 'os'; import * as fs from 'fs'; import { StorageTier, StorageConfig } from '../../src/types'; import { createUnifiedStorageManager } from '../../src/storage'; import { MockProvider, MockProviderState } from '../mocks/mock-provider'; describe('Provider Unavailability Tests', () => { // Create a temporary directory for local storage const tempDir = path.join(os.tmpdir(), 'data-storage-tests', `test-${Date.now()}`); // Create mock provider let mockProvider: MockProvider; // Storage configuration let config: StorageConfig; beforeAll(() => { // Create temp directory fs.mkdirSync(tempDir, { recursive: true }); // Create mock provider mockProvider = new MockProvider({ logging: false }); // Create storage configuration with type assertion to allow mock provider config = { providers: { hot: { type: 'redis' as any, // Using 'as any' to bypass type checking config: { host: 'localhost', port: 6379 } }, batching: { timeBased: { enabled: true, intervalMs: 1000 }, sizeBased: { enabled: true, maxBatchSize: 10 }, priorityOverride: true } } }; }); afterAll(() => { // Clean up temp directory fs.rmSync(tempDir, { recursive: true, force: true }); }); beforeEach(() => { // Reset mock provider mockProvider.setState(MockProviderState.HEALTHY); mockProvider.clearData(); mockProvider.resetMetrics(); }); it('should continue operation when provider is unavailable', async () => { // Arrange const storageManager = createUnifiedStorageManager(config, { localStorage: { directory: tempDir, maxSizeBytes: 10 * 1024 * 1024, // 10MB encrypt: false, compressionLevel: 0 } }); // Mock the provider factory to return our mock provider (storageManager as any).remoteStorageManager = { initialize: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), store: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), retrieve: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), update: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), delete: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), storeBatch: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), retrieveBatch: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), cleanup: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), migrate: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), getStatus: jest.fn().mockResolvedValue({ connected: false, healthy: false, error: 'Remote storage unavailable', queryPerformance: { hot: { averageQueryTime: 0, queriesPerSecond: 0 }, warm: { averageQueryTime: 0, queriesPerSecond: 0 }, cold: { averageQueryTime: 0, queriesPerSecond: 0 }, overall: 'unhealthy', providers: { hot: 'unhealthy', warm: 'unhealthy', cold: 'unhealthy' }, issues: { severity: 'error', message: 'Remote storage unavailable', component: 'remote-storage', timestamp: new Date() } } }), close: jest.fn().mockResolvedValue(undefined) }; // Act await storageManager.initialize(); // Store data const sensorData = { sensorId: 'temp-sensor-1', value: 25.5, unit: 'celsius', timestamp: new Date(), quality: 'good' }; const id = await storageManager.store(sensorData, 'sensor-readings', { timestamp: new Date(), tags: { location: 'cabin', sensor: 'temperature' }, tier: StorageTier.HOT }); // Retrieve data const results = await storageManager.retrieve({ category: 'sensor-readings' }); // Get status const status = await storageManager.getStatus(); // Get local storage status const localStatus = await storageManager.getLocalStorageStatus(); // Assert expect(id).toBeDefined(); expect(results.length).toBe(1); expect(results[0].data).toEqual(sensorData); expect(status.connected).toBe(true); // Should be true because local storage is available expect(status.healthy).toBe(false); // Should be false because remote storage is unavailable expect(localStatus.available).toBe(true); expect(localStatus.healthy).toBe(true); // Verify remote storage was attempted but failed expect((storageManager as any).remoteStorageManager.store).toHaveBeenCalled(); expect((storageManager as any).remoteStorageManager.retrieve).toHaveBeenCalled(); // Verify pending sync contains the stored item expect((storageManager as any).pendingSync.size).toBe(1); expect((storageManager as any).pendingSync.has(id)).toBe(true); // Clean up await storageManager.close(); }); it('should synchronize data when provider becomes available again', async () => { // Arrange const storageManager = createUnifiedStorageManager(config, { localStorage: { directory: tempDir, maxSizeBytes: 10 * 1024 * 1024, // 10MB encrypt: false, compressionLevel: 0 } }); // Mock the provider factory to return our mock provider const mockRemoteStore = jest.fn().mockResolvedValue('remote-id'); (storageManager as any).remoteStorageManager = { initialize: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), store: mockRemoteStore, retrieve: jest.fn().mockResolvedValue([]), update: jest.fn().mockResolvedValue(undefined), delete: jest.fn().mockResolvedValue(undefined), storeBatch: jest.fn().mockResolvedValue([]), retrieveBatch: jest.fn().mockResolvedValue([]), cleanup: jest.fn().mockResolvedValue({ itemsRemoved: 0, bytesFreed: 0, duration: 0 }), migrate: jest.fn().mockResolvedValue(undefined), getStatus: jest.fn() .mockRejectedValueOnce(new Error('Remote storage unavailable')) .mockResolvedValue({ connected: true, healthy: true, queryPerformance: { hot: { averageQueryTime: 50, queriesPerSecond: 10 }, warm: { averageQueryTime: 100, queriesPerSecond: 5 }, cold: { averageQueryTime: 200, queriesPerSecond: 2 }, overall: 'healthy', providers: { hot: 'healthy', warm: 'healthy', cold: 'healthy' }, issues: { severity: 'info', message: 'Remote storage is healthy', component: 'remote-storage', timestamp: new Date() } } }), close: jest.fn().mockResolvedValue(undefined) }; // Act await storageManager.initialize(); // Store data while provider is unavailable const sensorData = { sensorId: 'temp-sensor-1', value: 25.5, unit: 'celsius', timestamp: new Date(), quality: 'good' }; const id = await storageManager.store(sensorData, 'sensor-readings', { timestamp: new Date(), tags: { location: 'cabin', sensor: 'temperature' }, tier: StorageTier.HOT }); // Verify data is in pending sync expect((storageManager as any).pendingSync.size).toBe(1); expect((storageManager as any).pendingSync.has(id)).toBe(true); // Force synchronization (provider is now available) const syncResult = await storageManager.forceSynchronization(); // Assert expect(syncResult.success).toBe(true); expect(syncResult.itemCount).toBe(1); expect(syncResult.failedCount).toBe(0); expect(mockRemoteStore).toHaveBeenCalled(); // Verify pending sync is now empty expect((storageManager as any).pendingSync.size).toBe(0); // Clean up await storageManager.close(); }); it('should handle multiple provider failures and recoveries', async () => { // Arrange const storageManager = createUnifiedStorageManager(config, { localStorage: { directory: tempDir, maxSizeBytes: 10 * 1024 * 1024, // 10MB encrypt: false, compressionLevel: 0 } }); // Mock the provider factory with alternating failures and successes const mockRemoteStore = jest.fn() .mockRejectedValueOnce(new Error('Remote storage unavailable')) .mockResolvedValueOnce('remote-id-1') .mockRejectedValueOnce(new Error('Remote storage unavailable')) .mockResolvedValueOnce('remote-id-2'); const mockRemoteRetrieve = jest.fn() .mockRejectedValueOnce(new Error('Remote storage unavailable')) .mockResolvedValueOnce([{ data: { value: 1 }, metadata: { id: 'remote-id-1', category: 'test' } }]) .mockRejectedValueOnce(new Error('Remote storage unavailable')) .mockResolvedValueOnce([{ data: { value: 2 }, metadata: { id: 'remote-id-2', category: 'test' } }]); (storageManager as any).remoteStorageManager = { initialize: jest.fn().mockResolvedValue(undefined), store: mockRemoteStore, retrieve: mockRemoteRetrieve, update: jest.fn().mockResolvedValue(undefined), delete: jest.fn().mockResolvedValue(undefined), storeBatch: jest.fn().mockResolvedValue([]), retrieveBatch: jest.fn().mockResolvedValue([]), cleanup: jest.fn().mockResolvedValue({ itemsRemoved: 0, bytesFreed: 0, duration: 0 }), migrate: jest.fn().mockResolvedValue(undefined), getStatus: jest.fn().mockResolvedValue({ connected: true, healthy: true, queryPerformance: { hot: { averageQueryTime: 50, queriesPerSecond: 10 }, warm: { averageQueryTime: 100, queriesPerSecond: 5 }, cold: { averageQueryTime: 200, queriesPerSecond: 2 }, overall: 'healthy', providers: { hot: 'healthy', warm: 'healthy', cold: 'healthy' }, issues: { severity: 'info', message: 'Remote storage is healthy', component: 'remote-storage', timestamp: new Date() } } }), close: jest.fn().mockResolvedValue(undefined) }; // Act await storageManager.initialize(); // First store (should fail and use local storage) const id1 = await storageManager.store({ value: 1 }, 'test'); // Second store (should succeed and use remote storage) const id2 = await storageManager.store({ value: 2 }, 'test'); // First retrieve (should fail and use local storage) const results1 = await storageManager.retrieve({ category: 'test' }); // Second retrieve (should succeed and use remote storage) const results2 = await storageManager.retrieve({ category: 'test' }); // Assert expect(id1).toBeDefined(); expect(id2).toBe('remote-id-1'); expect(results1.length).toBe(2); // Both items from local storage expect(results2.length).toBe(1); // One item from remote storage // Verify call counts expect(mockRemoteStore).toHaveBeenCalledTimes(2); expect(mockRemoteRetrieve).toHaveBeenCalledTimes(2); // Verify pending sync contains only the first item expect((storageManager as any).pendingSync.size).toBe(1); expect((storageManager as any).pendingSync.has(id1)).toBe(true); // Clean up await storageManager.close(); }); it('should handle all providers being unavailable', async () => { // Arrange const storageManager = createUnifiedStorageManager(config, { localStorage: { directory: tempDir, maxSizeBytes: 10 * 1024 * 1024, // 10MB encrypt: false, compressionLevel: 0 } }); // Mock both remote and local storage to fail (storageManager as any).remoteStorageManager = { initialize: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), store: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), retrieve: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), update: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), delete: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), storeBatch: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), retrieveBatch: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), cleanup: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), migrate: jest.fn().mockRejectedValue(new Error('Remote storage unavailable')), getStatus: jest.fn().mockResolvedValue({ connected: false, healthy: false, error: 'Remote storage unavailable', queryPerformance: { hot: { averageQueryTime: 0, queriesPerSecond: 0 }, warm: { averageQueryTime: 0, queriesPerSecond: 0 }, cold: { averageQueryTime: 0, queriesPerSecond: 0 }, overall: 'unhealthy', providers: { hot: 'unhealthy', warm: 'unhealthy', cold: 'unhealthy' }, issues: { severity: 'error', message: 'Remote storage unavailable', component: 'remote-storage', timestamp: new Date() } } }), close: jest.fn().mockResolvedValue(undefined) }; // Mock local storage provider to fail (storageManager as any).localStorageProvider = { initialize: jest.fn().mockRejectedValue(new Error('Local storage unavailable')), store: jest.fn().mockRejectedValue(new Error('Local storage unavailable')), retrieve: jest.fn().mockRejectedValue(new Error('Local storage unavailable')), update: jest.fn().mockRejectedValue(new Error('Local storage unavailable')), delete: jest.fn().mockRejectedValue(new Error('Local storage unavailable')), storeBatch: jest.fn().mockRejectedValue(new Error('Local storage unavailable')), cleanup: jest.fn().mockRejectedValue(new Error('Local storage unavailable')), getStatus: jest.fn().mockResolvedValue({ connected: false, healthy: false, error: 'Local storage unavailable', queryPerformance: { hot: { averageQueryTime: 0, queriesPerSecond: 0 }, warm: { averageQueryTime: 0, queriesPerSecond: 0 }, cold: { averageQueryTime: 0, queriesPerSecond: 0 }, overall: 'unhealthy', providers: { hot: 'unhealthy', warm: 'unhealthy', cold: 'unhealthy' }, issues: { severity: 'error', message: 'Local storage unavailable', component: 'local-storage', timestamp: new Date() } } }), close: jest.fn().mockResolvedValue(undefined), getLocalStorageStatus: jest.fn().mockResolvedValue({ available: false, healthy: false, totalSizeBytes: 0, usedSizeBytes: 0, freeSizeBytes: 0, itemCount: 0, lastError: 'Local storage unavailable' }) }; // Act & Assert await expect(storageManager.initialize()).rejects.toThrow(); // Get status should still work const status = await storageManager.getStatus(); expect(status.connected).toBe(false); expect(status.healthy).toBe(false); expect(status.error).toBeDefined(); // Operations should fail await expect(storageManager.store({ value: 1 }, 'test')).rejects.toThrow(); await expect(storageManager.retrieve({ category: 'test' })).rejects.toThrow(); // Clean up await storageManager.close(); }); });