@julesl23/s5js
Version:
Enhanced TypeScript SDK for S5 decentralized storage with path-based API, media processing, and directory utilities
348 lines • 15.7 kB
JavaScript
/**
* Test suite for FS5Advanced - CID-aware API
*
* This test suite follows TDD principles - tests are written first to define
* the expected behavior of the Advanced CID API.
*/
import { describe, test, expect, beforeEach } from 'vitest';
import { FS5 } from '../../src/fs/fs5.js';
import { FS5Advanced } from '../../src/fs/fs5-advanced.js';
import { JSCryptoImplementation } from '../../src/api/crypto/js.js';
// Mock API for testing without S5 infrastructure
class MockAPI {
crypto;
blobs = new Map();
registry = new Map();
constructor() {
this.crypto = new JSCryptoImplementation();
}
async uploadBlob(blob) {
const data = new Uint8Array(await blob.arrayBuffer());
const hash = await this.crypto.hashBlake3(data);
const fullHash = new Uint8Array([0x1e, ...hash]);
const key = Buffer.from(hash).toString('hex');
this.blobs.set(key, data);
return { hash: fullHash, size: blob.size };
}
async downloadBlobAsBytes(hash) {
const actualHash = hash[0] === 0x1e ? hash.slice(1) : hash;
const key = Buffer.from(actualHash).toString('hex');
const data = this.blobs.get(key);
if (!data)
throw new Error(`Blob not found: ${key}`);
return data;
}
async registryGet(publicKey) {
const key = Buffer.from(publicKey).toString('hex');
return this.registry.get(key);
}
async registrySet(entry) {
const key = Buffer.from(entry.pk).toString('hex');
this.registry.set(key, entry);
}
}
// Mock identity
class MockIdentity {
fsRootKey = new Uint8Array(32).fill(42);
}
describe('FS5Advanced', () => {
let fs5;
let fs5Advanced;
let api;
let identity;
let directories;
beforeEach(() => {
api = new MockAPI();
identity = new MockIdentity();
fs5 = new FS5(api, identity);
// Initialize directory storage
directories = new Map();
directories.set('', {
magic: 'S5.pro',
header: {},
dirs: new Map(),
files: new Map()
});
// Mock FS5 internal methods for testing
fs5._loadDirectory = async (path) => {
const dir = directories.get(path || '');
if (!dir) {
throw new Error(`Directory not found: ${path}`);
}
return dir;
};
fs5._updateDirectory = async (path, updater) => {
// Ensure all parent directories exist
const segments = path.split('/').filter(s => s);
for (let i = 0; i < segments.length; i++) {
const currentPath = segments.slice(0, i + 1).join('/');
const parentPath = segments.slice(0, i).join('/') || '';
const dirName = segments[i];
if (!directories.has(currentPath)) {
const newDir = {
magic: 'S5.pro',
header: {},
dirs: new Map(),
files: new Map()
};
directories.set(currentPath, newDir);
const parent = directories.get(parentPath);
if (parent) {
parent.dirs.set(dirName, {
link: { type: 'fixed_hash_blake3', hash: new Uint8Array(32) }
});
}
}
}
const dir = directories.get(path || '') || {
magic: 'S5.pro',
header: {},
dirs: new Map(),
files: new Map()
};
const result = await updater(dir, new Uint8Array(32));
if (result) {
directories.set(path || '', result);
}
};
// Create FS5Advanced instance
fs5Advanced = new FS5Advanced(fs5);
});
describe('constructor', () => {
test('should create FS5Advanced instance from FS5', () => {
expect(fs5Advanced).toBeInstanceOf(FS5Advanced);
expect(fs5Advanced).toHaveProperty('pathToCID');
expect(fs5Advanced).toHaveProperty('cidToPath');
expect(fs5Advanced).toHaveProperty('getByCID');
expect(fs5Advanced).toHaveProperty('putByCID');
});
test('should throw error if FS5 instance is null', () => {
expect(() => new FS5Advanced(null)).toThrow();
});
});
describe('pathToCID', () => {
test('should extract CID from file path', async () => {
// Store a file first
const testData = 'Hello, CID World!';
await fs5.put('home/test.txt', testData);
// Get CID for that file
const cid = await fs5Advanced.pathToCID('home/test.txt');
expect(cid).toBeInstanceOf(Uint8Array);
expect(cid.length).toBeGreaterThan(0);
// CID should be 32 bytes (blake3 hash)
expect(cid.length).toBe(32);
});
test('should extract CID from directory path', async () => {
// Create a directory with content
await fs5.put('home/docs/readme.md', '# README');
// Get CID for the directory
const cid = await fs5Advanced.pathToCID('home/docs');
expect(cid).toBeInstanceOf(Uint8Array);
expect(cid.length).toBeGreaterThan(0);
});
test('should throw error for non-existent path', async () => {
await expect(fs5Advanced.pathToCID('home/nonexistent.txt'))
.rejects.toThrow();
});
test('should handle root path', async () => {
// Root directory should have a CID
const cid = await fs5Advanced.pathToCID('');
expect(cid).toBeInstanceOf(Uint8Array);
expect(cid.length).toBeGreaterThan(0);
});
test('should return consistent CID for same content', async () => {
const testData = 'Consistent content';
await fs5.put('home/file1.txt', testData);
await fs5.put('home/file2.txt', testData);
const cid1 = await fs5Advanced.pathToCID('home/file1.txt');
const cid2 = await fs5Advanced.pathToCID('home/file2.txt');
// Same content should have same CID
expect(cid1).toEqual(cid2);
});
});
describe('cidToPath', () => {
test('should find path for file CID', async () => {
const testData = 'Find me by CID';
await fs5.put('home/findme.txt', testData);
const cid = await fs5Advanced.pathToCID('home/findme.txt');
const path = await fs5Advanced.cidToPath(cid);
expect(path).toBe('home/findme.txt');
});
test('should find path for directory CID', async () => {
await fs5.put('home/mydir/file.txt', 'content');
const cid = await fs5Advanced.pathToCID('home/mydir');
const path = await fs5Advanced.cidToPath(cid);
expect(path).toBe('home/mydir');
});
test('should return null for unknown CID', async () => {
// Create a random CID that doesn't exist
const randomCID = new Uint8Array(32);
crypto.getRandomValues(randomCID);
const path = await fs5Advanced.cidToPath(randomCID);
expect(path).toBeNull();
});
test('should find first path if multiple paths have same CID', async () => {
const testData = 'Duplicate content';
await fs5.put('home/first.txt', testData);
await fs5.put('home/second.txt', testData);
const cid = await fs5Advanced.pathToCID('home/first.txt');
const foundPath = await fs5Advanced.cidToPath(cid);
// Should find one of the paths (implementation may vary)
expect(foundPath === 'home/first.txt' || foundPath === 'home/second.txt').toBe(true);
});
test('should throw error for invalid CID', async () => {
const invalidCID = new Uint8Array(10); // Wrong size
await expect(fs5Advanced.cidToPath(invalidCID))
.rejects.toThrow();
});
});
describe('getByCID', () => {
test('should retrieve file data by CID', async () => {
const testData = 'Retrieve by CID';
await fs5.put('home/data.txt', testData);
const cid = await fs5Advanced.pathToCID('home/data.txt');
const retrievedData = await fs5Advanced.getByCID(cid);
expect(retrievedData).toBe(testData);
});
test('should retrieve binary data by CID', async () => {
const binaryData = new Uint8Array([1, 2, 3, 4, 5]);
await fs5.put('home/binary.bin', binaryData);
const cid = await fs5Advanced.pathToCID('home/binary.bin');
const retrievedData = await fs5Advanced.getByCID(cid);
expect(retrievedData).toBeInstanceOf(Uint8Array);
expect(retrievedData).toEqual(binaryData);
});
test('should retrieve JSON data by CID', async () => {
const jsonData = { message: 'Hello', count: 42 };
await fs5.put('home/data.json', jsonData);
const cid = await fs5Advanced.pathToCID('home/data.json');
const retrievedData = await fs5Advanced.getByCID(cid);
expect(retrievedData).toEqual(jsonData);
});
test('should throw error for invalid CID', async () => {
const invalidCID = new Uint8Array(32);
crypto.getRandomValues(invalidCID);
await expect(fs5Advanced.getByCID(invalidCID))
.rejects.toThrow();
});
test('should handle large files', async () => {
// Create a larger file (~10KB)
const largeData = 'x'.repeat(10000);
await fs5.put('home/large.txt', largeData);
const cid = await fs5Advanced.pathToCID('home/large.txt');
const retrievedData = await fs5Advanced.getByCID(cid);
expect(retrievedData).toBe(largeData);
expect(retrievedData.length).toBe(10000);
});
});
describe('putByCID', () => {
test('should store data and return CID', async () => {
const testData = 'Store and get CID';
const cid = await fs5Advanced.putByCID(testData);
expect(cid).toBeInstanceOf(Uint8Array);
expect(cid.length).toBe(32);
// Verify we can retrieve it
const retrieved = await fs5Advanced.getByCID(cid);
expect(retrieved).toBe(testData);
});
test('should handle binary data', async () => {
const binaryData = new Uint8Array([10, 20, 30, 40, 50]);
const cid = await fs5Advanced.putByCID(binaryData);
expect(cid).toBeInstanceOf(Uint8Array);
const retrieved = await fs5Advanced.getByCID(cid);
expect(retrieved).toEqual(binaryData);
});
test('should handle JSON/CBOR data', async () => {
const objectData = {
name: 'Test Object',
value: 12345,
nested: { key: 'value' }
};
const cid = await fs5Advanced.putByCID(objectData);
expect(cid).toBeInstanceOf(Uint8Array);
const retrieved = await fs5Advanced.getByCID(cid);
expect(retrieved).toEqual(objectData);
});
test('should return consistent CID for same content', async () => {
const testData = 'Same content';
const cid1 = await fs5Advanced.putByCID(testData);
const cid2 = await fs5Advanced.putByCID(testData);
// Content-addressing: same content = same CID
expect(cid1).toEqual(cid2);
});
test('should handle empty data', async () => {
const emptyData = '';
const cid = await fs5Advanced.putByCID(emptyData);
expect(cid).toBeInstanceOf(Uint8Array);
expect(cid.length).toBe(32);
});
});
describe('integration tests', () => {
test('should maintain data integrity across CID and path operations', async () => {
const testData = 'Integrity test';
// Store using path
await fs5.put('home/integrity.txt', testData);
// Get CID
const cid = await fs5Advanced.pathToCID('home/integrity.txt');
// Retrieve by CID
const dataByCID = await fs5Advanced.getByCID(cid);
// Retrieve by path
const dataByPath = await fs5.get('home/integrity.txt');
// All should be consistent
expect(dataByCID).toBe(testData);
expect(dataByPath).toBe(testData);
expect(dataByCID).toBe(dataByPath);
});
test('should handle CID-based workflow', async () => {
// 1. Store data without path
const data = 'CID-first workflow';
const cid = await fs5Advanced.putByCID(data);
// 2. Retrieve by CID
const retrieved = await fs5Advanced.getByCID(cid);
expect(retrieved).toBe(data);
// 3. Store at path with same CID result
await fs5.put('home/linked.txt', data);
const cid2 = await fs5Advanced.pathToCID('home/linked.txt');
expect(cid2).toEqual(cid);
// 4. Find path from CID
const foundPath = await fs5Advanced.cidToPath(cid);
expect(foundPath).toBe('home/linked.txt');
});
test('should work with different data types', async () => {
// String
const stringData = 'string test';
await fs5.put('home/string.txt', stringData);
const stringCid = await fs5Advanced.pathToCID('home/string.txt');
expect(stringCid).toBeInstanceOf(Uint8Array);
// Binary
const binaryData = new Uint8Array([1, 2, 3]);
await fs5.put('home/binary.bin', binaryData);
const binaryCid = await fs5Advanced.pathToCID('home/binary.bin');
expect(binaryCid).toBeInstanceOf(Uint8Array);
// JSON object
const objectData = { key: 'value' };
await fs5.put('home/object.json', objectData);
const objectCid = await fs5Advanced.pathToCID('home/object.json');
expect(objectCid).toBeInstanceOf(Uint8Array);
// All should be retrievable
expect(await fs5Advanced.getByCID(stringCid)).toBe(stringData);
expect(await fs5Advanced.getByCID(binaryCid)).toEqual(binaryData);
expect(await fs5Advanced.getByCID(objectCid)).toEqual(objectData);
});
test('should not affect existing FS5 API functionality', async () => {
// Use composition of FS5 + Advanced API
await fs5.put('home/advanced.txt', 'advanced data');
const advancedCid = await fs5Advanced.pathToCID('home/advanced.txt');
expect(advancedCid).toBeInstanceOf(Uint8Array);
// Use regular FS5 API
await fs5.put('home/regular.txt', 'regular data');
// Both should work
expect(await fs5.get('home/advanced.txt')).toBe('advanced data');
expect(await fs5.get('home/regular.txt')).toBe('regular data');
// Advanced API should work with regular files
const cid = await fs5Advanced.pathToCID('home/regular.txt');
expect(await fs5Advanced.getByCID(cid)).toBe('regular data');
});
});
});
//# sourceMappingURL=fs5-advanced.test.js.map