Files
smartfs/test/test.node.provider.ts

559 lines
21 KiB
TypeScript

/**
* Tests for Node.js provider
*/
import * as path from 'path';
import * as fs from 'fs/promises';
import { tap, expect } from '@push.rocks/tapbundle';
import { SmartFs, SmartFsProviderNode } from '../ts/index.js';
// Create temp directory for tests
const tempDir = path.join(process.cwd(), '.nogit', 'test-temp');
// Create test instance
const nodeProvider = new SmartFsProviderNode();
const smartFs = new SmartFs(nodeProvider);
tap.preTask('setup temp directory', async () => {
await fs.mkdir(tempDir, { recursive: true });
});
tap.test('should create SmartFS instance with Node provider', async () => {
expect(smartFs).toBeInstanceOf(SmartFs);
expect(smartFs.getProviderName()).toEqual('node');
});
tap.test('should write and read a file', async () => {
const filePath = path.join(tempDir, 'test.txt');
await smartFs.file(filePath).write('Hello, World!');
const content = await smartFs.file(filePath).encoding('utf8').read();
expect(content).toEqual('Hello, World!');
});
tap.test('should write atomically', async () => {
const filePath = path.join(tempDir, 'atomic.txt');
await smartFs.file(filePath).atomic().write('Atomic write test');
const content = await smartFs.file(filePath).encoding('utf8').read();
expect(content).toEqual('Atomic write test');
});
tap.test('should check if file exists', async () => {
const filePath = path.join(tempDir, 'exists-test.txt');
await smartFs.file(filePath).write('exists');
const exists = await smartFs.file(filePath).exists();
expect(exists).toEqual(true);
const notExists = await smartFs.file(path.join(tempDir, 'nonexistent.txt')).exists();
expect(notExists).toEqual(false);
});
tap.test('should get file stats', async () => {
const filePath = path.join(tempDir, 'stats-test.txt');
await smartFs.file(filePath).write('stats test');
const stats = await smartFs.file(filePath).stat();
expect(stats).toHaveProperty('size');
expect(stats).toHaveProperty('mtime');
expect(stats).toHaveProperty('birthtime');
expect(stats.isFile).toEqual(true);
expect(stats.isDirectory).toEqual(false);
expect(stats.size).toBeGreaterThan(0);
});
tap.test('should append to a file', async () => {
const filePath = path.join(tempDir, 'append-test.txt');
await smartFs.file(filePath).write('Hello');
await smartFs.file(filePath).append(' World!');
const content = await smartFs.file(filePath).encoding('utf8').read();
expect(content).toEqual('Hello World!');
});
tap.test('should delete a file', async () => {
const filePath = path.join(tempDir, 'delete-test.txt');
await smartFs.file(filePath).write('to be deleted');
await smartFs.file(filePath).delete();
const exists = await smartFs.file(filePath).exists();
expect(exists).toEqual(false);
});
tap.test('should copy a file', async () => {
const sourcePath = path.join(tempDir, 'copy-source.txt');
const destPath = path.join(tempDir, 'copy-dest.txt');
await smartFs.file(sourcePath).write('copy me');
await smartFs.file(sourcePath).copy(destPath);
const sourceContent = await smartFs.file(sourcePath).encoding('utf8').read();
const destContent = await smartFs.file(destPath).encoding('utf8').read();
expect(sourceContent).toEqual('copy me');
expect(destContent).toEqual('copy me');
});
tap.test('should move a file', async () => {
const sourcePath = path.join(tempDir, 'move-source.txt');
const destPath = path.join(tempDir, 'move-dest.txt');
await smartFs.file(sourcePath).write('move me');
await smartFs.file(sourcePath).move(destPath);
const sourceExists = await smartFs.file(sourcePath).exists();
const destContent = await smartFs.file(destPath).encoding('utf8').read();
expect(sourceExists).toEqual(false);
expect(destContent).toEqual('move me');
});
tap.test('should create a directory', async () => {
const dirPath = path.join(tempDir, 'test-dir');
await smartFs.directory(dirPath).create();
const exists = await smartFs.directory(dirPath).exists();
expect(exists).toEqual(true);
});
tap.test('should create nested directories recursively', async () => {
const dirPath = path.join(tempDir, 'nested', 'deep', 'path');
await smartFs.directory(dirPath).recursive().create();
const exists = await smartFs.directory(dirPath).exists();
expect(exists).toEqual(true);
});
tap.test('should list directory contents', async () => {
const dirPath = path.join(tempDir, 'list-test');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'file1.txt')).write('file1');
await smartFs.file(path.join(dirPath, 'file2.txt')).write('file2');
await smartFs.directory(path.join(dirPath, 'subdir')).create();
const entries = await smartFs.directory(dirPath).list();
expect(entries).toHaveLength(3);
const names = entries.map((e) => e.name).sort();
expect(names).toEqual(['file1.txt', 'file2.txt', 'subdir']);
});
tap.test('should list directory contents recursively', async () => {
const dirPath = path.join(tempDir, 'recursive-test');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'file1.txt')).write('file1');
await smartFs.directory(path.join(dirPath, 'subdir')).create();
await smartFs.file(path.join(dirPath, 'subdir', 'file2.txt')).write('file2');
const entries = await smartFs.directory(dirPath).recursive().list();
expect(entries.length).toBeGreaterThanOrEqual(3);
});
tap.test('should filter directory listings with RegExp', async () => {
const dirPath = path.join(tempDir, 'filter-test');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'file1.ts')).write('ts file');
await smartFs.file(path.join(dirPath, 'file2.js')).write('js file');
await smartFs.file(path.join(dirPath, 'file3.ts')).write('ts file');
const entries = await smartFs.directory(dirPath).filter(/\.ts$/).list();
expect(entries).toHaveLength(2);
expect(entries.every((e) => e.name.endsWith('.ts'))).toEqual(true);
});
tap.test('should delete a directory recursively', async () => {
const dirPath = path.join(tempDir, 'delete-dir-test');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'file.txt')).write('file');
await smartFs.directory(path.join(dirPath, 'subdir')).create();
await smartFs.directory(dirPath).recursive().delete();
const exists = await smartFs.directory(dirPath).exists();
expect(exists).toEqual(false);
});
tap.test('should handle file streams', async () => {
const filePath = path.join(tempDir, 'stream-test.txt');
const testData = 'Stream test data with some content';
await smartFs.file(filePath).write(testData);
const readStream = await smartFs.file(filePath).readStream();
const chunks: Uint8Array[] = [];
const reader = readStream.getReader();
let done = false;
while (!done) {
const result = await reader.read();
done = result.done;
if (result.value) {
chunks.push(result.value);
}
}
const buffer = Buffer.concat(chunks.map((c) => Buffer.from(c)));
const content = buffer.toString('utf8');
expect(content).toEqual(testData);
});
tap.test('should write file streams', async () => {
const filePath = path.join(tempDir, 'write-stream-test.txt');
const testData = 'Writing via stream';
const buffer = Buffer.from(testData);
const writeStream = await smartFs.file(filePath).writeStream();
const writer = writeStream.getWriter();
await writer.write(new Uint8Array(buffer));
await writer.close();
const content = await smartFs.file(filePath).encoding('utf8').read();
expect(content).toEqual(testData);
});
tap.test('should execute transactions', async () => {
const tx = smartFs.transaction();
const file1Path = path.join(tempDir, 'tx-file1.txt');
const file2Path = path.join(tempDir, 'tx-file2.txt');
const file3Path = path.join(tempDir, 'tx-file3.txt');
await tx
.file(file1Path)
.write('transaction file 1')
.file(file2Path)
.write('transaction file 2')
.file(file3Path)
.write('transaction file 3')
.commit();
const content1 = await smartFs.file(file1Path).encoding('utf8').read();
const content2 = await smartFs.file(file2Path).encoding('utf8').read();
const content3 = await smartFs.file(file3Path).encoding('utf8').read();
expect(content1).toEqual('transaction file 1');
expect(content2).toEqual('transaction file 2');
expect(content3).toEqual('transaction file 3');
});
tap.test('should handle file watching', async () => {
const filePath = path.join(tempDir, 'watch-test.txt');
await smartFs.file(filePath).write('initial');
return new Promise<void>(async (resolve) => {
const watcher = await smartFs
.watch(filePath)
.onChange(async (event) => {
expect(event.type).toEqual('change');
await watcher.stop();
resolve();
})
.start();
// Wait a bit for watcher to be ready
setTimeout(async () => {
await smartFs.file(filePath).write('changed');
}, 100);
});
});
// --- treeHash tests ---
tap.test('should compute treeHash for a directory', async () => {
const dirPath = path.join(tempDir, 'hash-test');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'file1.txt')).write('content1');
await smartFs.file(path.join(dirPath, 'file2.txt')).write('content2');
const hash = await smartFs.directory(dirPath).treeHash();
expect(hash).toBeTruthy();
expect(typeof hash).toEqual('string');
expect(hash.length).toEqual(64); // SHA-256 produces 64 hex chars
});
tap.test('treeHash should be deterministic (same content = same hash)', async () => {
const dirPath = path.join(tempDir, 'hash-deterministic');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'a.txt')).write('aaa');
await smartFs.file(path.join(dirPath, 'b.txt')).write('bbb');
const hash1 = await smartFs.directory(dirPath).treeHash();
const hash2 = await smartFs.directory(dirPath).treeHash();
expect(hash1).toEqual(hash2);
});
tap.test('treeHash should change when file content changes', async () => {
const dirPath = path.join(tempDir, 'hash-content-change');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'file.txt')).write('original');
const hashBefore = await smartFs.directory(dirPath).treeHash();
await smartFs.file(path.join(dirPath, 'file.txt')).write('modified');
const hashAfter = await smartFs.directory(dirPath).treeHash();
expect(hashBefore).not.toEqual(hashAfter);
});
tap.test('treeHash should change when file is added', async () => {
const dirPath = path.join(tempDir, 'hash-file-add');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'file1.txt')).write('content1');
const hashBefore = await smartFs.directory(dirPath).treeHash();
await smartFs.file(path.join(dirPath, 'file2.txt')).write('content2');
const hashAfter = await smartFs.directory(dirPath).treeHash();
expect(hashBefore).not.toEqual(hashAfter);
});
tap.test('treeHash should work recursively', async () => {
const dirPath = path.join(tempDir, 'hash-recursive');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'root.txt')).write('root');
await smartFs.directory(path.join(dirPath, 'sub')).create();
await smartFs.file(path.join(dirPath, 'sub', 'nested.txt')).write('nested');
// Non-recursive should only include root.txt
const hashNonRecursive = await smartFs.directory(dirPath).treeHash();
// Recursive should include both files
const hashRecursive = await smartFs.directory(dirPath).recursive().treeHash();
expect(hashNonRecursive).not.toEqual(hashRecursive);
});
tap.test('treeHash should respect filter', async () => {
const dirPath = path.join(tempDir, 'hash-filter');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'file.ts')).write('typescript');
await smartFs.file(path.join(dirPath, 'file.js')).write('javascript');
// Hash only .ts files
const hashTs = await smartFs.directory(dirPath).filter(/\.ts$/).treeHash();
// Hash only .js files
const hashJs = await smartFs.directory(dirPath).filter(/\.js$/).treeHash();
// Should be different since they're hashing different files
expect(hashTs).not.toEqual(hashJs);
});
tap.test('treeHash should support different algorithms', async () => {
const dirPath = path.join(tempDir, 'hash-algorithm');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'file.txt')).write('test content');
const sha256 = await smartFs.directory(dirPath).treeHash({ algorithm: 'sha256' });
const sha512 = await smartFs.directory(dirPath).treeHash({ algorithm: 'sha512' });
expect(sha256.length).toEqual(64); // SHA-256 = 64 hex chars
expect(sha512.length).toEqual(128); // SHA-512 = 128 hex chars
expect(sha256).not.toEqual(sha512);
});
tap.test('treeHash of empty directory should return consistent hash', async () => {
const dirPath = path.join(tempDir, 'hash-empty');
await smartFs.directory(dirPath).create();
const hash1 = await smartFs.directory(dirPath).treeHash();
const hash2 = await smartFs.directory(dirPath).treeHash();
expect(hash1).toEqual(hash2);
expect(hash1.length).toEqual(64);
});
// --- Directory copy/move tests ---
tap.test('should copy a directory', async () => {
const sourcePath = path.join(tempDir, 'copy-dir-source');
const destPath = path.join(tempDir, 'copy-dir-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'file1.txt')).write('content1');
await smartFs.file(path.join(sourcePath, 'file2.txt')).write('content2');
await smartFs.directory(sourcePath).copy(destPath);
// Source should still exist
const sourceExists = await smartFs.directory(sourcePath).exists();
expect(sourceExists).toEqual(true);
// Destination should exist with same files
const destExists = await smartFs.directory(destPath).exists();
expect(destExists).toEqual(true);
const destContent1 = await smartFs.file(path.join(destPath, 'file1.txt')).encoding('utf8').read();
const destContent2 = await smartFs.file(path.join(destPath, 'file2.txt')).encoding('utf8').read();
expect(destContent1).toEqual('content1');
expect(destContent2).toEqual('content2');
});
tap.test('should copy a directory recursively with nested subdirectories', async () => {
const sourcePath = path.join(tempDir, 'copy-recursive-source');
const destPath = path.join(tempDir, 'copy-recursive-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'root.txt')).write('root');
await smartFs.directory(path.join(sourcePath, 'sub1')).create();
await smartFs.file(path.join(sourcePath, 'sub1', 'nested1.txt')).write('nested1');
await smartFs.directory(path.join(sourcePath, 'sub1', 'sub2')).create();
await smartFs.file(path.join(sourcePath, 'sub1', 'sub2', 'deep.txt')).write('deep');
await smartFs.directory(sourcePath).copy(destPath);
// Verify all files copied
expect(await smartFs.file(path.join(destPath, 'root.txt')).exists()).toEqual(true);
expect(await smartFs.file(path.join(destPath, 'sub1', 'nested1.txt')).exists()).toEqual(true);
expect(await smartFs.file(path.join(destPath, 'sub1', 'sub2', 'deep.txt')).exists()).toEqual(true);
const deepContent = await smartFs.file(path.join(destPath, 'sub1', 'sub2', 'deep.txt')).encoding('utf8').read();
expect(deepContent).toEqual('deep');
});
tap.test('should copy directory with filter applied', async () => {
const sourcePath = path.join(tempDir, 'copy-filter-source');
const destPath = path.join(tempDir, 'copy-filter-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'file.ts')).write('typescript');
await smartFs.file(path.join(sourcePath, 'file.js')).write('javascript');
await smartFs.file(path.join(sourcePath, 'file.txt')).write('text');
// Copy only .ts files
await smartFs.directory(sourcePath).filter(/\.ts$/).copy(destPath);
expect(await smartFs.file(path.join(destPath, 'file.ts')).exists()).toEqual(true);
expect(await smartFs.file(path.join(destPath, 'file.js')).exists()).toEqual(false);
expect(await smartFs.file(path.join(destPath, 'file.txt')).exists()).toEqual(false);
});
tap.test('should copy all files when applyFilter(false)', async () => {
const sourcePath = path.join(tempDir, 'copy-no-filter-source');
const destPath = path.join(tempDir, 'copy-no-filter-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'file.ts')).write('typescript');
await smartFs.file(path.join(sourcePath, 'file.js')).write('javascript');
// Filter is set but applyFilter(false) ignores it
await smartFs.directory(sourcePath).filter(/\.ts$/).applyFilter(false).copy(destPath);
expect(await smartFs.file(path.join(destPath, 'file.ts')).exists()).toEqual(true);
expect(await smartFs.file(path.join(destPath, 'file.js')).exists()).toEqual(true);
});
tap.test('should copy with overwrite(true) replacing existing files', async () => {
const sourcePath = path.join(tempDir, 'copy-overwrite-source');
const destPath = path.join(tempDir, 'copy-overwrite-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'file.txt')).write('new content');
await smartFs.directory(destPath).create();
await smartFs.file(path.join(destPath, 'file.txt')).write('old content');
await smartFs.directory(sourcePath).overwrite(true).copy(destPath);
const content = await smartFs.file(path.join(destPath, 'file.txt')).encoding('utf8').read();
expect(content).toEqual('new content');
});
tap.test('should throw error when onConflict is error and target exists', async () => {
const sourcePath = path.join(tempDir, 'copy-conflict-error-source');
const destPath = path.join(tempDir, 'copy-conflict-error-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'file.txt')).write('content');
await smartFs.directory(destPath).create();
let threw = false;
try {
await smartFs.directory(sourcePath).onConflict('error').copy(destPath);
} catch (e: any) {
threw = true;
expect(e.message).toInclude('EEXIST');
}
expect(threw).toEqual(true);
});
tap.test('should replace target when onConflict is replace', async () => {
const sourcePath = path.join(tempDir, 'copy-replace-source');
const destPath = path.join(tempDir, 'copy-replace-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'new.txt')).write('new');
await smartFs.directory(destPath).create();
await smartFs.file(path.join(destPath, 'old.txt')).write('old');
await smartFs.directory(sourcePath).onConflict('replace').copy(destPath);
// Old file should be gone, new file should exist
expect(await smartFs.file(path.join(destPath, 'old.txt')).exists()).toEqual(false);
expect(await smartFs.file(path.join(destPath, 'new.txt')).exists()).toEqual(true);
});
tap.test('should move a directory', async () => {
const sourcePath = path.join(tempDir, 'move-dir-source');
const destPath = path.join(tempDir, 'move-dir-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'file1.txt')).write('content1');
await smartFs.file(path.join(sourcePath, 'file2.txt')).write('content2');
await smartFs.directory(sourcePath).move(destPath);
// Source should no longer exist
const sourceExists = await smartFs.directory(sourcePath).exists();
expect(sourceExists).toEqual(false);
// Destination should exist with files
const destExists = await smartFs.directory(destPath).exists();
expect(destExists).toEqual(true);
const destContent1 = await smartFs.file(path.join(destPath, 'file1.txt')).encoding('utf8').read();
expect(destContent1).toEqual('content1');
});
tap.test('should move directory recursively', async () => {
const sourcePath = path.join(tempDir, 'move-recursive-source');
const destPath = path.join(tempDir, 'move-recursive-dest');
await smartFs.directory(sourcePath).create();
await smartFs.file(path.join(sourcePath, 'root.txt')).write('root');
await smartFs.directory(path.join(sourcePath, 'sub')).create();
await smartFs.file(path.join(sourcePath, 'sub', 'nested.txt')).write('nested');
await smartFs.directory(sourcePath).move(destPath);
// Source should not exist
expect(await smartFs.directory(sourcePath).exists()).toEqual(false);
// All files should be at destination
expect(await smartFs.file(path.join(destPath, 'root.txt')).exists()).toEqual(true);
expect(await smartFs.file(path.join(destPath, 'sub', 'nested.txt')).exists()).toEqual(true);
});
tap.test('should copy empty directory', async () => {
const sourcePath = path.join(tempDir, 'copy-empty-source');
const destPath = path.join(tempDir, 'copy-empty-dest');
await smartFs.directory(sourcePath).create();
await smartFs.directory(sourcePath).copy(destPath);
expect(await smartFs.directory(destPath).exists()).toEqual(true);
});
tap.test('cleanup temp directory', async () => {
await fs.rm(tempDir, { recursive: true, force: true });
expect(true).toEqual(true);
});
export default tap.start();