filecoin-pin
Version:
Bridge IPFS content to Filecoin Onchain Cloud using familiar tools
420 lines (352 loc) • 14.7 kB
text/typescript
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')
})
})
})