filecoin-pin
Version:
Bridge IPFS content to Filecoin Onchain Cloud using familiar tools
328 lines (269 loc) • 10.5 kB
text/typescript
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 } from '../../core/car/index.js'
describe('CARWritingBlockstore', () => {
let blockstore: CARWritingBlockstore
let testCID: CID
let testBlock: Uint8Array
const testOutputPath = './test-output.car'
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 = new CARWritingBlockstore({
rootCID: testCID,
outputPath: testOutputPath,
})
})
afterEach(async () => {
// Clean up test files
await blockstore.cleanup()
try {
await stat(testOutputPath)
await rm(testOutputPath)
} catch {
// File doesn't exist, nothing to clean up
}
})
describe('Initialization', () => {
it('should initialize with root CID and output path', () => {
expect(blockstore).toBeDefined()
const stats = blockstore.getStats()
expect(stats.blocksWritten).toBe(0)
expect(stats.totalSize).toBe(0)
expect(stats.finalized).toBe(false)
})
it('should emit initialized event', async () => {
let eventData: any
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('should emit block:stored event when putting blocks', async () => {
let eventData: any
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('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))
// Consume the async generator to get the bytes
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)
// Need to consume the generator to trigger the error
await expect(async () => {
for await (const _ of blockstore.get(nonExistentCID)) {
// Should not reach here
}
}).rejects.toThrow('Block not found')
})
it('should emit block:missing event for missing blocks', async () => {
let eventData: any
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 {
// Consume the generator to trigger the error and event
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'
)
// Block should still exist after failed delete
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 () => {
// Put some blocks first
const cids: CID[] = []
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)
}
// deleteMany should throw an error
await expect(async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const _ of blockstore.deleteMany(cids)) {
// Should not reach here
}
}).rejects.toThrow('DeleteMany operation not supported on CAR writing blockstore')
})
it('should get many blocks', async () => {
// Put some blocks first
const blocks = []
const cids: CID[] = []
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)
// Consume the generator to get the actual bytes and compare all blocks
for (let i = 0; i < results.length; i++) {
const resultBytes = await toBuffer(results[i]?.bytes || [])
expect(resultBytes).toEqual(blocks[i]?.bytes)
}
})
it('should get all blocks', async () => {
// Put some blocks first
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('CAR File Operations', () => {
it('should create a CAR file', async () => {
await blockstore.put(testCID, testBlock)
await blockstore.finalize()
// Verify file was created
const fileExists = await stat(testOutputPath)
.then(() => true)
.catch(() => false)
expect(fileExists).toBe(true)
// Verify file is not empty
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('should emit finalized event', async () => {
let eventData: any
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 () => {
// Write at least one block before finalizing
await blockstore.put(testCID, testBlock)
await blockstore.finalize()
// Now try to put another block - this should fail
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)
})
})
describe('Error Handling', () => {
it('should handle cleanup gracefully', async () => {
await blockstore.put(testCID, testBlock)
let cleanupEmitted = false
blockstore.on('cleanup', () => {
cleanupEmitted = true
})
await blockstore.cleanup()
expect(cleanupEmitted).toBe(true)
})
it('should throw error when finalizing without any blocks', async () => {
// Create a fresh blockstore that hasn't written any blocks
const emptyBlockstore = new CARWritingBlockstore({
rootCID: testCID,
outputPath: './test-empty.car',
})
await expect(emptyBlockstore.finalize()).rejects.toThrow(
'Cannot finalize CAR blockstore without any blocks written'
)
})
})
})