@sailboat-computer/data-storage
Version:
Shared data storage library for sailboat computer v3
457 lines (404 loc) • 16.4 kB
text/typescript
/**
* 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();
});
});