import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as plugins from '../ts/plugins.js'; import * as paths from '../ts/paths.js'; import { StorageManager } from '../ts/storage/classes.storagemanager.js'; import { promises as fs } from 'fs'; import * as path from 'path'; // Test data const testData = { string: 'Hello, World!', json: { name: 'test', value: 42, nested: { data: true } }, largeString: 'x'.repeat(10000) }; tap.test('Storage Manager - Memory Backend', async () => { // Create StorageManager without config (defaults to memory) const storage = new StorageManager(); // Test basic get/set await storage.set('/test/key', testData.string); const value = await storage.get('/test/key'); expect(value).toEqual(testData.string); // Test JSON helpers await storage.setJSON('/test/json', testData.json); const jsonValue = await storage.getJSON('/test/json'); expect(jsonValue).toEqual(testData.json); // Test exists expect(await storage.exists('/test/key')).toEqual(true); expect(await storage.exists('/nonexistent')).toEqual(false); // Test delete await storage.delete('/test/key'); expect(await storage.exists('/test/key')).toEqual(false); // Test list await storage.set('/items/1', 'one'); await storage.set('/items/2', 'two'); await storage.set('/other/3', 'three'); const items = await storage.list('/items'); expect(items.length).toEqual(2); expect(items).toContain('/items/1'); expect(items).toContain('/items/2'); // Verify memory backend expect(storage.getBackend()).toEqual('memory'); }); tap.test('Storage Manager - Filesystem Backend', async () => { const testDir = path.join(paths.dataDir, '.test-storage'); // Clean up test directory if it exists try { await fs.rm(testDir, { recursive: true, force: true }); } catch {} // Create StorageManager with filesystem path const storage = new StorageManager({ fsPath: testDir }); // Test basic operations await storage.set('/test/file', testData.string); const value = await storage.get('/test/file'); expect(value).toEqual(testData.string); // Verify file exists on disk const filePath = path.join(testDir, 'test', 'file'); const fileExists = await fs.access(filePath).then(() => true).catch(() => false); expect(fileExists).toEqual(true); // Test atomic writes (temp file should not exist) const tempPath = filePath + '.tmp'; const tempExists = await fs.access(tempPath).then(() => true).catch(() => false); expect(tempExists).toEqual(false); // Test nested paths await storage.set('/deeply/nested/path/to/file', testData.largeString); const nestedValue = await storage.get('/deeply/nested/path/to/file'); expect(nestedValue).toEqual(testData.largeString); // Test list with filesystem await storage.set('/fs/items/a', 'alpha'); await storage.set('/fs/items/b', 'beta'); await storage.set('/fs/other/c', 'gamma'); // Filesystem backend now properly supports list const fsItems = await storage.list('/fs/items'); expect(fsItems.length).toEqual(2); // Should find both items // Clean up await fs.rm(testDir, { recursive: true, force: true }); }); tap.test('Storage Manager - Custom Function Backend', async () => { // Create in-memory storage for custom functions const customStore = new Map(); const storage = new StorageManager({ readFunction: async (key: string) => { return customStore.get(key) || null; }, writeFunction: async (key: string, value: string) => { customStore.set(key, value); } }); // Test basic operations await storage.set('/custom/key', testData.string); expect(customStore.has('/custom/key')).toEqual(true); const value = await storage.get('/custom/key'); expect(value).toEqual(testData.string); // Test that delete sets empty value (as per implementation) await storage.delete('/custom/key'); expect(customStore.get('/custom/key')).toEqual(''); // Verify custom backend (filesystem is implemented as custom backend internally) expect(storage.getBackend()).toEqual('custom'); }); tap.test('Storage Manager - Key Validation', async () => { const storage = new StorageManager(); // Test key normalization await storage.set('test/key', 'value1'); // Missing leading slash const value1 = await storage.get('/test/key'); expect(value1).toEqual('value1'); // Test dangerous path elements are removed await storage.set('/test/../danger/key', 'value2'); const value2 = await storage.get('/test/danger/key'); // .. is removed, not the whole path segment expect(value2).toEqual('value2'); // Test multiple slashes are normalized await storage.set('/test///multiple////slashes', 'value3'); const value3 = await storage.get('/test/multiple/slashes'); expect(value3).toEqual('value3'); // Test invalid keys throw errors let emptyKeyError: Error | null = null; try { await storage.set('', 'value'); } catch (error) { emptyKeyError = error as Error; } expect(emptyKeyError).toBeTruthy(); expect(emptyKeyError?.message).toEqual('Storage key must be a non-empty string'); let nullKeyError: Error | null = null; try { await storage.set(null as any, 'value'); } catch (error) { nullKeyError = error as Error; } expect(nullKeyError).toBeTruthy(); expect(nullKeyError?.message).toEqual('Storage key must be a non-empty string'); }); tap.test('Storage Manager - Concurrent Access', async () => { const storage = new StorageManager(); const promises: Promise[] = []; // Simulate concurrent writes for (let i = 0; i < 100; i++) { promises.push(storage.set(`/concurrent/key${i}`, `value${i}`)); } await Promise.all(promises); // Verify all writes succeeded for (let i = 0; i < 100; i++) { const value = await storage.get(`/concurrent/key${i}`); expect(value).toEqual(`value${i}`); } // Test concurrent reads const readPromises: Promise[] = []; for (let i = 0; i < 100; i++) { readPromises.push(storage.get(`/concurrent/key${i}`)); } const results = await Promise.all(readPromises); for (let i = 0; i < 100; i++) { expect(results[i]).toEqual(`value${i}`); } }); tap.test('Storage Manager - Backend Priority', async () => { const testDir = path.join(paths.dataDir, '.test-storage-priority'); // Test that custom functions take priority over fsPath let warningLogged = false; const originalWarn = console.warn; console.warn = (message: string) => { if (message.includes('Using custom read/write functions')) { warningLogged = true; } }; const storage = new StorageManager({ fsPath: testDir, readFunction: async () => 'custom-value', writeFunction: async () => {} }); console.warn = originalWarn; expect(warningLogged).toEqual(true); expect(storage.getBackend()).toEqual('custom'); // Custom functions take priority // Clean up try { await fs.rm(testDir, { recursive: true, force: true }); } catch {} }); tap.test('Storage Manager - Error Handling', async () => { // Test filesystem errors const storage = new StorageManager({ readFunction: async () => { throw new Error('Read error'); }, writeFunction: async () => { throw new Error('Write error'); } }); // Read errors should return null const value = await storage.get('/error/key'); expect(value).toEqual(null); // Write errors should propagate let writeError: Error | null = null; try { await storage.set('/error/key', 'value'); } catch (error) { writeError = error as Error; } expect(writeError).toBeTruthy(); expect(writeError?.message).toEqual('Write error'); // Test JSON parse errors const jsonStorage = new StorageManager({ readFunction: async () => 'invalid json', writeFunction: async () => {} }); // Test JSON parse errors let jsonError: Error | null = null; try { await jsonStorage.getJSON('/invalid/json'); } catch (error) { jsonError = error as Error; } expect(jsonError).toBeTruthy(); expect(jsonError?.message).toContain('JSON'); }); tap.test('Storage Manager - List Operations', async () => { const storage = new StorageManager(); // Populate storage with hierarchical data await storage.set('/app/config/database', 'db-config'); await storage.set('/app/config/cache', 'cache-config'); await storage.set('/app/data/users/1', 'user1'); await storage.set('/app/data/users/2', 'user2'); await storage.set('/app/logs/error.log', 'errors'); // List root const rootItems = await storage.list('/'); expect(rootItems.length).toBeGreaterThanOrEqual(5); // List specific paths const configItems = await storage.list('/app/config'); expect(configItems.length).toEqual(2); expect(configItems).toContain('/app/config/database'); expect(configItems).toContain('/app/config/cache'); const userItems = await storage.list('/app/data/users'); expect(userItems.length).toEqual(2); // List non-existent path const emptyList = await storage.list('/nonexistent/path'); expect(emptyList.length).toEqual(0); }); export default tap.start();