import { FileSystemStorageAdapter, FileSystemStorageConfig } from '../../../../src/adapters/storage/FileSystemStorageAdapter'; import * as fs from 'fs-extra'; import * as path from 'path'; interface ErrorTestItem { id?: string; name: string; value: number; createdAt?: number; updatedAt?: number; } // Test directory for error handling storage const errorTestRootDir = path.join(process.cwd(), 'test-fs-error-storage'); // Invalid paths for testing const invalidRootDirs = [ '/dev/null/non-existent-directory', // Path inside dev/null '/this/path/should/not/exist/anywhere', // Non-existent deep path '/root/invalid/path', // Path requiring root permissions 'invalid-relative-path' // Relative path (should be absolute) ]; describe('FileSystemStorageAdapter Error Handling Tests', () => { let adapter: FileSystemStorageAdapter; beforeEach(() => { adapter = new FileSystemStorageAdapter(); }); // Clean up test directory after each test afterEach(async () => { await adapter.close(); if (await fs.pathExists(errorTestRootDir)) { await fs.remove(errorTestRootDir); } }); describe('Invalid Path Handling', () => { test('should handle invalid root directory initialization', async () => { for (const invalidDir of invalidRootDirs) { const invalidConfig: FileSystemStorageConfig = { type: 'file', rootDir: invalidDir, fileExtension: 'json' }; try { await adapter.initialize(invalidConfig); // Check if the adapter is available const isAvailable = await adapter.isAvailable(); // For invalid directories, isAvailable should return false expect(isAvailable).toBe(false); } catch (error) { // Initialization failed, which is also acceptable for invalid paths expect(error).toBeDefined(); } } }); test('should handle non-existent namespace gracefully', async () => { // Create a valid adapter const validConfig: FileSystemStorageConfig = { type: 'file', rootDir: errorTestRootDir, fileExtension: 'json' }; await adapter.initialize(validConfig); // Try to get an item from a non-existent namespace const result = await adapter.getById('nonExistentNamespace', 'nonExistentId'); expect(result).toBeNull(); // Try to find items from a non-existent namespace const findResult = await adapter.find('nonExistentNamespace'); expect(findResult.data).toEqual([]); expect(findResult.total).toBe(0); // Try to delete from non-existent namespace const deleteResult = await adapter.delete('nonExistentNamespace', 'nonExistentId'); expect(deleteResult).toBe(false); }); test('should handle path traversal attempts by sanitizing paths', async () => { // Create a valid adapter const validConfig: FileSystemStorageConfig = { type: 'file', rootDir: errorTestRootDir, fileExtension: 'json' }; await adapter.initialize(validConfig); // Create a legitimate item first const testItem: Omit = { name: 'Legitimate Item', value: 42 }; const createdItem = await adapter.create('legitimateNs', testItem); expect(createdItem).toBeDefined(); expect(createdItem.id).toBeDefined(); // Try path traversal in namespace name - should succeed but sanitize path const maliciousNamespace = '../../../../../etc/passwd'; const sanitizedNamespaceItem = await adapter.create(maliciousNamespace, testItem); expect(sanitizedNamespaceItem).toBeDefined(); // Try path traversal in ID - should succeed but sanitize ID const maliciousId = '../../../../../etc/passwd'; const sanitizedIdItem = await adapter.create('legitimateNs', { ...testItem, id: maliciousId }); expect(sanitizedIdItem).toBeDefined(); expect(sanitizedIdItem.id).not.toBe(maliciousId); // Should be sanitized }); test('should sanitize malicious namespaces and IDs', async () => { // Create a valid adapter const validConfig: FileSystemStorageConfig = { type: 'file', rootDir: errorTestRootDir, fileExtension: 'json' }; await adapter.initialize(validConfig); // Test with special characters in namespace const specialNamespace = 'namespace/with/slashes\tand\nnewlines:and:colons*and*stars'; const createdItem = await adapter.create(specialNamespace, { name: 'Test Item', value: 42 }); expect(createdItem).toBeDefined(); // Verify the namespace was sanitized by checking the directory structure const expectedNamespaceDir = path.join(errorTestRootDir, 'namespace_with_slashes_and_newlines_and_colons_and_stars'); expect(await fs.pathExists(expectedNamespaceDir)).toBe(true); // Test with special characters in ID const specialId = 'id/with/slashes\tand\nnewlines:and:colons*and*stars'; const createdItemWithSpecialId = await adapter.create('normalNs', { name: 'Test Item with Special ID', value: 42, id: specialId }); expect(createdItemWithSpecialId).toBeDefined(); // Verify the ID was sanitized by checking the file exists const expectedFile = path.join(errorTestRootDir, 'normalNs', 'id_with_slashes_and_newlines_and_colons_and_stars.json'); expect(await fs.pathExists(expectedFile)).toBe(true); }); }); describe('Permission Error Handling', () => { test('should handle permission denied errors gracefully', async () => { // Create a valid adapter first const validConfig: FileSystemStorageConfig = { type: 'file', rootDir: errorTestRootDir, fileExtension: 'json' }; await adapter.initialize(validConfig); // Create a file and then make it read-only const testItem = await adapter.create('testNs', { name: 'Test Item', value: 42 }); expect(testItem).toBeDefined(); // Get the file path const filePath = path.join(errorTestRootDir, 'testNs', `${testItem.id}.json`); expect(await fs.pathExists(filePath)).toBe(true); // Make the file read-only await fs.chmod(filePath, 0o444); // Read-only permissions // Try to update the read-only file - should fail gracefully const updateResult = await adapter.update('testNs', testItem.id!, { value: 99 }); expect(updateResult).toBeNull(); // Try to delete the read-only file - on Unix/Linux, this succeeds because delete permission depends on directory, not file const deleteResult = await adapter.delete('testNs', testItem.id!); // We expect true because the delete operation should succeed due to directory permissions expect(deleteResult).toBe(true); // Verify the file is actually deleted expect(await fs.pathExists(filePath)).toBe(false); // No need to restore permissions - file has been deleted }); test('should handle directory permission errors', async () => { // Create a valid adapter const validConfig: FileSystemStorageConfig = { type: 'file', rootDir: errorTestRootDir, fileExtension: 'json' }; await adapter.initialize(validConfig); // Create a namespace directory and make it read-only const testItem = await adapter.create('restrictedNs', { name: 'Restricted Item', value: 42 }); expect(testItem).toBeDefined(); // Get the directory path const dirPath = path.join(errorTestRootDir, 'restrictedNs'); expect(await fs.pathExists(dirPath)).toBe(true); try { // Make the directory read-only await fs.chmod(dirPath, 0o555); // Read-execute permissions only // Try to create a new item in the read-only directory // This should fail with EACCES error, but our code should handle it gracefully try { const createResult = await adapter.create('restrictedNs', { name: 'New Item', value: 99 }); // Should either succeed (if directory permissions allow file creation) or fail gracefully // This depends on the OS and file system, so we accept either outcome expect(createResult === null || createResult?.id).toBeDefined(); } catch (error) { // The create operation might throw an error, which is also acceptable expect(error).toBeDefined(); } } finally { // Always restore write permissions, even if the test fails await fs.chmod(dirPath, 0o755); } }); }); describe('File System Error Handling', () => { let validConfig: FileSystemStorageConfig; beforeEach(() => { validConfig = { type: 'file', rootDir: errorTestRootDir, fileExtension: 'json' }; }); test('should handle non-existent files gracefully', async () => { await adapter.initialize(validConfig); // Try to get a non-existent file const getResult = await adapter.getById('testNs', 'nonExistentId'); expect(getResult).toBeNull(); // Try to update a non-existent file const updateResult = await adapter.update('testNs', 'nonExistentId', { value: 99 }); expect(updateResult).toBeNull(); // Try to delete a non-existent file const deleteResult = await adapter.delete('testNs', 'nonExistentId'); expect(deleteResult).toBe(false); }); test('should handle corrupted JSON files', async () => { await adapter.initialize(validConfig); // Create a legitimate item first const testItem = await adapter.create('corruptNs', { name: 'Legitimate Item', value: 42 }); expect(testItem).toBeDefined(); // Get the file path const filePath = path.join(errorTestRootDir, 'corruptNs', `${testItem.id}.json`); expect(await fs.pathExists(filePath)).toBe(true); // Corrupt the file by writing invalid JSON await fs.writeFile(filePath, '{ invalid json content without closing bracket', 'utf8'); // Try to get the corrupted file - should return null gracefully const getResult = await adapter.getById('corruptNs', testItem.id!); expect(getResult).toBeNull(); // Try to find items in the namespace - should skip corrupted file const findResult = await adapter.find('corruptNs'); expect(findResult.data).toEqual([]); expect(findResult.total).toBe(0); // Try to update the corrupted file - should return null const updateResult = await adapter.update('corruptNs', testItem.id!, { value: 99 }); expect(updateResult).toBeNull(); }); test('should handle disk full scenarios (simulated with large content)', async () => { await adapter.initialize(validConfig); // Test with large content that might cause issues but won't crash the process const largeContent = 'x'.repeat(10 * 1024 * 1024); // 10MB - manageable size for testing // This should either succeed or fail gracefully depending on system resources try { const largeItem = await adapter.create('largeNs', { name: 'Very Large Item', content: largeContent, value: 0 }); // If it succeeds, that's fine too if (largeItem) { expect(largeItem.id).toBeDefined(); } } catch (error) { // If it fails, it should fail gracefully expect(error).toBeDefined(); } }); test('should handle isAvailable check with invalid directory', async () => { await adapter.initialize(validConfig); expect(await adapter.isAvailable()).toBe(true); // Close and reinitialize with invalid directory await adapter.close(); const invalidConfig: FileSystemStorageConfig = { type: 'file', rootDir: '/invalid/non/existent/path', fileExtension: 'json' }; // Initialize with invalid directory (may succeed but isAvailable should return false) try { await adapter.initialize(invalidConfig); expect(await adapter.isAvailable()).toBe(false); } catch (error) { // Initialization failed, which is also acceptable expect(error).toBeDefined(); } }); }); describe('Edge Case Error Scenarios', () => { test('should handle empty file contents', async () => { await adapter.initialize({ type: 'file', rootDir: errorTestRootDir, fileExtension: 'json' }); // Create a legitimate item const testItem = await adapter.create('emptyNs', { name: 'Test Item', value: 42 }); expect(testItem).toBeDefined(); // Get the file path const filePath = path.join(errorTestRootDir, 'emptyNs', `${testItem.id}.json`); expect(await fs.pathExists(filePath)).toBe(true); // Write empty content to the file await fs.writeFile(filePath, '', 'utf8'); // Try to get the empty file - should return null const getResult = await adapter.getById('emptyNs', testItem.id!); expect(getResult).toBeNull(); // Try to find items - should skip empty file const findResult = await adapter.find('emptyNs'); expect(findResult.data).toEqual([]); }); test('should handle special file types', async () => { await adapter.initialize({ type: 'file', rootDir: errorTestRootDir, fileExtension: 'json' }); // Create a directory that might be mistaken for a file const specialDirPath = path.join(errorTestRootDir, 'testNs', 'special-file.json'); await fs.ensureDir(specialDirPath); // Try to get this directory as if it were a file - should return null const getResult = await adapter.getById('testNs', 'special-file'); expect(getResult).toBeNull(); // Try to find items - should handle the directory gracefully const findResult = await adapter.find('testNs'); expect(Array.isArray(findResult.data)).toBe(true); }); test('should handle multiple concurrent operations without crashing', async () => { await adapter.initialize({ type: 'file', rootDir: errorTestRootDir, fileExtension: 'json' }); // Run multiple concurrent operations that should all succeed const operations = []; for (let i = 0; i < 20; i++) { operations.push( adapter.create('concurrentNs', { name: `Test Item ${i}`, value: i }) ); } // All operations should complete without crashing const results = await Promise.all(operations); // Check that all operations returned valid results results.forEach(result => { expect(result).toBeDefined(); expect(result.id).toBeDefined(); }); expect(results.length).toBe(20); }); }); });