UNPKG

filecoin-pin

Version:

Bridge IPFS content to Filecoin Onchain Cloud using familiar tools

539 lines (424 loc) 19.9 kB
/** * Integration test to verify parity between add and import commands * * This test ensures that: * 1. Adding a file with `add` command produces a valid CAR * 2. Importing that CAR with `import` command works correctly * 3. Both paths produce the same piece CID when uploaded */ import { randomBytes } from 'node:crypto' import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { CarReader } from '@ipld/car' import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { createCarFromPath } from '../../core/unixfs/index.js' // Test constants const PLACEHOLDER_CID = 'bafyaaiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' describe('UnixFS CAR Creation', () => { const testDir = join(tmpdir(), 'filecoin-pin-add-import-test') const testFile = join(testDir, 'test-content.bin') // Use random data to avoid deduplication - need >1MiB for multi-block const testContent = randomBytes(1024 * 1024 * 1.5) // 1.5MB of random data const countBlocks = async (carPath: string): Promise<number> => { const carData = await readFile(carPath) const reader = await CarReader.fromBytes(carData) let count = 0 for await (const _block of reader.blocks()) { count++ } return count } beforeEach(async () => { // Create test directory and file await rm(testDir, { recursive: true, force: true }) await mkdir(testDir, { recursive: true }) await writeFile(testFile, testContent) }) afterEach(async () => { // Clean up test directory await rm(testDir, { recursive: true, force: true }) }) describe('Bare mode (no directory wrapper)', () => { it('should create a valid CAR that can be imported', async () => { // Step 1: Create CAR from file using add logic const { carPath, rootCid } = await createCarFromPath(testFile, { bare: true }) // Step 2: Read and validate the CAR file const carData = await readFile(carPath) const reader = await CarReader.fromBytes(carData) // Verify roots const roots = await reader.getRoots() expect(roots.length).toBe(1) expect(roots[0]?.toString()).toBe(rootCid.toString()) // Step 3: Verify we can iterate through blocks let blockCount = 0 let totalSize = 0 for await (const { cid, bytes } of reader.blocks()) { blockCount++ totalSize += bytes.length // Root block should be present if (cid.toString() === rootCid.toString()) { expect(bytes.length).toBeGreaterThan(0) } } expect(blockCount).toBeGreaterThan(0) expect(totalSize).toBeGreaterThan(0) // Clean up await rm(carPath, { force: true }) }) it('should produce consistent root CIDs for same content in bare mode', async () => { // Create multiple CARs from same content in bare mode const results = await Promise.all([ createCarFromPath(testFile, { bare: true }), createCarFromPath(testFile, { bare: true }), createCarFromPath(testFile, { bare: true }), ]) // All should have the same root CID const rootCids = results.map((r) => r.rootCid.toString()) expect(new Set(rootCids).size).toBe(1) // Clean up all temp CARs await Promise.all(results.map((r) => rm(r.carPath, { force: true }))) }) it('should handle small single-block files correctly in bare mode', async () => { const smallFile = join(testDir, 'small.txt') await writeFile(smallFile, 'tiny') const { carPath, rootCid } = await createCarFromPath(smallFile, { bare: true }) // Should still produce valid CAR const carData = await readFile(carPath) expect(carData.length).toBeGreaterThan(0) // Should have valid UnixFS root // Small files use raw codec (0x55), larger files shard so will have a dag-pb (0x70) root expect([0x55, 0x70]).toContain(rootCid.code) // Verify CAR structure const reader = await CarReader.fromBytes(carData) const roots = await reader.getRoots() expect(roots[0]?.toString()).toBe(rootCid.toString()) // Count blocks - small file should have just 1 block let blockCount = 0 for await (const _block of reader.blocks()) { blockCount++ } expect(blockCount).toBe(1) // Single block for tiny file // Clean up await rm(carPath, { force: true }) }) it('should handle larger files with multiple blocks in bare mode', async () => { // Create a file large enough to require multiple UnixFS blocks // UnixFS chunks at 1MiB (1048576 bytes) by default in Helia const largeFile = join(testDir, 'large.bin') // Use random bytes to avoid deduplication const largeContent = randomBytes(1024 * 1024 * 2) // 2MB of random data await writeFile(largeFile, largeContent) const { carPath, rootCid } = await createCarFromPath(largeFile, { bare: true }) // Read the CAR and count blocks const carData = await readFile(carPath) const reader = await CarReader.fromBytes(carData) let blockCount = 0 let hasRootBlock = false for await (const { cid } of reader.blocks()) { blockCount++ if (cid.toString() === rootCid.toString()) { hasRootBlock = true } } // Should have multiple blocks for chunked content // 2 data blocks + 1 dag-pb root block linking them expect(blockCount).toBe(3) expect(hasRootBlock).toBe(true) // Clean up await rm(carPath, { force: true }) }) it('should validate placeholder CID is replaced in bare mode', async () => { const { carPath, rootCid } = await createCarFromPath(testFile, { bare: true }) // The placeholder CID should never appear in final output expect(rootCid.toString()).not.toBe(PLACEHOLDER_CID) // Verify the CAR file has the correct root const carData = await readFile(carPath) const reader = await CarReader.fromBytes(carData) const roots = await reader.getRoots() expect(roots[0]?.toString()).not.toBe(PLACEHOLDER_CID) expect(roots[0]?.toString()).toBe(rootCid.toString()) // Clean up await rm(carPath, { force: true }) }) }) describe('Directory wrapper mode (default)', () => { it('should create a valid CAR with directory wrapper', async () => { const { carPath, rootCid } = await createCarFromPath(testFile, { bare: false }) // Read and validate the CAR file const carData = await readFile(carPath) const reader = await CarReader.fromBytes(carData) // Verify roots const roots = await reader.getRoots() expect(roots.length).toBe(1) expect(roots[0]?.toString()).toBe(rootCid.toString()) // Should have blocks for file data plus directory let blockCount = 0 for await (const _block of reader.blocks()) { blockCount++ } // Expect: 2 data blocks (1.5MB chunked at 1MiB) + 1 file metadata + 1 wrapping directory = 4 expect(blockCount).toBe(4) // Clean up await rm(carPath, { force: true }) }) it('should have one extra block compared to bare mode', async () => { const { carPath: bareCar } = await createCarFromPath(testFile, { bare: true }) const { carPath: dirCar } = await createCarFromPath(testFile, { bare: false }) const bareBlocks = await countBlocks(bareCar) const dirBlocks = await countBlocks(dirCar) // Directory mode should have exactly one extra block (the directory) expect(dirBlocks).toBe(bareBlocks + 1) // Clean up await rm(bareCar, { force: true }) await rm(dirCar, { force: true }) }) it('should produce different root CIDs between bare and directory modes', async () => { const { carPath: bareCar, rootCid: bareCid } = await createCarFromPath(testFile, { bare: true }) const { carPath: dirCar, rootCid: dirCid } = await createCarFromPath(testFile, { bare: false }) // Root CIDs should be different expect(bareCid.toString()).not.toBe(dirCid.toString()) // Clean up await rm(bareCar, { force: true }) await rm(dirCar, { force: true }) }) it('should produce consistent root CIDs for same content in directory mode', async () => { const results = await Promise.all([ createCarFromPath(testFile, { bare: false }), createCarFromPath(testFile, { bare: false }), createCarFromPath(testFile, { bare: false }), ]) // All should have the same root CID const rootCids = results.map((r) => r.rootCid.toString()) expect(new Set(rootCids).size).toBe(1) // Clean up all temp CARs await Promise.all(results.map((r) => rm(r.carPath, { force: true }))) }) it('should handle small files with directory wrapper', async () => { const smallFile = join(testDir, 'small.txt') await writeFile(smallFile, 'tiny') const { carPath: bareCar } = await createCarFromPath(smallFile, { bare: true }) const { carPath: dirCar } = await createCarFromPath(smallFile, { bare: false }) const bareBlocks = await countBlocks(bareCar) const dirBlocks = await countBlocks(dirCar) // Small file: 1 data block + 1 directory block expect(bareBlocks).toBe(1) expect(dirBlocks).toBe(2) // Clean up await rm(bareCar, { force: true }) await rm(dirCar, { force: true }) }) }) describe('Directory CAR creation', () => { it('should create a valid CAR from a directory with multiple files', async () => { // Create a test directory structure const testDirPath = join(testDir, 'test-directory') await mkdir(testDirPath, { recursive: true }) // Create multiple files with different sizes await writeFile(join(testDirPath, 'file1.txt'), 'content1') await writeFile(join(testDirPath, 'file2.bin'), randomBytes(1024)) await writeFile(join(testDirPath, 'large.bin'), randomBytes(1024 * 1024 * 2)) // 2MB const { carPath, rootCid } = await createCarFromPath(testDirPath) // Verify the result expect(rootCid).toBeDefined() // Read and validate the CAR file const carData = await readFile(carPath) const reader = await CarReader.fromBytes(carData) // Verify roots const roots = await reader.getRoots() expect(roots.length).toBe(1) expect(roots[0]?.toString()).toBe(rootCid.toString()) // Count blocks - should have blocks for: // - 3 file data blocks (file1.txt, file2.bin, 2 blocks for large.bin) // - 1 file metadata for large.bin (since it's chunked) // - 1 root directory let blockCount = 0 for await (const _block of reader.blocks()) { blockCount++ } expect(blockCount).toBeGreaterThanOrEqual(5) // At least 5 blocks // Clean up await rm(carPath, { force: true }) await rm(testDirPath, { recursive: true, force: true }) }) it('should handle nested directory structures correctly', async () => { // Create nested directory structure const rootDir = join(testDir, 'nested-test') const subDir1 = join(rootDir, 'subdir1') const subDir2 = join(rootDir, 'subdir2') const deepDir = join(subDir1, 'deep') await mkdir(deepDir, { recursive: true }) await mkdir(subDir2, { recursive: true }) // Add files at different levels await writeFile(join(rootDir, 'root.txt'), 'root content') await writeFile(join(subDir1, 'sub1.txt'), 'subdir1 content') await writeFile(join(subDir2, 'sub2.txt'), 'subdir2 content') await writeFile(join(deepDir, 'deep.txt'), 'deep content') const { carPath, rootCid } = await createCarFromPath(rootDir) // Verify the result expect(rootCid).toBeDefined() // Verify CAR structure const carData = await readFile(carPath) const reader = await CarReader.fromBytes(carData) const roots = await reader.getRoots() expect(roots.length).toBe(1) expect(roots[0]?.toString()).toBe(rootCid.toString()) // Count blocks - should have at least: // - 4 file blocks (one per file) // - Directory blocks for nested structure let blockCount = 0 for await (const _block of reader.blocks()) { blockCount++ } expect(blockCount).toBeGreaterThan(4) // More than just file blocks // Clean up await rm(carPath, { force: true }) await rm(rootDir, { recursive: true, force: true }) }) it('should handle empty directories correctly', async () => { const emptyDir = join(testDir, 'empty-dir') await mkdir(emptyDir, { recursive: true }) const { carPath, rootCid } = await createCarFromPath(emptyDir) expect(rootCid).toBeDefined() // Verify CAR has at least the directory block const carData = await readFile(carPath) const reader = await CarReader.fromBytes(carData) let blockCount = 0 for await (const _block of reader.blocks()) { blockCount++ } expect(blockCount).toBe(1) // Just the empty directory block // Clean up await rm(carPath, { force: true }) await rm(emptyDir, { recursive: true, force: true }) }) it('should handle directories with only subdirectories (no files)', async () => { const parentDir = join(testDir, 'dirs-only') const subDir1 = join(parentDir, 'sub1') const subDir2 = join(parentDir, 'sub2') const subSubDir = join(subDir1, 'subsub') await mkdir(subSubDir, { recursive: true }) await mkdir(subDir2, { recursive: true }) const { carPath, rootCid } = await createCarFromPath(parentDir) expect(rootCid).toBeDefined() // Verify CAR has directory blocks const carData = await readFile(carPath) const reader = await CarReader.fromBytes(carData) let blockCount = 0 for await (const _block of reader.blocks()) { blockCount++ } // Empty directories deduplicate to the same CID, so we expect: // 1. Empty directory block (shared by all empty dirs) // 2. sub1 directory block (links to subsub) // 3. parent directory block (links to sub1 and sub2) expect(blockCount).toBe(3) // Clean up await rm(carPath, { force: true }) await rm(parentDir, { recursive: true, force: true }) }) it('should handle mixed content (files and directories) correctly', async () => { const mixedDir = join(testDir, 'mixed-content') const subDir = join(mixedDir, 'subdir') await mkdir(subDir, { recursive: true }) // Add various file types and sizes await writeFile(join(mixedDir, 'text.txt'), 'text content') await writeFile(join(mixedDir, 'binary.bin'), randomBytes(512)) await writeFile(join(subDir, 'nested.json'), JSON.stringify({ test: true })) await writeFile(join(subDir, 'large.dat'), randomBytes(1024 * 1024 * 1.5)) // 1.5MB const { carPath, rootCid } = await createCarFromPath(mixedDir) expect(rootCid).toBeDefined() // Verify the CAR contains all expected content const blockCount = await countBlocks(carPath) // Should have blocks for: 4 files (large.dat chunked to 2) + file metadata for large.dat + 2 directories expect(blockCount).toBeGreaterThanOrEqual(7) // Clean up await rm(carPath, { force: true }) await rm(mixedDir, { recursive: true, force: true }) }) it('should produce consistent CIDs for identical directory structures', async () => { // Create two identical directory structures const dir1 = join(testDir, 'consistent1') const dir2 = join(testDir, 'consistent2') for (const dir of [dir1, dir2]) { const subDir = join(dir, 'sub') await mkdir(subDir, { recursive: true }) await writeFile(join(dir, 'file.txt'), 'same content') await writeFile(join(subDir, 'nested.txt'), 'nested content') } const result1 = await createCarFromPath(dir1) const result2 = await createCarFromPath(dir2) // Should produce identical CIDs for identical content expect(result1.rootCid.toString()).toBe(result2.rootCid.toString()) // Clean up await rm(result1.carPath, { force: true }) await rm(result2.carPath, { force: true }) await rm(dir1, { recursive: true, force: true }) await rm(dir2, { recursive: true, force: true }) }) it('should handle very deep directory nesting', async () => { // Create a deeply nested structure let currentPath = testDir const depth = 10 for (let i = 0; i < depth; i++) { currentPath = join(currentPath, `level${i}`) } await mkdir(currentPath, { recursive: true }) await writeFile(join(currentPath, 'deep-file.txt'), 'very deep') const basePath = join(testDir, 'level0') const { carPath, rootCid } = await createCarFromPath(basePath) expect(rootCid).toBeDefined() // Should have blocks for all directory levels plus the file const blockCount = await countBlocks(carPath) expect(blockCount).toBeGreaterThanOrEqual(depth + 1) // At least one block per level + file // Clean up await rm(carPath, { force: true }) await rm(basePath, { recursive: true, force: true }) }) it('should create exactly 3 blocks for simple directory with 2 small files', async () => { // Create a simple directory with 2 small files const simpleDir = join(testDir, 'simple-dir') await mkdir(simpleDir, { recursive: true }) // Create 2 small files (small enough to be single blocks each) await writeFile(join(simpleDir, 'file1.txt'), 'content of file 1') await writeFile(join(simpleDir, 'file2.txt'), 'content of file 2') const { carPath, rootCid } = await createCarFromPath(simpleDir) expect(rootCid).toBeDefined() // Expected blocks: // 1. file1.txt data block (raw) // 2. file2.txt data block (raw) // 3. directory block (dag-pb) linking to file1 and file2 const blockCount = await countBlocks(carPath) expect(blockCount).toBe(3) // Clean up await rm(carPath, { force: true }) await rm(simpleDir, { recursive: true, force: true }) }) it('should deduplicate identical content blocks', async () => { // Create a directory with multiple files containing identical content const dedupDir = join(testDir, 'dedup-test') await mkdir(dedupDir, { recursive: true }) // Create 3 files with identical content - they should deduplicate to one block const identicalContent = 'This is the same content in all files' await writeFile(join(dedupDir, 'file1.txt'), identicalContent) await writeFile(join(dedupDir, 'file2.txt'), identicalContent) await writeFile(join(dedupDir, 'file3.txt'), identicalContent) const { carPath, rootCid } = await createCarFromPath(dedupDir) expect(rootCid).toBeDefined() // Expected blocks: // 1. Single data block (shared by all 3 files due to deduplication) // 2. directory block (dag-pb) linking to the 3 files (which all point to same data block) const blockCount = await countBlocks(carPath) expect(blockCount).toBe(2) // Only 2 blocks instead of 4 due to deduplication // Verify CAR still has correct structure const carData = await readFile(carPath) const reader = await CarReader.fromBytes(carData) const roots = await reader.getRoots() expect(roots.length).toBe(1) expect(roots[0]?.toString()).toBe(rootCid.toString()) // Clean up await rm(carPath, { force: true }) await rm(dedupDir, { recursive: true, force: true }) }) }) })