filecoin-pin
Version:
Bridge IPFS content to Filecoin Onchain Cloud using familiar tools
303 lines • 13.5 kB
JavaScript
import { readFile, rm, stat } from 'node:fs/promises';
import toBuffer from 'it-to-buffer';
import { CID } from 'multiformats/cid';
import * as raw from 'multiformats/codecs/raw';
import { sha256 } from 'multiformats/hashes/sha2';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { CARWritingBlockstore as BrowserCARBlockstore } from '../../core/car/browser-car-blockstore.js';
import { CARWritingBlockstore as NodeCARBlockstore } from '../../core/car/car-blockstore.js';
const testOutputPath = './test-output-shared.car';
const factories = [
{
name: 'Node.js (file-based)',
hasEvents: true,
hasFileOutput: true,
hasBrowserAPI: false,
supportsReading: true,
create: (rootCID) => new NodeCARBlockstore({
rootCID,
outputPath: testOutputPath,
}),
cleanup: async () => {
try {
await stat(testOutputPath);
await rm(testOutputPath);
}
catch {
// File doesn't exist
}
},
},
{
name: 'Browser (in-memory)',
hasEvents: false,
hasFileOutput: false,
hasBrowserAPI: true,
supportsReading: false, // Browser version is write-only
create: (rootCID) => new BrowserCARBlockstore({
rootCID,
}),
cleanup: async () => {
// No cleanup needed for in-memory
},
},
];
describe.each(factories)('CARWritingBlockstore - $name', (factory) => {
let blockstore;
let testCID;
let testBlock;
beforeEach(async () => {
// Create a test block and CID
testBlock = new TextEncoder().encode('Hello, IPFS!');
const hash = await sha256.digest(testBlock);
testCID = CID.create(1, raw.code, hash);
blockstore = factory.create(testCID);
});
afterEach(async () => {
await blockstore.cleanup();
await factory.cleanup();
// Add a small delay to ensure file operations complete
await new Promise((resolve) => setTimeout(resolve, 10));
});
describe('Initialization', () => {
it('should initialize with root CID', () => {
expect(blockstore).toBeDefined();
const stats = blockstore.getStats();
expect(stats.blocksWritten).toBe(0);
expect(stats.totalSize).toBe(0);
expect(stats.finalized).toBe(false);
});
it.skipIf(!factory.hasEvents)('should emit initialized event', async () => {
let eventData;
blockstore.on('initialized', (data) => {
eventData = data;
});
await blockstore.initialize();
expect(eventData).toBeDefined();
expect(eventData.rootCID).toEqual(testCID);
expect(eventData.outputPath).toBe(testOutputPath);
});
});
describe('Block Operations', () => {
it('should put a block and update statistics', async () => {
const returnedCID = await blockstore.put(testCID, testBlock);
expect(returnedCID).toEqual(testCID);
const stats = blockstore.getStats();
expect(stats.blocksWritten).toBe(1);
expect(stats.totalSize).toBe(testBlock.length);
});
it.skipIf(!factory.hasEvents)('should emit block:stored event when putting blocks', async () => {
let eventData;
blockstore.on('block:stored', (data) => {
eventData = data;
});
await blockstore.put(testCID, testBlock);
expect(eventData).toBeDefined();
expect(eventData.cid).toEqual(testCID);
expect(eventData.size).toBe(testBlock.length);
});
it.skipIf(!factory.supportsReading)('should get a block that was previously put', async () => {
await blockstore.put(testCID, testBlock);
// Allow filesystem to settle after write
await new Promise((resolve) => setTimeout(resolve, 50));
const result = await toBuffer(blockstore.get(testCID));
expect(result).toEqual(testBlock);
});
it('should throw error when getting non-existent block', async () => {
const nonExistentHash = await sha256.digest(new TextEncoder().encode('nonexistent'));
const nonExistentCID = CID.create(1, raw.code, nonExistentHash);
await expect(async () => {
for await (const _ of blockstore.get(nonExistentCID)) {
// Should not reach here
}
}).rejects.toThrow('Block not found');
});
it.skipIf(!factory.hasEvents)('should emit block:missing event for missing blocks', async () => {
let eventData;
blockstore.on('block:missing', (data) => {
eventData = data;
});
const nonExistentHash = await sha256.digest(new TextEncoder().encode('nonexistent'));
const nonExistentCID = CID.create(1, raw.code, nonExistentHash);
try {
for await (const _ of blockstore.get(nonExistentCID)) {
// Should not reach here
}
}
catch (_error) {
// Expected to throw
}
expect(eventData).toBeDefined();
expect(eventData.cid).toEqual(nonExistentCID);
const stats = blockstore.getStats();
expect(stats.missingBlocks.has(nonExistentCID.toString())).toBe(true);
});
it('should check if block exists', async () => {
expect(await blockstore.has(testCID)).toBe(false);
await blockstore.put(testCID, testBlock);
expect(await blockstore.has(testCID)).toBe(true);
});
it('should throw error when trying to delete blocks', async () => {
await blockstore.put(testCID, testBlock);
expect(await blockstore.has(testCID)).toBe(true);
await expect(blockstore.delete(testCID)).rejects.toThrow('Delete operation not supported on CAR writing blockstore');
expect(await blockstore.has(testCID)).toBe(true);
});
});
describe('Batch Operations', () => {
it('should put many blocks', async () => {
const blocks = [];
for (let i = 0; i < 3; i++) {
const bytes = new TextEncoder().encode(`Block ${i}`);
const hash = await sha256.digest(bytes);
const cid = CID.create(1, raw.code, hash);
blocks.push({ cid, bytes });
}
const results = [];
for await (const cid of blockstore.putMany(blocks)) {
results.push(cid);
}
expect(results).toHaveLength(3);
const stats = blockstore.getStats();
expect(stats.blocksWritten).toBe(3);
});
it('should throw error when trying to delete many blocks', async () => {
const cids = [];
for (let i = 0; i < 3; i++) {
const data = new TextEncoder().encode(`Block ${i}`);
const hash = await sha256.digest(data);
const cid = CID.create(1, raw.code, hash);
cids.push(cid);
await blockstore.put(cid, data);
}
await expect(async () => {
for await (const _ of blockstore.deleteMany(cids)) {
// Should not reach here
}
}).rejects.toThrow('DeleteMany operation not supported on CAR writing blockstore');
});
it.skipIf(!factory.supportsReading)('should get many blocks', async () => {
const blocks = [];
const cids = [];
for (let i = 0; i < 3; i++) {
const bytes = new TextEncoder().encode(`Block ${i}`);
const hash = await sha256.digest(bytes);
const cid = CID.create(1, raw.code, hash);
blocks.push({ cid, bytes });
cids.push(cid);
await blockstore.put(cid, bytes);
}
// Allow filesystem to settle after writes
await new Promise((resolve) => setTimeout(resolve, 50));
const results = [];
for await (const pair of blockstore.getMany(cids)) {
results.push(pair);
}
expect(results).toHaveLength(3);
const firstPair = results[0];
expect(firstPair).toBeDefined();
if (firstPair != null) {
const firstBytes = await toBuffer(firstPair.bytes);
expect(firstBytes).toEqual(blocks[0]?.bytes);
}
});
it('should get all blocks', async () => {
for (let i = 0; i < 3; i++) {
const data = new TextEncoder().encode(`Block ${i}`);
const hash = await sha256.digest(data);
const cid = CID.create(1, raw.code, hash);
await blockstore.put(cid, data);
}
const results = [];
for await (const pair of blockstore.getAll()) {
results.push(pair);
}
expect(results).toHaveLength(3);
});
});
describe('Finalization', () => {
it.skipIf(!factory.hasFileOutput)('should create a CAR file', async () => {
await blockstore.put(testCID, testBlock);
await blockstore.finalize();
const fileExists = await stat(testOutputPath)
.then(() => true)
.catch(() => false);
expect(fileExists).toBe(true);
const fileContent = await readFile(testOutputPath);
expect(fileContent.length).toBeGreaterThan(0);
});
it('should finalize and return statistics', async () => {
await blockstore.put(testCID, testBlock);
const finalStats = await blockstore.finalize();
expect(finalStats.finalized).toBe(true);
expect(finalStats.blocksWritten).toBe(1);
expect(finalStats.totalSize).toBe(testBlock.length);
expect(finalStats.missingBlocks.size).toBe(0);
});
it.skipIf(!factory.hasEvents)('should emit finalized event', async () => {
let eventData;
blockstore.on('finalized', (data) => {
eventData = data;
});
await blockstore.put(testCID, testBlock);
await blockstore.finalize();
expect(eventData).toBeDefined();
expect(eventData.finalized).toBe(true);
});
it('should prevent putting blocks after finalization', async () => {
await blockstore.put(testCID, testBlock);
await blockstore.finalize();
const anotherBlock = new TextEncoder().encode('Another block');
const anotherHash = await sha256.digest(anotherBlock);
const anotherCID = CID.create(1, raw.code, anotherHash);
await expect(blockstore.put(anotherCID, anotherBlock)).rejects.toThrow('Cannot put blocks in finalized CAR blockstore');
});
it('should handle multiple finalize calls gracefully', async () => {
await blockstore.put(testCID, testBlock);
const stats1 = await blockstore.finalize();
const stats2 = await blockstore.finalize();
expect(stats1).toEqual(stats2);
});
it.skipIf(!factory.hasBrowserAPI)('should provide CAR bytes after finalization', async () => {
await blockstore.put(testCID, testBlock);
await blockstore.finalize();
const carBytes = blockstore.getCarBytes();
expect(carBytes).toBeInstanceOf(Uint8Array);
expect(carBytes.length).toBeGreaterThan(0);
});
it.skipIf(!factory.hasBrowserAPI)('should throw when getting CAR bytes before finalization', () => {
expect(() => {
;
blockstore.getCarBytes();
}).toThrow('Cannot get CAR bytes before finalization');
});
});
describe('Error Handling', () => {
it.skipIf(!factory.hasEvents)('should handle cleanup gracefully', async () => {
let cleanupEmitted = false;
blockstore.on('cleanup', () => {
cleanupEmitted = true;
});
await blockstore.cleanup();
expect(cleanupEmitted).toBe(true);
});
it.skipIf(!factory.hasFileOutput)('should throw error when finalizing without any blocks', async () => {
// Only Node.js version checks for empty blockstore
const emptyBlockstore = factory.create(testCID);
await expect(emptyBlockstore.finalize()).rejects.toThrow('Cannot finalize CAR blockstore without any blocks written');
});
});
describe('Deduplication', () => {
it('should deduplicate identical blocks', async () => {
// Put same block twice
await blockstore.put(testCID, testBlock);
await blockstore.put(testCID, testBlock);
// Allow filesystem to settle after writes
await new Promise((resolve) => setTimeout(resolve, 50));
const stats = blockstore.getStats();
// Should only count once
expect(stats.blocksWritten).toBe(1);
});
});
});
//# sourceMappingURL=car-blockstore-shared.test.js.map