UNPKG

filecoin-pin

Version:

Bridge IPFS content to Filecoin Onchain Cloud using familiar tools

420 lines (352 loc) 14.7 kB
import * as synapseSdk from '@filoz/synapse-sdk' import { CID } from 'multiformats/cid' import type { Logger } from 'pino' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createConfig } from '../../config.js' import { getSynapseService, resetSynapseService, type SynapseSetupConfig, setupSynapse, } from '../../core/synapse/index.js' import { uploadToSynapse } from '../../core/upload/index.js' import { createLogger } from '../../logger.js' // Mock the Synapse SDK - vi.mock requires async import for ES modules vi.mock('@filoz/synapse-sdk', async () => await import('../mocks/synapse-sdk.js')) // Test CID for upload tests const TEST_CID = CID.parse('bafkreia5fn4rmshmb7cl7fufkpcw733b5anhuhydtqstnglpkzosqln5kq') describe('synapse-service', () => { let config: SynapseSetupConfig let logger: Logger beforeEach(() => { // Create test config with Synapse enabled config = { ...createConfig(), privateKey: '0x0000000000000000000000000000000000000000000000000000000000000001', // Fake test key rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1', } logger = createLogger({ logLevel: 'info' }) // Reset the service instances resetSynapseService() vi.clearAllMocks() }) afterEach(() => { vi.clearAllMocks() }) describe('setupSynapse', () => { it('should throw error when no authentication is provided', async () => { // Create an invalid config with no authentication const invalidConfig = { rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1', } as any await expect(setupSynapse(invalidConfig, logger)).rejects.toThrow('Authentication required') }) it('should initialize Synapse when private key is configured', async () => { const result = await setupSynapse(config, logger) expect(result).not.toBeNull() expect(result?.synapse).toBeDefined() expect(result?.storage).toBeDefined() }) it('should log initialization events', async () => { const infoSpy = vi.spyOn(logger, 'info') await setupSynapse(config, logger) // Check that initialization logs were called expect(infoSpy).toHaveBeenCalledWith( expect.objectContaining({ event: 'synapse.init', authMode: 'standard', rpcUrl: config.rpcUrl, }), 'Initializing Synapse SDK' ) expect(infoSpy).toHaveBeenCalledWith( expect.objectContaining({ event: 'synapse.init.success' }), 'Synapse SDK initialized' ) }) it('should call provider selection callback', async () => { const callbacks: any[] = [] const originalCreate = synapseSdk.Synapse.create // Capture callbacks vi.mocked(originalCreate).mockImplementationOnce(async (options) => { const synapse = await originalCreate(options) const originalCreateContext = synapse.storage.createContext.bind(synapse.storage) synapse.storage.createContext = async (opts: any) => { if (opts?.callbacks?.onProviderSelected != null) { callbacks.push(opts.callbacks.onProviderSelected) } return await originalCreateContext(opts) } return synapse }) await setupSynapse(config, logger) expect(callbacks.length).toBeGreaterThan(0) }) }) describe('getSynapseService', () => { it('should return null when not initialized', () => { // Ensure service is reset resetSynapseService() const result = getSynapseService() expect(result).toBeNull() }) it('should return service after initialization', async () => { await setupSynapse(config, logger) const result = getSynapseService() expect(result).not.toBeNull() expect(result?.synapse).toBeDefined() expect(result?.storage).toBeDefined() }) }) describe('uploadToSynapse', () => { let service: any beforeEach(async () => { service = await setupSynapse(config, logger) }) it('should upload data successfully', async () => { const data = new Uint8Array([1, 2, 3]) const contextId = 'pin-123' const result = await uploadToSynapse(service, data, TEST_CID, logger, { contextId }) expect(result).toHaveProperty('pieceCid') expect(result).toHaveProperty('pieceId') expect(result).toHaveProperty('dataSetId') expect(result.pieceCid).toMatch(/^bafkzcib/) expect(result.dataSetId).toBe('123') }) it('should log upload events', async () => { const infoSpy = vi.spyOn(logger, 'info') const data = new Uint8Array([1, 2, 3]) const contextId = 'pin-456' await uploadToSynapse(service, data, TEST_CID, logger, { contextId }) expect(infoSpy).toHaveBeenCalledWith( expect.objectContaining({ event: 'synapse.upload.piece_uploaded', contextId, }), 'Upload to PDP server complete' ) expect(infoSpy).toHaveBeenCalledWith( expect.objectContaining({ event: 'synapse.upload.success', contextId, }), 'Successfully uploaded to Filecoin with Synapse' ) }) it('should call upload callbacks', async () => { let uploadCompleteCallbackCalled = false let pieceAddedCallbackCalled = false const data = new Uint8Array([1, 2, 3]) await uploadToSynapse(service, data, TEST_CID, logger, { contextId: 'pin-789', onProgress(event) { switch (event.type) { case 'onUploadComplete': { uploadCompleteCallbackCalled = true break } case 'onPieceAdded': { pieceAddedCallbackCalled = true break } } }, }) expect(uploadCompleteCallbackCalled).toBe(true) expect(pieceAddedCallbackCalled).toBe(true) }) }) describe('Provider Information', () => { it('should capture provider info during initialization', async () => { const mockConfig: SynapseSetupConfig = { privateKey: 'test-private-key', rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1', } const service = await setupSynapse(mockConfig, logger) // Check that provider info was captured expect(service.providerInfo).toBeDefined() expect(service.providerInfo?.id).toBe(1) expect(service.providerInfo?.name).toBe('Mock Provider') expect(service.providerInfo?.products?.PDP?.data?.serviceURL).toBe('http://localhost:8888/pdp') }) it('should include provider info in upload result', async () => { // Ensure synapse is initialized with provider info const mockConfig: SynapseSetupConfig = { privateKey: 'test-private-key', rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1', } const service = await setupSynapse(mockConfig, logger) // Now test the upload with synapse-upload.ts const data = new Uint8Array([1, 2, 3]) const result = await uploadToSynapse(service, data, TEST_CID, logger, { contextId: 'test-upload', }) // Verify provider info is included in result expect(result.providerInfo).toBeDefined() expect(result.providerInfo?.id).toBe(1) expect(result.providerInfo?.name).toBe('Mock Provider') expect(result.providerInfo?.products?.PDP?.data?.serviceURL).toBe('http://localhost:8888/pdp') }) it('should always include provider info', async () => { // Initialize with provider info const mockConfig: SynapseSetupConfig = { privateKey: 'test-private-key', rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1', } const service = await setupSynapse(mockConfig, logger) const data = new Uint8Array([1, 2, 3]) const result = await uploadToSynapse(service, data, TEST_CID, logger, { contextId: 'test-upload', }) // Verify upload includes provider info expect(result.pieceCid).toBeDefined() expect(result.providerInfo).toBeDefined() expect(result.providerInfo.id).toBe(1) expect(result.providerInfo.name).toBe('Mock Provider') }) it('should handle provider without serviceURL gracefully', async () => { const mockConfig: SynapseSetupConfig = { privateKey: 'test-private-key', rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1', } const service = await setupSynapse(mockConfig, logger) // Modify provider info to not have serviceURL if (service.providerInfo) { ;(service.providerInfo as any).products = { PDP: { data: { // No serviceURL }, }, } } const data = new Uint8Array([1, 2, 3]) const result = await uploadToSynapse(service, data, TEST_CID, logger, { contextId: 'test-upload', }) // Verify upload works with provider info (serviceURL is empty) expect(result.pieceCid).toBeDefined() expect(result.providerInfo).toBeDefined() expect(result.providerInfo.products?.PDP?.data?.serviceURL).toBeUndefined() }) }) describe('Session Key Authentication', () => { it('should accept session key with valid ADD_PIECES permission even without CREATE_DATA_SET', async () => { const mockConfig = { walletAddress: '0x1234567890123456789012345678901234567890', sessionKey: '0x0000000000000000000000000000000000000000000000000000000000000001', rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1', } // Mock session key with no CREATE_DATA_SET permission (expiry = 0) const mockCreateSessionKey = vi.fn(() => { const now = Math.floor(Date.now() / 1000) const oneYear = 365 * 24 * 60 * 60 return { fetchExpiries: async (typehashes: string[]) => { const expiries: Record<string, bigint> = {} for (const typehash of typehashes) { // CREATE_DATA_SET has no permission (0) // ADD_PIECES has valid future expiry if (typehash.includes('1234567890abcdef')) { // This is CREATE_DATA_SET_TYPEHASH expiries[typehash] = 0n } else { // This is ADD_PIECES_TYPEHASH expiries[typehash] = BigInt(now + oneYear) } } return expiries }, } }) // Override the mock const originalCreate = synapseSdk.Synapse.create vi.mocked(synapseSdk.Synapse.create).mockImplementationOnce(async (options) => { const synapse = await originalCreate(options) ;(synapse as any).createSessionKey = mockCreateSessionKey ;(synapse as any).setSession = vi.fn() return synapse }) // Should not throw - session key with only ADD_PIECES is valid for reusing datasets const service = await setupSynapse(mockConfig as any, logger) expect(service).toBeDefined() expect(mockCreateSessionKey).toHaveBeenCalled() }) it('should reject session key with expired CREATE_DATA_SET permission', async () => { const mockConfig = { walletAddress: '0x1234567890123456789012345678901234567890', sessionKey: '0x0000000000000000000000000000000000000000000000000000000000000001', rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1', } // Mock session key with expired CREATE_DATA_SET permission const mockCreateSessionKey = vi.fn(() => { const now = Math.floor(Date.now() / 1000) const oneYear = 365 * 24 * 60 * 60 return { fetchExpiries: async (typehashes: string[]) => { const expiries: Record<string, bigint> = {} for (const typehash of typehashes) { // CREATE_DATA_SET expired 1 hour ago // ADD_PIECES has valid future expiry if (typehash.includes('1234567890abcdef')) { // This is CREATE_DATA_SET_TYPEHASH - expired expiries[typehash] = BigInt(now - 3600) } else { // This is ADD_PIECES_TYPEHASH - valid expiries[typehash] = BigInt(now + oneYear) } } return expiries }, } }) // Override the mock const originalCreate = synapseSdk.Synapse.create vi.mocked(synapseSdk.Synapse.create).mockImplementationOnce(async (options) => { const synapse = await originalCreate(options) ;(synapse as any).createSessionKey = mockCreateSessionKey return synapse }) // Should throw - expired CREATE_DATA_SET permission await expect(setupSynapse(mockConfig as any, logger)).rejects.toThrow('Session key expired or expiring soon') }) it('should reject session key with expired ADD_PIECES permission', async () => { const mockConfig = { walletAddress: '0x1234567890123456789012345678901234567890', sessionKey: '0x0000000000000000000000000000000000000000000000000000000000000001', rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1', } // Mock session key with expired ADD_PIECES permission const mockCreateSessionKey = vi.fn(() => { const now = Math.floor(Date.now() / 1000) const oneYear = 365 * 24 * 60 * 60 return { fetchExpiries: async (typehashes: string[]) => { const expiries: Record<string, bigint> = {} for (const typehash of typehashes) { // CREATE_DATA_SET has valid permission // ADD_PIECES expired 1 hour ago if (typehash.includes('1234567890abcdef')) { // This is CREATE_DATA_SET_TYPEHASH - valid expiries[typehash] = BigInt(now + oneYear) } else { // This is ADD_PIECES_TYPEHASH - expired expiries[typehash] = BigInt(now - 3600) } } return expiries }, } }) // Override the mock const originalCreate = synapseSdk.Synapse.create vi.mocked(synapseSdk.Synapse.create).mockImplementationOnce(async (options) => { const synapse = await originalCreate(options) ;(synapse as any).createSessionKey = mockCreateSessionKey return synapse }) // Should throw - expired ADD_PIECES permission (always required) await expect(setupSynapse(mockConfig as any, logger)).rejects.toThrow('Session key expired or expiring soon') }) }) })