UNPKG

filecoin-pin

Version:

IPFS Pinning Service API implementation that pins to Filecoin's PDP service

336 lines (262 loc) 12.3 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { rm } from 'node:fs/promises' import { existsSync } from 'node:fs' import { FilecoinPinStore } from '../../filecoin-pin-store.js' import { createConfig } from '../../config.js' import { createLogger } from '../../logger.js' import { createHelia } from 'helia' import { CID } from 'multiformats/cid' import { sha256 } from 'multiformats/hashes/sha2' import * as raw from 'multiformats/codecs/raw' import { MockSynapse } from '../mocks/synapse-mocks.js' // Mock the Synapse SDK vi.mock('@filoz/synapse-sdk', async () => await import('../mocks/synapse-sdk.js')) describe('FilecoinPinStore', () => { let pinStore: FilecoinPinStore let contentOriginHelia: any let testCID: CID let testBlock: Uint8Array let testUser: any const testOutputDir = './test-car-output' beforeEach(async () => { // Create test data testBlock = new TextEncoder().encode('Test block for Filecoin pin store') const hash = await sha256.digest(testBlock) testCID = CID.create(1, raw.code, hash) testUser = { id: 'test-user', name: 'Test User' } // Create test config with output directory and fake private key const config = { ...createConfig(), carStoragePath: testOutputDir, privateKey: '0x0000000000000000000000000000000000000000000000000000000000000001' // Fake test key } const logger = createLogger(config) // Create a Helia node to serve content contentOriginHelia = await createHelia() // Create mock Synapse service const mockSynapse = new MockSynapse() const mockStorage = await mockSynapse.createStorage() const synapseService = { synapse: mockSynapse as any, storage: mockStorage } // Create Filecoin pin store with mock Synapse pinStore = new FilecoinPinStore({ config, logger, synapseService }) await pinStore.start() }) afterEach(async () => { await pinStore.stop() if (contentOriginHelia != null) { await contentOriginHelia.stop() } // Clean up test files if (existsSync(testOutputDir)) { await rm(testOutputDir, { recursive: true, force: true }) } }) describe('Pin Operations', () => { it('should create a pin with Filecoin metadata', async () => { const pinResult = await pinStore.pin(testUser, testCID, { name: 'Test Pin', meta: { test: 'metadata' } }) expect(pinResult).toBeDefined() expect(pinResult.id).toBeDefined() expect(pinResult.status).toBe('queued') expect(pinResult.pin.cid).toBe(testCID.toString()) expect(pinResult.pin.name).toBe('Test Pin') // Check Filecoin-specific metadata expect(pinResult.filecoin).toBeDefined() expect(pinResult.filecoin?.carFilePath).toContain(testCID.toString()) expect(pinResult.filecoin?.pinStarted).toBeDefined() expect(pinResult.filecoin?.carStats).toBeDefined() // Check info field enhancements expect(pinResult.info?.car_file_path).toBeDefined() expect(pinResult.info?.blocks_written).toBe('0') expect(pinResult.info?.total_size).toBe('0') }, 10000) it('should emit events during pin processing', async () => { const events: any[] = [] pinStore.on('pin:block:stored', (data) => events.push({ type: 'block:stored', data })) pinStore.on('pin:block:missing', (data) => events.push({ type: 'block:missing', data })) pinStore.on('pin:car:completed', (data) => events.push({ type: 'car:completed', data })) // Add the block to content origin Helia first so it can be found await contentOriginHelia.blockstore.put(testCID, testBlock) // Get origin multiaddr for pinning const originAddrs = contentOriginHelia.libp2p.getMultiaddrs() const origins = originAddrs.map((addr: any) => addr.toString()) const pinResult = await pinStore.pin(testUser, testCID, { name: 'Event Test', origins }) // Wait a bit for background processing await new Promise(resolve => setTimeout(resolve, 2000)) // Should have at least some events expect(events.length).toBeGreaterThan(0) // Check for completion event const completionEvent = events.find(e => e.type === 'car:completed') if (completionEvent != null) { expect(completionEvent.data.pinId).toBe(pinResult.id) expect(completionEvent.data.userId).toBe(testUser.id) } }, 15000) it('should handle pin updates', async () => { const pinResult = await pinStore.pin(testUser, testCID, { name: 'Original Name' }) const updated = await pinStore.update(testUser, pinResult.id, { name: 'Updated Name', meta: { updated: 'true' } }) expect(updated).toBeDefined() expect(updated?.pin.name).toBe('Updated Name') expect(updated?.pin.meta?.updated).toBe('true') expect(updated?.filecoin).toBeDefined() }) it('should retrieve pin status with Filecoin metadata', async () => { const pinResult = await pinStore.pin(testUser, testCID, { name: 'Retrieve Test' }) const retrieved = await pinStore.get(testUser, pinResult.id) expect(retrieved).toBeDefined() expect(retrieved?.id).toBe(pinResult.id) expect(retrieved?.filecoin).toBeDefined() expect(retrieved?.filecoin?.carFilePath).toContain(testCID.toString()) expect(retrieved?.info?.car_file_path).toBeDefined() }) it('should cancel pins and clean up resources', async () => { const pinResult = await pinStore.pin(testUser, testCID, { name: 'Cancel Test' }) // Wait a moment to ensure pin processing starts await new Promise(resolve => setTimeout(resolve, 150)) await pinStore.cancel(testUser, pinResult.id) // Check that pin is no longer active after cancellation const activePinsAfterCancel = pinStore.getActivePinStats() expect(activePinsAfterCancel).toHaveLength(0) // Try to retrieve cancelled pin const retrieved = await pinStore.get(testUser, pinResult.id) expect(retrieved).toBeUndefined() }) it('should list pins with Filecoin metadata', async () => { // Create multiple pins const pin1 = await pinStore.pin(testUser, testCID, { name: 'Pin 1' }) const hash2 = await sha256.digest(new TextEncoder().encode('second block')) const cid2 = CID.create(1, raw.code, hash2) const pin2 = await pinStore.pin(testUser, cid2, { name: 'Pin 2' }) const list = await pinStore.list(testUser) expect(list.count).toBeGreaterThanOrEqual(2) expect(list.results).toHaveLength(list.count) // Check that all results have Filecoin metadata const pin1Result = list.results.find(p => p.id === pin1.id) const pin2Result = list.results.find(p => p.id === pin2.id) expect(pin1Result).toBeDefined() expect(pin1Result?.filecoin).toBeDefined() expect(pin1Result?.info?.car_file_path).toBeDefined() expect(pin2Result).toBeDefined() expect(pin2Result?.filecoin).toBeDefined() expect(pin2Result?.info?.car_file_path).toBeDefined() }) }) describe('Statistics and Monitoring', () => { it('should track active pin statistics', async () => { await pinStore.pin(testUser, testCID, { name: 'Stats Test 1' }) const hash2 = await sha256.digest(new TextEncoder().encode('stats test 2')) const cid2 = CID.create(1, raw.code, hash2) await pinStore.pin(testUser, cid2, { name: 'Stats Test 2' }) // Wait for pins to start processing await new Promise(resolve => setTimeout(resolve, 150)) const stats = pinStore.getActivePinStats() // Pins may complete quickly in test environment, so we check for reasonable activity expect(stats.length).toBeGreaterThanOrEqual(0) // If we have active pins, verify their structure if (stats.length > 0) { expect(stats[0]?.pinId).toBeDefined() expect(stats[0]?.stats).toBeDefined() expect(typeof stats[0]?.duration).toBe('number') } }) it('should provide detailed pin information', async () => { const pinResult = await pinStore.pin(testUser, testCID, { name: 'Detail Test', meta: { priority: 'high', source: 'test' } }) // Wait for processing to complete await new Promise(resolve => setTimeout(resolve, 2000)) const retrieved = await pinStore.get(testUser, pinResult.id) expect(retrieved).toBeDefined() expect(retrieved?.info?.car_file_path).toBeDefined() expect(retrieved?.info?.blocks_written).toBeDefined() expect(retrieved?.info?.total_size).toBeDefined() // These fields are only present after completion if (retrieved?.status === 'pinned') { expect(retrieved?.info?.pin_duration).toBeDefined() } // Original metadata should be preserved expect(retrieved?.pin.meta?.priority).toBe('high') expect(retrieved?.pin.meta?.source).toBe('test') }) }) describe('Error Handling', () => { it('should handle pin processing failures gracefully', async () => { // Create a CID for non-existent content const nonExistentHash = await sha256.digest(new TextEncoder().encode('nonexistent')) const nonExistentCID = CID.create(1, raw.code, nonExistentHash) const events: any[] = [] pinStore.on('pin:failed', (data) => events.push({ type: 'pin:failed', data })) const pinResult = await pinStore.pin(testUser, nonExistentCID, { name: 'Failure Test' }) // Wait for background processing to potentially fail await new Promise(resolve => setTimeout(resolve, 2000)) // Pin should be created and may process to completion (which is successful behavior for missing content) expect(pinResult).toBeDefined() expect(['queued', 'pinning', 'pinned'].includes(pinResult.status)).toBe(true) // The pin may or may not fail depending on timing, but we should handle it gracefully const retrieved = await pinStore.get(testUser, pinResult.id) expect(retrieved).toBeDefined() }, 10000) it('should clean up resources on stop', async () => { await pinStore.pin(testUser, testCID, { name: 'Cleanup Test 1' }) const hash2 = await sha256.digest(new TextEncoder().encode('cleanup test 2')) const cid2 = CID.create(1, raw.code, hash2) await pinStore.pin(testUser, cid2, { name: 'Cleanup Test 2' }) // Wait for pins to start processing await new Promise(resolve => setTimeout(resolve, 150)) // Stop should clean up all resources await pinStore.stop() // Should have no active pins after stop (cleanup is always successful) expect(pinStore.getActivePinStats()).toHaveLength(0) }) }) describe('Pin Store Lifecycle', () => { it('should start and stop cleanly', async () => { // Create mock Synapse service const mockSynapse = new MockSynapse() const mockStorage = await mockSynapse.createStorage() const synapseService = { synapse: mockSynapse as any, storage: mockStorage } const config = { ...createConfig(), privateKey: '0x0000000000000000000000000000000000000000000000000000000000000001' // Fake test key } const newPinStore = new FilecoinPinStore({ config, logger: createLogger(config), synapseService }) await expect(newPinStore.start()).resolves.not.toThrow() await expect(newPinStore.stop()).resolves.not.toThrow() }) it('should handle multiple start/stop cycles', async () => { // Create mock Synapse service const mockSynapse = new MockSynapse() const mockStorage = await mockSynapse.createStorage() const synapseService = { synapse: mockSynapse as any, storage: mockStorage } const config = { ...createConfig(), privateKey: '0x0000000000000000000000000000000000000000000000000000000000000001' // Fake test key } const newPinStore = new FilecoinPinStore({ config, logger: createLogger(config), synapseService }) await newPinStore.start() await newPinStore.stop() await newPinStore.start() await newPinStore.stop() // Should not throw expect(true).toBe(true) }) }) })