filecoin-pin
Version:
Bridge IPFS content to Filecoin Onchain Cloud using familiar tools
279 lines (247 loc) • 8.57 kB
text/typescript
/**
* Unit tests for add command functionality
*
* Tests the add command's ability to:
* - Create UnixFS CAR files from regular files
* - Clean up temporary files
* - Handle errors properly
* - Integrate with Synapse upload flow
*/
import { randomBytes } from 'node:crypto'
import { mkdir, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { runAdd } from '../../add/add.js'
// Mock the external dependencies at module level
vi.mock('../../common/upload-flow.js', () => ({
validatePaymentSetup: vi.fn(),
performUpload: vi.fn().mockResolvedValue({
pieceCid: 'bafkzcibtest1234567890',
pieceId: 789,
dataSetId: '456',
network: 'calibration',
transactionHash: '0xabc123',
providerInfo: {
id: 1,
name: 'Test Provider',
serviceURL: 'http://test.provider',
},
}),
displayUploadResults: vi.fn(),
}))
vi.mock('../../core/synapse/index.js', () => ({
initializeSynapse: vi.fn().mockImplementation(async (config: any) => {
// Validate auth config (mirrors validateAuthConfig in actual code)
const hasStandardAuth = config.privateKey != null
const hasSessionKeyAuth = config.walletAddress != null && config.sessionKey != null
if (!hasStandardAuth && !hasSessionKeyAuth) {
throw new Error('Authentication required: provide either a privateKey or walletAddress + sessionKey')
}
return {
getNetwork: () => 'calibration',
}
}),
createStorageContext: vi.fn().mockResolvedValue({
storage: {},
providerInfo: {
id: 1,
name: 'Test Provider',
serviceURL: 'http://test.provider',
},
}),
cleanupSynapseService: vi.fn(),
}))
vi.mock('../../core/unixfs/index.js', () => ({
createCarFromPath: vi.fn((_filePath: string, options: any) => {
const bare = options?.bare || false
// Different CIDs for bare vs directory mode
const cid = bare
? 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi'
: 'bafybeihw4ytkqxrq7q7e3p2l5s5di7zjzkhxdmfwvqfylkdamdg3xybpbq'
return Promise.resolve({
carPath: '/tmp/test.car',
rootCid: {
toString: () => cid,
},
})
}),
cleanupTempCar: vi.fn(),
}))
vi.mock('../../utils/cli-helpers.js', () => ({
intro: vi.fn(),
outro: vi.fn(),
cancel: vi.fn(),
createSpinner: vi.fn(() => ({
start: vi.fn(),
stop: vi.fn(),
message: vi.fn(),
})),
formatFileSize: vi.fn((size: number) => `${size} bytes`),
}))
// We need to partially mock fs/promises to keep real file operations for test setup
// but mock readFile for the CAR reading part
vi.mock('node:fs/promises', async () => {
const actual = await vi.importActual<typeof import('node:fs/promises')>('node:fs/promises')
return {
...actual,
readFile: vi.fn((path: string) => {
// If it's reading the temp CAR, return mock data
if (path === '/tmp/test.car') {
return Promise.resolve(Buffer.from('mock-car-data'))
}
// Otherwise use real readFile
return actual.readFile(path)
}),
}
})
// Test CID constants (defined after vi.mock calls due to hoisting)
const TEST_BARE_CID = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi'
const TEST_DIR_WRAPPED_CID = 'bafybeihw4ytkqxrq7q7e3p2l5s5di7zjzkhxdmfwvqfylkdamdg3xybpbq'
const TEST_PIECE_CID = 'bafkzcibtest1234567890'
describe('Add Command', () => {
const testDir = join(process.cwd(), 'test-add-files')
const testFile = join(testDir, 'test.bin')
// Use random bytes to avoid deduplication and ensure multi-block CAR (>1MiB)
const testContent = randomBytes(1024 * 1024 * 1.5) // 1.5MB of random data
beforeEach(async () => {
// Create test directory and file
await mkdir(testDir, { recursive: true })
await writeFile(testFile, testContent)
})
afterEach(async () => {
// Clean up test directory
await rm(testDir, { recursive: true, force: true })
vi.clearAllMocks()
})
describe('runAdd command', () => {
it('should successfully add a file with directory wrapper by default', async () => {
const result = await runAdd({
filePath: testFile,
privateKey: 'test-private-key',
rpcUrl: 'wss://test.rpc.url',
})
// Verify the result structure (should use directory wrapper CID by default)
expect(result).toMatchObject({
filePath: testFile,
fileSize: expect.any(Number),
rootCid: TEST_DIR_WRAPPED_CID, // Directory wrapper CID
pieceCid: TEST_PIECE_CID,
pieceId: 789,
dataSetId: '456',
transactionHash: '0xabc123',
providerInfo: {
id: 1,
name: 'Test Provider',
},
})
// Verify createCarFromPath was called without bare flag
const { createCarFromPath } = await import('../../core/unixfs/index.js')
expect(vi.mocked(createCarFromPath)).toHaveBeenCalledWith(
testFile,
expect.objectContaining({
logger: expect.any(Object),
// bare is not passed when undefined, due to spread operator
})
)
})
it('should successfully add a file in bare mode when specified', async () => {
const result = await runAdd({
filePath: testFile,
privateKey: 'test-private-key',
rpcUrl: 'wss://test.rpc.url',
bare: true,
})
// Verify the result structure (should use bare mode CID)
expect(result).toMatchObject({
filePath: testFile,
fileSize: expect.any(Number),
rootCid: TEST_BARE_CID, // Bare mode CID
pieceCid: TEST_PIECE_CID,
pieceId: 789,
dataSetId: '456',
transactionHash: '0xabc123',
providerInfo: {
id: 1,
name: 'Test Provider',
},
})
// Verify createCarFromPath was called with bare flag
const { createCarFromPath } = await import('../../core/unixfs/index.js')
expect(vi.mocked(createCarFromPath)).toHaveBeenCalledWith(
testFile,
expect.objectContaining({
logger: expect.any(Object),
bare: true,
})
)
})
it('passes metadata options through to upload and storage context', async () => {
await runAdd({
filePath: testFile,
privateKey: 'test-private-key',
rpcUrl: 'wss://test.rpc.url',
pieceMetadata: { region: 'us-west', note: '' },
dataSetMetadata: { purpose: 'erc8004' },
})
const { createStorageContext, initializeSynapse } = await import('../../core/synapse/index.js')
expect(vi.mocked(initializeSynapse)).toHaveBeenCalledWith(
expect.objectContaining({
dataSetMetadata: { purpose: 'erc8004' },
}),
expect.anything()
)
expect(vi.mocked(createStorageContext)).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
dataset: {
metadata: { purpose: 'erc8004' },
},
logger: expect.anything(),
})
)
const { performUpload } = await import('../../common/upload-flow.js')
expect(vi.mocked(performUpload)).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.objectContaining({
metadata: { region: 'us-west', note: '' },
})
)
})
it('should reject when file does not exist', async () => {
const mockExit = vi.spyOn(process, 'exit')
await expect(
runAdd({
filePath: '/non/existent/file.txt',
privateKey: 'test-key',
})
).rejects.toThrow('Path not found')
expect(mockExit).not.toHaveBeenCalled()
mockExit.mockRestore()
})
it('should reject when private key is missing', async () => {
const mockExit = vi.spyOn(process, 'exit')
await expect(
runAdd({
filePath: testFile,
// No private key
})
).rejects.toThrow()
expect(mockExit).not.toHaveBeenCalled()
mockExit.mockRestore()
})
it('should reject --bare flag with directories', async () => {
const mockExit = vi.spyOn(process, 'exit')
await expect(
runAdd({
filePath: testDir, // Directory
privateKey: 'test-key',
bare: true, // --bare flag should not work with directories
})
).rejects.toThrow('--bare flag is not supported for directories')
expect(mockExit).not.toHaveBeenCalled()
mockExit.mockRestore()
})
})
})