UNPKG

@julesl23/s5js

Version:

Enhanced TypeScript SDK for S5 decentralized storage with path-based API, media processing, and directory utilities

325 lines 14.1 kB
// test/fs/utils/walker.test.ts import { describe, it, expect, beforeEach } from 'vitest'; import { DirectoryWalker } from '../../../src/fs/utils/walker.js'; import { FS5 } from '../../../src/fs/fs5.js'; import { webcrypto } from 'crypto'; // Mock S5 API class MockS5API { storage = new Map(); registry = new Map(); crypto = { hashBlake3Sync: (data) => { // Simple mock hash - just use first 32 bytes or pad const hash = new Uint8Array(32); for (let i = 0; i < Math.min(data.length, 32); i++) { hash[i] = data[i]; } return hash; }, hashBlake3Blob: async (blob) => { const data = new Uint8Array(await blob.arrayBuffer()); return MockS5API.prototype.crypto.hashBlake3Sync(data); }, generateSecureRandomBytes: (size) => { const bytes = new Uint8Array(size); webcrypto.getRandomValues(bytes); return bytes; }, newKeyPairEd25519: async (seed) => { return { publicKey: seed, privateKey: seed }; }, encryptXChaCha20Poly1305: async (key, nonce, plaintext) => { // Simple mock - just return plaintext with 16-byte tag return new Uint8Array([...plaintext, ...new Uint8Array(16)]); }, decryptXChaCha20Poly1305: async (key, nonce, ciphertext) => { // Simple mock - remove tag return ciphertext.subarray(0, ciphertext.length - 16); }, signRawRegistryEntry: async (keyPair, entry) => { // Simple mock signature return new Uint8Array(64); }, signEd25519: async (keyPair, message) => { // Simple mock signature return new Uint8Array(64); } }; async uploadBlob(blob) { const data = new Uint8Array(await blob.arrayBuffer()); const hash = this.crypto.hashBlake3Sync(data); const key = Buffer.from(hash).toString('hex'); this.storage.set(key, data); return { hash: new Uint8Array([0x1e, ...hash]), size: blob.size }; } async downloadBlobAsBytes(hash) { // If hash has multihash prefix, remove it const actualHash = hash[0] === 0x1e ? hash.slice(1) : hash; const key = Buffer.from(actualHash).toString('hex'); const data = this.storage.get(key); if (!data) throw new Error("Blob not found"); return data; } async registryGet(publicKey) { const key = Buffer.from(publicKey).toString('hex'); const entry = this.registry.get(key); // Return proper registry entry structure if (!entry) { return { exists: false, data: null, revision: 0 }; } return { exists: true, data: entry.data, revision: entry.revision || 1, signature: entry.signature || new Uint8Array(64) }; } async registrySet(entry) { const key = Buffer.from(entry.pk).toString('hex'); this.registry.set(key, { data: entry.data, revision: entry.revision || 1, signature: entry.signature || new Uint8Array(64) }); } registryListen(publicKey) { // Mock implementation - return empty async iterator return (async function* () { // Empty async generator })(); } async registryListenOnEntry(publicKey, callback) { // Mock implementation - just return a no-op unsubscribe function return () => { }; } } class MockIdentity { fsRootKey = new Uint8Array(32).fill(1); // Add required properties for proper identity initialization get publicKey() { return new Uint8Array(32).fill(2); } get privateKey() { return new Uint8Array(64).fill(3); } // For registry operations keyPair = { publicKey: new Uint8Array(32).fill(2), privateKey: new Uint8Array(64).fill(3) }; } describe('DirectoryWalker', () => { let fs; let api; let identity; beforeEach(async () => { api = new MockS5API(); identity = new MockIdentity(); fs = new FS5(api, identity); try { // Initialize the filesystem with root directories await fs.ensureIdentityInitialized(); // Create test directory structure await fs.put('home/test/file1.txt', 'content1'); await fs.put('home/test/file2.txt', 'content2'); await fs.put('home/test/dir1/file3.txt', 'content3'); await fs.put('home/test/dir1/file4.txt', 'content4'); await fs.put('home/test/dir1/subdir/file5.txt', 'content5'); await fs.put('home/test/dir2/file6.txt', 'content6'); await fs.put('home/test/empty/.gitkeep', ''); } catch (error) { // Silently handle initialization errors // Tests will fail appropriately if fs is not properly initialized } }); describe('walk async iterator', () => { it('should walk all files and directories recursively by default', async () => { const walker = new DirectoryWalker(fs, 'home/test'); const results = []; for await (const item of walker.walk()) { results.push(item); } // Should include all files and directories expect(results.length).toBeGreaterThanOrEqual(9); // At least 6 files + 3 directories // Check for specific items const paths = results.map(r => r.path); expect(paths).toContain('home/test/file1.txt'); expect(paths).toContain('home/test/dir1/file3.txt'); expect(paths).toContain('home/test/dir1/subdir/file5.txt'); expect(paths).toContain('home/test/dir1'); expect(paths).toContain('home/test/dir1/subdir'); }); it('should respect includeFiles option', async () => { const walker = new DirectoryWalker(fs, 'home/test'); const results = []; for await (const item of walker.walk({ includeFiles: false })) { results.push(item); } // Should only include directories expect(results.every(r => r.type === 'directory')).toBe(true); expect(results.length).toBeGreaterThanOrEqual(3); // dir1, dir1/subdir, dir2 }); it('should respect includeDirectories option', async () => { const walker = new DirectoryWalker(fs, 'home/test'); const results = []; for await (const item of walker.walk({ includeDirectories: false })) { results.push(item); } // Should only include files expect(results.every(r => r.type === 'file')).toBe(true); expect(results.length).toBe(7); // All files including .gitkeep }); it('should apply custom filter function', async () => { const walker = new DirectoryWalker(fs, 'home/test'); const results = []; const filter = (name, type) => { // Only include .txt files and directories return type === 'directory' || name.endsWith('.txt'); }; for await (const item of walker.walk({ filter })) { results.push(item); } // Should not include .gitkeep const fileNames = results.filter(r => r.type === 'file').map(r => r.name); expect(fileNames).not.toContain('.gitkeep'); expect(fileNames.every(name => name.endsWith('.txt'))).toBe(true); }); it('should respect maxDepth option', async () => { const walker = new DirectoryWalker(fs, 'home/test'); const results = []; for await (const item of walker.walk({ maxDepth: 1 })) { results.push(item); } // Should not include deeply nested items const paths = results.map(r => r.path); expect(paths).not.toContain('home/test/dir1/subdir/file5.txt'); expect(paths).not.toContain('home/test/dir1/subdir'); // Should include depth 0 and 1 items expect(paths).toContain('home/test/file1.txt'); expect(paths).toContain('home/test/dir1'); expect(paths).toContain('home/test/dir1/file3.txt'); }); it('should handle non-recursive walking', async () => { const walker = new DirectoryWalker(fs, 'home/test'); const results = []; for await (const item of walker.walk({ recursive: false })) { results.push(item); } // Should only include direct children const paths = results.map(r => r.path); expect(paths).toContain('home/test/file1.txt'); expect(paths).toContain('home/test/file2.txt'); expect(paths).toContain('home/test/dir1'); expect(paths).toContain('home/test/dir2'); // Should not include nested items expect(paths).not.toContain('home/test/dir1/file3.txt'); expect(paths).not.toContain('home/test/dir1/subdir'); }); it('should support cursor resume', async () => { const walker = new DirectoryWalker(fs, 'home/test'); // First, get some items and a cursor const firstBatch = []; let lastCursor; for await (const item of walker.walk({ maxDepth: 1 })) { firstBatch.push(item); lastCursor = item.cursor; if (firstBatch.length >= 3) break; // Stop after 3 items } expect(lastCursor).toBeDefined(); // Resume from cursor const resumedBatch = []; for await (const item of walker.walk({ cursor: lastCursor, maxDepth: 1 })) { resumedBatch.push(item); } // Should not include items from first batch const firstPaths = firstBatch.map(r => r.path); const resumedPaths = resumedBatch.map(r => r.path); expect(firstPaths.some(path => resumedPaths.includes(path))).toBe(false); }); it('should include depth information', async () => { const walker = new DirectoryWalker(fs, 'home/test'); const results = []; for await (const item of walker.walk()) { results.push(item); } // Check depth values const file1 = results.find(r => r.path === 'home/test/file1.txt'); expect(file1?.depth).toBe(0); const dir1 = results.find(r => r.path === 'home/test/dir1'); expect(dir1?.depth).toBe(0); const file3 = results.find(r => r.path === 'home/test/dir1/file3.txt'); expect(file3?.depth).toBe(1); const subdir = results.find(r => r.path === 'home/test/dir1/subdir'); expect(subdir?.depth).toBe(1); const file5 = results.find(r => r.path === 'home/test/dir1/subdir/file5.txt'); expect(file5?.depth).toBe(2); }); it('should handle empty directories', async () => { const walker = new DirectoryWalker(fs, 'home/test/empty'); const results = []; for await (const item of walker.walk()) { results.push(item); } // Should only contain .gitkeep expect(results.length).toBe(1); expect(results[0].name).toBe('.gitkeep'); }); it('should handle non-existent directories gracefully', async () => { const walker = new DirectoryWalker(fs, 'home/non-existent'); const results = []; try { for await (const item of walker.walk()) { results.push(item); } } catch (error) { // Should handle gracefully expect(error).toBeDefined(); } expect(results.length).toBe(0); }); }); describe('count method', () => { it('should count all files and directories with total size', async () => { const walker = new DirectoryWalker(fs, 'home/test'); const stats = await walker.count(); expect(stats.files).toBe(7); expect(stats.directories).toBeGreaterThanOrEqual(3); expect(stats.totalSize).toBeGreaterThan(0); }); it('should count with filter applied', async () => { const walker = new DirectoryWalker(fs, 'home/test'); const stats = await walker.count({ filter: (name) => name.endsWith('.txt') }); expect(stats.files).toBe(6); // Excluding .gitkeep expect(stats.directories).toBe(0); // Filter excludes directories }); it('should count non-recursively', async () => { const walker = new DirectoryWalker(fs, 'home/test'); const stats = await walker.count({ recursive: false }); expect(stats.files).toBe(2); // file1.txt, file2.txt expect(stats.directories).toBe(2); // dir1, dir2 }); it('should count with maxDepth', async () => { const walker = new DirectoryWalker(fs, 'home/test'); const stats = await walker.count({ maxDepth: 1 }); expect(stats.files).toBe(6); // All except file5.txt in subdir expect(stats.directories).toBe(2); // dir1, dir2 (not subdir) }); it('should handle empty directory count', async () => { const walker = new DirectoryWalker(fs, 'home/test/empty'); const stats = await walker.count(); expect(stats.files).toBe(1); // .gitkeep expect(stats.directories).toBe(0); expect(stats.totalSize).toBe(0); // .gitkeep is empty }); }); }); //# sourceMappingURL=walker.test.js.map