289 lines
9.0 KiB
TypeScript
289 lines
9.0 KiB
TypeScript
|
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<string, string>();
|
||
|
|
||
|
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<void>[] = [];
|
||
|
|
||
|
// 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<string | null>[] = [];
|
||
|
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();
|