qmemory
Version:
A comprehensive production-ready Node.js utility library with MongoDB document operations, user ownership enforcement, Express.js HTTP utilities, environment-aware logging, and in-memory storage. Features 96%+ test coverage with comprehensive error handli
554 lines (434 loc) • 19.5 kB
JavaScript
/**
* Binary Storage Tests
*
* Comprehensive test suite for all binary storage implementations:
* - Interface compliance testing
* - Memory storage functionality
* - File system storage functionality
* - Storage factory functionality
* - Error handling and edge cases
*/
const {
IStorage,
MemoryBinaryStorage,
FileSystemBinaryStorage,
StorageFactory,
getDefaultStorage
} = require('../../lib/binary-storage');
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
describe('Binary Storage', () => {
describe('IStorage Interface', () => {
test('should define the required interface methods', () => {
const storage = new IStorage();
expect(typeof storage.save).toBe('function');
expect(typeof storage.get).toBe('function');
expect(typeof storage.delete).toBe('function');
expect(typeof storage.exists).toBe('function');
expect(typeof storage.getStats).toBe('function');
});
test('should throw errors for unimplemented methods', async () => {
const storage = new IStorage();
await expect(storage.save('key', Buffer.from('data'))).rejects.toThrow('save method must be implemented');
await expect(storage.get('key')).rejects.toThrow('get method must be implemented');
await expect(storage.delete('key')).rejects.toThrow('delete method must be implemented');
await expect(storage.exists('key')).rejects.toThrow('exists method must be implemented');
});
test('should provide default stats implementation', async () => {
const storage = new IStorage();
const stats = await storage.getStats();
expect(stats).toEqual({
type: 'unknown',
itemCount: 0,
totalSize: 0
});
});
});
describe('MemoryBinaryStorage', () => {
let storage;
beforeEach(() => {
storage = new MemoryBinaryStorage();
});
afterEach(async () => {
await storage.clear();
});
describe('Basic Operations', () => {
test('should save and retrieve binary data', async () => {
const key = 'test-image';
const data = Buffer.from('binary image data', 'utf8');
await storage.save(key, data);
const retrieved = await storage.get(key);
expect(retrieved).toEqual(data);
expect(Buffer.isBuffer(retrieved)).toBe(true);
});
test('should return null for non-existent keys', async () => {
const result = await storage.get('non-existent-key');
expect(result).toBeNull();
});
test('should check existence correctly', async () => {
const key = 'test-key';
const data = Buffer.from('test data');
expect(await storage.exists(key)).toBe(false);
await storage.save(key, data);
expect(await storage.exists(key)).toBe(true);
await storage.delete(key);
expect(await storage.exists(key)).toBe(false);
});
test('should delete stored data', async () => {
const key = 'delete-test';
const data = Buffer.from('data to delete');
await storage.save(key, data);
expect(await storage.exists(key)).toBe(true);
await storage.delete(key);
expect(await storage.exists(key)).toBe(false);
expect(await storage.get(key)).toBeNull();
});
test('should handle deleting non-existent keys gracefully', async () => {
await expect(storage.delete('non-existent')).resolves.not.toThrow();
});
});
describe('Data Integrity', () => {
test('should store and retrieve data without corruption', async () => {
const originalData = Buffer.from([0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD]);
await storage.save('binary-test', originalData);
const retrievedData = await storage.get('binary-test');
expect(retrievedData).toEqual(originalData);
expect(retrievedData.length).toBe(originalData.length);
// Verify each byte
for (let i = 0; i < originalData.length; i++) {
expect(retrievedData[i]).toBe(originalData[i]);
}
});
test('should handle empty buffers', async () => {
const emptyBuffer = Buffer.alloc(0);
await storage.save('empty', emptyBuffer);
const retrieved = await storage.get('empty');
expect(retrieved).toEqual(emptyBuffer);
expect(retrieved.length).toBe(0);
});
test('should handle large buffers', async () => {
const largeBuffer = Buffer.alloc(1024 * 1024, 0xAB); // 1MB of 0xAB
await storage.save('large', largeBuffer);
const retrieved = await storage.get('large');
expect(retrieved.length).toBe(largeBuffer.length);
expect(retrieved[0]).toBe(0xAB);
expect(retrieved[retrieved.length - 1]).toBe(0xAB);
});
test('should return copies of data to prevent external mutation', async () => {
const originalData = Buffer.from([1, 2, 3, 4]);
await storage.save('mutation-test', originalData);
const retrieved1 = await storage.get('mutation-test');
const retrieved2 = await storage.get('mutation-test');
// Modify one copy
retrieved1[0] = 99;
// Other copy should be unchanged
expect(retrieved2[0]).toBe(1);
// Original stored data should be unchanged
const retrieved3 = await storage.get('mutation-test');
expect(retrieved3[0]).toBe(1);
});
});
describe('Key Validation', () => {
test('should reject invalid keys', async () => {
const data = Buffer.from('test');
await expect(storage.save('', data)).rejects.toThrow('Key must be a non-empty string');
await expect(storage.save(123, data)).rejects.toThrow('Key must be a non-empty string');
await expect(storage.save(null, data)).rejects.toThrow('Key must be a non-empty string');
await expect(storage.save(undefined, data)).rejects.toThrow('Key must be a non-empty string');
});
test('should reject keys with path separators', async () => {
const data = Buffer.from('test');
await expect(storage.save('key/with/slash', data)).rejects.toThrow('Key cannot contain path separators');
await expect(storage.save('key\\with\\backslash', data)).rejects.toThrow('Key cannot contain path separators');
await expect(storage.save('../relative', data)).rejects.toThrow('Key cannot contain path separators');
});
test('should reject overly long keys', async () => {
const longKey = 'a'.repeat(251);
const data = Buffer.from('test');
await expect(storage.save(longKey, data)).rejects.toThrow('Key must be 250 characters or less');
});
});
describe('Data Validation', () => {
test('should reject non-Buffer data', async () => {
await expect(storage.save('key', 'string')).rejects.toThrow('Data must be a Buffer object');
await expect(storage.save('key', 123)).rejects.toThrow('Data must be a Buffer object');
await expect(storage.save('key', {})).rejects.toThrow('Data must be a Buffer object');
await expect(storage.save('key', null)).rejects.toThrow('Data must be a Buffer object');
});
});
describe('Size Management', () => {
test('should track storage size correctly', async () => {
const data1 = Buffer.from('hello');
const data2 = Buffer.from('world');
await storage.save('key1', data1);
await storage.save('key2', data2);
const stats = await storage.getStats();
expect(stats.totalSize).toBe(data1.length + data2.length);
expect(stats.itemCount).toBe(2);
});
test('should enforce size limits', async () => {
const smallStorage = new MemoryBinaryStorage(100); // 100 byte limit
const largeData = Buffer.alloc(150, 0xFF);
await expect(smallStorage.save('large', largeData)).rejects.toThrow('Storage size limit exceeded');
});
test('should update size when replacing existing data', async () => {
const smallData = Buffer.from('small');
const largeData = Buffer.from('much larger data string');
await storage.save('key', smallData);
const stats1 = await storage.getStats();
await storage.save('key', largeData);
const stats2 = await storage.getStats();
expect(stats2.totalSize).toBe(largeData.length);
expect(stats2.itemCount).toBe(1);
expect(stats2.totalSize - stats1.totalSize).toBe(largeData.length - smallData.length);
});
test('should decrease size when deleting data', async () => {
const data = Buffer.from('data to delete');
await storage.save('key', data);
const statsBefore = await storage.getStats();
await storage.delete('key');
const statsAfter = await storage.getStats();
expect(statsAfter.totalSize).toBe(statsBefore.totalSize - data.length);
expect(statsAfter.itemCount).toBe(statsBefore.itemCount - 1);
});
});
describe('Statistics', () => {
test('should provide accurate statistics', async () => {
await storage.save('key1', Buffer.from('data1'));
await storage.save('key2', Buffer.from('data2'));
await storage.save('key3', Buffer.from('data3'));
const stats = await storage.getStats();
expect(stats.type).toBe('memory');
expect(stats.itemCount).toBe(3);
expect(stats.totalSize).toBe(15); // 'data1' + 'data2' + 'data3'
expect(stats.keys).toEqual(expect.arrayContaining(['key1', 'key2', 'key3']));
expect(stats.utilizationPercent).toBeDefined();
expect(typeof stats.utilizationPercent).toBe('number');
});
test('should calculate utilization percentage correctly', async () => {
const storage = new MemoryBinaryStorage(1000);
await storage.save('key', Buffer.alloc(250));
const stats = await storage.getStats();
expect(stats.utilizationPercent).toBe(25);
});
});
describe('Clear Functionality', () => {
test('should clear all data', async () => {
await storage.save('key1', Buffer.from('data1'));
await storage.save('key2', Buffer.from('data2'));
await storage.clear();
expect(await storage.exists('key1')).toBe(false);
expect(await storage.exists('key2')).toBe(false);
const stats = await storage.getStats();
expect(stats.itemCount).toBe(0);
expect(stats.totalSize).toBe(0);
});
});
});
describe('FileSystemBinaryStorage', () => {
let storage;
let tempDir;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'binary-storage-test-'));
storage = new FileSystemBinaryStorage(tempDir);
});
afterEach(async () => {
try {
await fs.rmdir(tempDir, { recursive: true });
} catch (error) {
// Ignore cleanup errors
}
});
describe('Basic Operations', () => {
test('should save and retrieve binary data from file system', async () => {
const key = 'test-file';
const data = Buffer.from('file system test data', 'utf8');
await storage.save(key, data);
const retrieved = await storage.get(key);
expect(retrieved).toEqual(data);
expect(Buffer.isBuffer(retrieved)).toBe(true);
});
test('should return null for non-existent files', async () => {
const result = await storage.get('non-existent-key');
expect(result).toBeNull();
});
test('should check file existence correctly', async () => {
const key = 'existence-test';
const data = Buffer.from('test data');
expect(await storage.exists(key)).toBe(false);
await storage.save(key, data);
expect(await storage.exists(key)).toBe(true);
await storage.delete(key);
expect(await storage.exists(key)).toBe(false);
});
test('should delete files correctly', async () => {
const key = 'delete-test';
const data = Buffer.from('data to delete');
await storage.save(key, data);
expect(await storage.exists(key)).toBe(true);
await storage.delete(key);
expect(await storage.exists(key)).toBe(false);
expect(await storage.get(key)).toBeNull();
});
test('should handle deleting non-existent files gracefully', async () => {
await expect(storage.delete('non-existent')).resolves.not.toThrow();
});
});
describe('File System Integration', () => {
test('should create storage directory if it does not exist', async () => {
const newDir = path.join(tempDir, 'new-storage-dir');
const newStorage = new FileSystemBinaryStorage(newDir);
await newStorage.save('test', Buffer.from('data'));
const dirExists = await fs.access(newDir).then(() => true).catch(() => false);
expect(dirExists).toBe(true);
});
test('should handle concurrent operations safely', async () => {
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(storage.save(`key${i}`, Buffer.from(`data${i}`)));
}
await Promise.all(promises);
for (let i = 0; i < 10; i++) {
const exists = await storage.exists(`key${i}`);
expect(exists).toBe(true);
}
});
test('should persist data across storage instances', async () => {
const key = 'persistence-test';
const data = Buffer.from('persistent data');
await storage.save(key, data);
// Create new storage instance pointing to same directory
const newStorage = new FileSystemBinaryStorage(tempDir);
const retrieved = await newStorage.get(key);
expect(retrieved).toEqual(data);
});
});
describe('Key Safety', () => {
test('should handle special characters in keys safely', async () => {
const specialKeys = [
'key-with-dashes',
'key_with_underscores',
'key.with.dots',
'key with spaces',
'key@with#special$chars',
'key(with)parentheses'
];
for (const key of specialKeys) {
const data = Buffer.from(`data for ${key}`);
await storage.save(key, data);
const retrieved = await storage.get(key);
expect(retrieved).toEqual(data);
}
});
test('should reject dangerous keys', async () => {
const dangerousKeys = [
'../escape',
'path/with/slash',
'path\\with\\backslash',
'key\0with\0null'
];
const data = Buffer.from('test');
for (const key of dangerousKeys) {
await expect(storage.save(key, data)).rejects.toThrow();
}
});
});
describe('Statistics', () => {
test('should provide file system statistics', async () => {
await storage.save('file1', Buffer.from('content1'));
await storage.save('file2', Buffer.from('content2'));
const stats = await storage.getStats();
expect(stats.type).toBe('filesystem');
expect(stats.itemCount).toBe(2);
expect(stats.totalSize).toBeGreaterThan(0);
expect(stats.storageDir).toBe(tempDir);
expect(stats.keys).toEqual(expect.arrayContaining(['file1', 'file2']));
});
test('should handle statistics for empty storage', async () => {
const stats = await storage.getStats();
expect(stats.type).toBe('filesystem');
expect(stats.itemCount).toBe(0);
expect(stats.totalSize).toBe(0);
});
});
});
describe('StorageFactory', () => {
test('should create memory storage by default', () => {
const storage = StorageFactory.createStorage();
expect(storage).toBeInstanceOf(MemoryBinaryStorage);
});
test('should create memory storage with custom config', () => {
const storage = StorageFactory.createStorage({
type: 'memory',
config: { maxSize: 5000 }
});
expect(storage).toBeInstanceOf(MemoryBinaryStorage);
expect(storage.maxSize).toBe(5000);
});
test('should create file system storage', () => {
const storage = StorageFactory.createStorage({
type: 'filesystem',
config: { storageDir: './test-storage' }
});
expect(storage).toBeInstanceOf(FileSystemBinaryStorage);
});
test('should create file system storage with "file" alias', () => {
const storage = StorageFactory.createStorage({ type: 'file' });
expect(storage).toBeInstanceOf(FileSystemBinaryStorage);
});
test('should fall back to memory storage for unknown types', () => {
const storage = StorageFactory.createStorage({ type: 'unknown' });
expect(storage).toBeInstanceOf(MemoryBinaryStorage);
});
test('should create storage from environment variables', () => {
// Test with default environment (no variables set)
const storage = StorageFactory.createFromEnvironment();
expect(storage).toBeInstanceOf(MemoryBinaryStorage);
});
});
describe('Default Storage', () => {
test('should provide a default storage instance', () => {
const storage1 = getDefaultStorage();
const storage2 = getDefaultStorage();
expect(storage1).toBe(storage2); // Should be the same instance (singleton)
expect(storage1).toBeInstanceOf(MemoryBinaryStorage);
});
});
describe('Error Handling', () => {
test('should provide meaningful error messages', async () => {
const storage = new MemoryBinaryStorage();
try {
await storage.save('', Buffer.from('data'));
} catch (error) {
expect(error.message).toContain('Key must be a non-empty string');
}
try {
await storage.save('key', 'not a buffer');
} catch (error) {
expect(error.message).toContain('Data must be a Buffer object');
}
});
test('should handle storage errors gracefully', async () => {
const storage = new FileSystemBinaryStorage('/invalid/path/that/cannot/exist');
await expect(storage.save('key', Buffer.from('data'))).rejects.toThrow();
});
});
describe('Performance Characteristics', () => {
test('memory storage should be fast for small datasets', async () => {
const storage = new MemoryBinaryStorage();
const start = Date.now();
for (let i = 0; i < 100; i++) {
await storage.save(`key${i}`, Buffer.from(`data${i}`));
}
const saveTime = Date.now() - start;
expect(saveTime).toBeLessThan(1000); // Should complete in under 1 second
const readStart = Date.now();
for (let i = 0; i < 100; i++) {
await storage.get(`key${i}`);
}
const readTime = Date.now() - readStart;
expect(readTime).toBeLessThan(500); // Reads should be even faster
});
});
});