@simplepg/dservice
Version:
JavaScript implementation of the SimplePage backend service
820 lines (659 loc) • 31 kB
JavaScript
import { IpfsService } from '../../src/services/ipfs.js'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { CARFactory } from 'cartonne'
import * as dagCbor from '@ipld/dag-cbor'
import * as dagPb from '@ipld/dag-pb'
import * as raw from 'multiformats/codecs/raw'
import { CID } from 'multiformats/cid'
import { identity } from 'multiformats/hashes/identity'
import { TestEnvironmentKubo } from '@simplepg/test-utils'
import all from 'it-all'
describe('IpfsService', () => {
let testEnvKubo
let ipfsService
let kuboApi
const mockLogger = {
info: () => {},
debug: () => {},
error: () => {},
warn: () => {},
}
function stat(cid) {
return kuboApi.block.stat(cid, { offline: true })
}
beforeAll(async () => {
testEnvKubo = new TestEnvironmentKubo()
kuboApi = await testEnvKubo.start()
ipfsService = new IpfsService({ ipfsClient: kuboApi, logger: mockLogger })
// ipfsService.client = kuboApi
}, 30000)
afterAll(async () => {
await testEnvKubo.stop()
})
it('healthCheck: should return true when IPFS node is running', async () => {
const result = await ipfsService.healthCheck()
expect(result).toBe(true)
})
it('writeCar: should write multiple files and verify them', async () => {
const files = {
'index.html': '<html>Test</html>',
'index.md': '# Test',
'data.json': '{"hello": "world"}',
'_template.html': '<template>Test</template>'
}
// Create a CAR file using cartonne's high-level API
const factory = new CARFactory()
// Add required codecs
factory.codecs.add(dagCbor)
factory.codecs.add(dagPb)
factory.codecs.add(raw)
const car = factory.build()
// Create a directory structure
const dirLinks = {}
for (const [name, content] of Object.entries(files)) {
const contentBuffer = Buffer.from(content)
// Specify raw codec when adding content
const cid = car.put(contentBuffer, { codec: raw })
dirLinks[name] = { cid, size: contentBuffer.length }
}
// Add directory as root with dag-cbor codec
const rootCid = car.put(dirLinks, { isRoot: true, codec: dagCbor })
// Get the CAR file as bytes
const carBuffer = car.bytes
// Write content using the service
const returnedCid = await ipfsService.writeCar(carBuffer, 'test-domain.eth')
expect(returnedCid).toBeTruthy()
expect(returnedCid.toString()).toBe(rootCid.toString())
// Verify the directory block exists in IPFS
const exists = await kuboApi.block.stat(returnedCid)
expect(exists).toBeTruthy()
// Verify all file blocks exist in IPFS
for (const { cid } of Object.values(dirLinks)) {
const fileExists = await kuboApi.block.stat(cid.toString())
expect(fileExists).toBeTruthy()
}
// Verify the pin exists with correct label
const pins = await kuboApi.pin.ls({ name: 'spg_staged_test-domain.eth' })
const pin = await pins.next()
expect(pin.value.type).toBe('recursive')
// Get all pins to debug the structure
const allPins = await kuboApi.pin.ls({ name: 'spg_staged_' })
for await (const p of allPins) {
expect(p?.name).toMatch(/^spg_staged_test-domain\.eth_\d+$/)
expect(p.type).toBe('recursive')
}
// Verify all files exist and have correct content
for (const [name, content] of Object.entries(files)) {
const fileCid = dirLinks[name].cid
// Verify block exists
const fileExists = await kuboApi.block.stat(fileCid.toString())
expect(fileExists).toBeTruthy()
// Verify content
const retrievedBlock = await kuboApi.block.get(fileCid.toString())
const retrievedContent = uint8ArrayToString(retrievedBlock)
expect(retrievedContent).toBe(content)
}
// Verify directory structure by decoding block manually
const block = await kuboApi.block.get(returnedCid)
const dirData = dagCbor.decode(block)
const listedFiles = Object.keys(dirData)
expect(listedFiles.sort()).toEqual(Object.keys(files).sort())
})
it('readCarLite: should read only index files from a directory', async () => {
// Create a test directory structure
const files = {
'index.html': '<html>Test</html>',
'index.md': '# Test',
'ignored.txt': 'Should not be included',
'_template.html': '<template>Test</template>'
}
// Add files to IPFS
const entries = []
for (const [name, content] of Object.entries(files)) {
entries.push({
path: name,
content: Buffer.from(content)
})
}
// Add directory to IPFS
let dirCid
for await (const result of kuboApi.addAll(entries, { wrapWithDirectory: true, cidVersion: 1 })) {
if (result.path === '') { // This is the root directory
dirCid = result.cid.toString()
}
}
// Read using readCarLite
const carBuffer = await ipfsService.readCarLite(dirCid)
expect(carBuffer).toBeTruthy()
// Create a new CAR reader to verify contents
const factory = new CARFactory()
factory.codecs.add(dagCbor)
factory.codecs.add(dagPb)
factory.codecs.add(raw)
const car = await factory.fromBytes(carBuffer)
expect(car.roots[0].toString()).toBe(dirCid)
expect(car.roots.length).toBe(1)
let blockCount = 0
for (const block of car.blocks) {
blockCount++
}
expect(blockCount).toBe(4)
// Get the root block
const rootData = await car.get(car.roots[0])
// Verify only index files are in the directory
const fileNames = []
for (const link of rootData.Links) {
fileNames.push(link.Name)
}
expect(fileNames.sort()).toEqual(['index.html', 'index.md', 'ignored.txt', '_template.html'].sort())
// Verify each file's content
for (const link of rootData.Links) {
if (['index.html', 'index.md', '_template.html'].includes(link.Name)) {
const fileBlock = await car.get(link.Hash)
const content = uint8ArrayToString(fileBlock)
expect(content).toBe(files[link.Name])
}
}
})
it('finalizePage: should create and preserve finalizations for each block number', async () => {
const domain = 'test-domain2.eth'
const blockNumbers = [12345, 12346, 12347]
const cids = []
// Create first version with shared content
const sharedContent = 'Shared content'
const oldFiles = {
'index.html': '<html>Old</html>',
'shared.txt': sharedContent
}
const factory = new CARFactory()
factory.codecs.add(dagCbor)
factory.codecs.add(dagPb)
factory.codecs.add(raw)
// Create and finalize first version
const oldCar = factory.build()
const oldDirLinks = {}
for (const [name, content] of Object.entries(oldFiles)) {
const contentBuffer = Buffer.from(content)
const cid = oldCar.put(contentBuffer, { codec: raw })
oldDirLinks[name] = { cid, size: contentBuffer.length }
}
const oldRootCid = oldCar.put(oldDirLinks, { isRoot: true, codec: dagCbor })
// Stage and finalize first version
const oldStagedCid = await ipfsService.writeCar(oldCar.bytes, domain)
await ipfsService.finalizePage(oldStagedCid, domain, blockNumbers[0])
cids.push(oldStagedCid)
// Create and finalize additional versions
for (let i = 1; i < blockNumbers.length; i++) {
const blockNumber = blockNumbers[i]
const files = {
'index.html': `<html>Version ${blockNumber}</html>`,
'shared.txt': sharedContent, // Keep shared content
'data.json': `{"version": ${blockNumber}}`
}
const car = factory.build()
const dirLinks = {}
for (const [name, content] of Object.entries(files)) {
const contentBuffer = Buffer.from(content)
const cid = car.put(contentBuffer, { codec: raw })
dirLinks[name] = { cid, size: contentBuffer.length }
}
const rootCid = car.put(dirLinks, { isRoot: true, codec: dagCbor })
// Stage and finalize
const stagedCid = await ipfsService.writeCar(car.bytes, domain)
await ipfsService.finalizePage(stagedCid, domain, blockNumber)
cids.push(stagedCid)
}
// Verify finalizations DAG exists
const finalizationsPins = await kuboApi.pin.ls({ name: 'spg_finalizations' })
const finalizationsPinResults = []
for await (const pin of finalizationsPins) {
finalizationsPinResults.push(pin)
}
expect(finalizationsPinResults.length).toBe(1)
// Get the finalizations DAG
const finalizationsCid = finalizationsPinResults[0].cid
const finalizationsNode = await kuboApi.dag.get(finalizationsCid)
const finalizations = finalizationsNode.value
// Verify domain exists in finalizations
expect(finalizations[domain]).toBeTruthy()
expect(finalizations[domain].length).toBe(blockNumbers.length)
// Verify each finalization exists with correct block number and CID
for (let i = 0; i < blockNumbers.length; i++) {
const blockNumber = blockNumbers[i]
const expectedCid = cids[i]
const finalization = finalizations[domain].find(f => f.blockNumber === blockNumber)
expect(finalization).toBeTruthy()
expect(finalization.cid).toEqual(expectedCid)
// Verify content is accessible
const block = await kuboApi.block.get(expectedCid)
const dirData = dagCbor.decode(block)
// Verify index.html content
const indexContent = await kuboApi.block.get(dirData['index.html'].cid.toString())
if (i === 0) {
expect(uint8ArrayToString(indexContent)).toBe('<html>Old</html>')
} else {
expect(uint8ArrayToString(indexContent)).toBe(`<html>Version ${blockNumber}</html>`)
}
// Verify shared content
const sharedContent = await kuboApi.block.get(dirData['shared.txt'].cid.toString())
expect(uint8ArrayToString(sharedContent)).toBe('Shared content')
// Verify data.json for newer versions
if (i > 0) {
const dataContent = await kuboApi.block.get(dirData['data.json'].cid.toString())
expect(uint8ArrayToString(dataContent)).toBe(`{"version": ${blockNumber}}`)
}
}
})
it('pruneStaged: should remove old staged pins but keep recent ones', async () => {
const domain1 = 'test-domain1.eth'
const domain2 = 'test-domain2.eth'
// Create test content
const factory = new CARFactory()
factory.codecs.add(dagCbor)
// Create two simple test files with just root objects
const car1 = factory.build()
const car2 = factory.build()
// Create simple root objects
const rootCid1 = car1.put({ test: 'content1' }, { isRoot: true, codec: dagCbor })
const rootCid2 = car2.put({ test: 'content2' }, { isRoot: true, codec: dagCbor })
// Create service with 1 hour max age
const ipfsServiceWithPrune = new IpfsService({
ipfsClient: kuboApi,
maxStagedAge: 60 * 60, // 1 hour in seconds
logger: mockLogger
})
ipfsServiceWithPrune.client = kuboApi
// Stage both files
const stagedCid1 = await ipfsServiceWithPrune.writeCar(car1.bytes, domain1)
const stagedCid2 = await ipfsServiceWithPrune.writeCar(car2.bytes, domain2)
// Manually modify the timestamp of first pin to be 2 hours old
const twoHoursAgo = Math.floor(Date.now() / 1000) - (2 * 60 * 60)
const oldLabel = `spg_staged_${domain1}_${twoHoursAgo}`
await kuboApi.pin.rm(stagedCid1, { recursive: true }) // Remove current pin
await kuboApi.pin.add(stagedCid1, { recursive: true, name: oldLabel }) // Re-add with old timestamp
// Run prune
await ipfsServiceWithPrune.pruneStaged()
// Verify old pin is removed
const oldPins = await kuboApi.pin.ls({ name: `spg_staged_${domain1}` })
const oldResults = []
for await (const pin of oldPins) {
oldResults.push(pin)
}
expect(oldResults.length).toBe(0)
// Verify recent pin still exists
const recentPins = await kuboApi.pin.ls({ name: `spg_staged_${domain2}` })
const recentResults = []
for await (const pin of recentPins) {
recentResults.push(pin)
}
expect(recentResults.length).toBe(1)
})
it('isPageFinalized: should correctly check if a page is finalized', async () => {
const domain = 'test-domain3.eth'
const blockNumber = 12345
// Create and stage some test content
const factory = new CARFactory()
factory.codecs.add(dagCbor)
const car = factory.build()
const rootCid = car.put({ test: 'content' }, { isRoot: true, codec: dagCbor })
// Stage and finalize the content
const stagedCid = await ipfsService.writeCar(car.bytes, domain)
await ipfsService.finalizePage(stagedCid, domain, blockNumber)
// Check if page is finalized
const isFinalized = await ipfsService.isPageFinalized(stagedCid, domain, blockNumber)
expect(isFinalized).toBe(true)
// Check with wrong CID
const wrongCid = CID.create(1, 0x55, identity.digest(new Uint8Array([0x01, 0x02, 0x03])))
const isWrongFinalized = await ipfsService.isPageFinalized(wrongCid, domain, blockNumber)
expect(isWrongFinalized).toBe(false)
})
describe('nukePage', () => {
it('should remove all finalizations for the given domain', async () => {
const domain = 'test-prune-domain.eth'
const blockNumbers = [100, 101, 102]
// Create and finalize multiple versions for the domain
const factory = new CARFactory()
factory.codecs.add(dagCbor)
factory.codecs.add(raw)
const finalizedCids = []
for (const blockNumber of blockNumbers) {
const car = factory.build()
const content = `Content for block ${blockNumber}`
const contentCid = car.put(Buffer.from(content), { codec: raw })
const rootCid = car.put({ content: contentCid }, { isRoot: true, codec: dagCbor })
// Stage and finalize
const stagedCid = await ipfsService.writeCar(car.bytes, domain)
await ipfsService.finalizePage(stagedCid, domain, blockNumber)
finalizedCids.push(stagedCid)
}
// Verify finalizations exist before pruning
const finalizationsBefore = await ipfsService.finalizations.getAll(domain)
expect(finalizationsBefore).toBeTruthy()
expect(finalizationsBefore.length).toBe(blockNumbers.length)
// Prune the page
await ipfsService.nukePage(domain)
// Verify finalizations are removed
const finalizationsAfter = await ipfsService.finalizations.getAll(domain)
expect(finalizationsAfter.length).toBe(0)
})
it('should remove all blocks under the finalized CIDs', async () => {
const domain = 'test-prune-blocks.eth'
const blockNumber = 200
// Create a multi-level structure with shared content
const factory = new CARFactory()
factory.codecs.add(dagCbor)
factory.codecs.add(raw)
const car = factory.build()
// Create nested structure: root -> dir -> file
const fileContent = 'Shared file content'
const fileCid = car.put(Buffer.from(fileContent), { codec: raw })
const dirContent = { 'file.txt': { cid: fileCid, size: fileContent.length } }
const dirCid = car.put(dirContent, { codec: dagCbor })
const rootContent = { 'directory': { cid: dirCid, size: 0 } }
const rootCid = car.put(rootContent, { isRoot: true, codec: dagCbor })
// Stage and finalize
const stagedCid = await ipfsService.writeCar(car.bytes, domain)
await ipfsService.finalizePage(stagedCid, domain, blockNumber)
// Verify all blocks exist before pruning
expect(await stat(rootCid)).toBeTruthy()
expect(await stat(dirCid)).toBeTruthy()
expect(await stat(fileCid)).toBeTruthy()
// Prune the page
await ipfsService.nukePage(domain)
// Verify all blocks are removed
await expect(stat(rootCid)).rejects.toThrow()
await expect(stat(dirCid)).rejects.toThrow()
await expect(stat(fileCid)).rejects.toThrow()
})
it('should preserve blocks that are pinned by other domains', async () => {
const domain1 = 'test-shared1.eth'
const domain2 = 'test-shared2.eth'
const blockNumber1 = 300
const blockNumber2 = 301
// Create shared content
const factory = new CARFactory()
factory.codecs.add(dagCbor)
factory.codecs.add(raw)
const sharedContent = 'Shared content between domains'
const sharedCar = factory.build()
const sharedCid = sharedCar.put(Buffer.from(sharedContent), { codec: raw })
// Add shared content to IPFS
await kuboApi.block.put(Buffer.from(sharedContent), { cid: sharedCid })
// Create content for domain1 that references shared content
const car1 = factory.build()
const root1Content = {
'shared.txt': { cid: sharedCid, size: sharedContent.length },
'unique1.txt': { cid: car1.put(Buffer.from('Unique to domain1'), { codec: raw }), size: 18 }
}
const root1Cid = car1.put(root1Content, { isRoot: true, codec: dagCbor })
// Create content for domain2 that also references shared content
const car2 = factory.build()
const root2Content = {
'shared.txt': { cid: sharedCid, size: sharedContent.length },
'unique2.txt': { cid: car2.put(Buffer.from('Unique to domain2'), { codec: raw }), size: 18 }
}
const root2Cid = car2.put(root2Content, { isRoot: true, codec: dagCbor })
// Finalize both domains
const stagedCid1 = await ipfsService.writeCar(car1.bytes, domain1)
await ipfsService.finalizePage(stagedCid1, domain1, blockNumber1)
const stagedCid2 = await ipfsService.writeCar(car2.bytes, domain2)
await ipfsService.finalizePage(stagedCid2, domain2, blockNumber2)
// Verify all blocks exist before pruning
expect(await stat(root1Cid)).toBeTruthy()
expect(await stat(root2Cid)).toBeTruthy()
expect(await stat(sharedCid)).toBeTruthy()
// Prune only domain1
await ipfsService.nukePage(domain1)
// Verify domain1's unique blocks are removed
await expect(stat(root1Cid)).rejects.toThrow()
// Verify domain2's blocks are preserved
expect(await stat(root2Cid)).toBeTruthy()
// Verify shared content is preserved (still pinned by domain2)
expect(await stat(sharedCid)).toBeTruthy()
// Verify domain1's final cids are removed
const finalizations1 = await ipfsService.finalizations.getAll(domain1)
expect(finalizations1.length).toBe(0)
// Verify domain2's final cids are preserved
const finalizations2 = await ipfsService.finalizations.getAll(domain2)
expect(finalizations2.length).toBe(1)
})
it('should handle recursive pins correctly', async () => {
const domain = 'test-recursive.eth'
const blockNumber = 400
// Create a deep recursive structure
const factory = new CARFactory()
factory.codecs.add(dagCbor)
factory.codecs.add(raw)
const car = factory.build()
// Create deep nested structure: root -> level1 -> level2 -> level3 -> file
const fileContent = 'Deep file content'
const fileCid = car.put(Buffer.from(fileContent), { codec: raw })
const level3Content = { 'deep-file.txt': { cid: fileCid, size: fileContent.length } }
const level3Cid = car.put(level3Content, { codec: dagCbor })
const level2Content = { 'level3': { cid: level3Cid, size: 0 } }
const level2Cid = car.put(level2Content, { codec: dagCbor })
const level1Content = { 'level2': { cid: level2Cid, size: 0 } }
const level1Cid = car.put(level1Content, { codec: dagCbor })
const rootContent = { 'level1': { cid: level1Cid, size: 0 } }
const rootCid = car.put(rootContent, { isRoot: true, codec: dagCbor })
// Stage and finalize
const stagedCid = await ipfsService.writeCar(car.bytes, domain)
await ipfsService.finalizePage(stagedCid, domain, blockNumber)
// Verify all levels exist before pruning
expect(await stat(rootCid)).toBeTruthy()
expect(await stat(level1Cid)).toBeTruthy()
expect(await stat(level2Cid)).toBeTruthy()
expect(await stat(level3Cid)).toBeTruthy()
expect(await stat(fileCid)).toBeTruthy()
// Prune the page
await ipfsService.nukePage(domain)
// Verify all levels are removed
await expect(stat(rootCid)).rejects.toThrow()
await expect(stat(level1Cid)).rejects.toThrow()
await expect(stat(level2Cid)).rejects.toThrow()
await expect(stat(level3Cid)).rejects.toThrow()
await expect(stat(fileCid)).rejects.toThrow()
})
it('should handle mixed shared and unique content in recursive structures', async () => {
const domain1 = 'test-mixed1.eth'
const domain2 = 'test-mixed2.eth'
const blockNumber1 = 500
const blockNumber2 = 501
// Create shared content
const factory = new CARFactory()
factory.codecs.add(dagCbor)
factory.codecs.add(raw)
const sharedContent = 'Shared content in recursive structure'
const sharedCar = factory.build()
const sharedCid = sharedCar.put(Buffer.from(sharedContent), { codec: raw })
// Add shared content to IPFS
await kuboApi.block.put(Buffer.from(sharedContent), { cid: sharedCid })
// Create recursive structure for domain1
const car1 = factory.build()
const unique1Content = 'Unique to domain1'
const unique1Cid = car1.put(Buffer.from(unique1Content), { codec: raw })
const level1Content1 = {
'shared.txt': { cid: sharedCid, size: sharedContent.length },
'unique1.txt': { cid: unique1Cid, size: unique1Content.length }
}
const level1Cid1 = car1.put(level1Content1, { codec: dagCbor })
const root1Content = { 'level1': { cid: level1Cid1, size: 0 } }
const root1Cid = car1.put(root1Content, { isRoot: true, codec: dagCbor })
// Create recursive structure for domain2
const car2 = factory.build()
const unique2Content = 'Unique to domain2'
const unique2Cid = car2.put(Buffer.from(unique2Content), { codec: raw })
const level1Content2 = {
'shared.txt': { cid: sharedCid, size: sharedContent.length },
'unique2.txt': { cid: unique2Cid, size: unique2Content.length }
}
const level1Cid2 = car2.put(level1Content2, { codec: dagCbor })
const root2Content = { 'level1': { cid: level1Cid2, size: 0 } }
const root2Cid = car2.put(root2Content, { isRoot: true, codec: dagCbor })
// Finalize both domains
const stagedCid1 = await ipfsService.writeCar(car1.bytes, domain1)
await ipfsService.finalizePage(stagedCid1, domain1, blockNumber1)
const stagedCid2 = await ipfsService.writeCar(car2.bytes, domain2)
await ipfsService.finalizePage(stagedCid2, domain2, blockNumber2)
// Verify all blocks exist before pruning
expect(await stat(root1Cid)).toBeTruthy()
expect(await stat(root2Cid)).toBeTruthy()
expect(await stat(level1Cid1)).toBeTruthy()
expect(await stat(level1Cid2)).toBeTruthy()
expect(await stat(unique1Cid)).toBeTruthy()
expect(await stat(unique2Cid)).toBeTruthy()
expect(await stat(sharedCid)).toBeTruthy()
// Prune only domain1
await ipfsService.nukePage(domain1)
// Verify domain1's unique blocks are removed
await expect(stat(root1Cid)).rejects.toThrow()
await expect(stat(level1Cid1)).rejects.toThrow()
await expect(stat(unique1Cid)).rejects.toThrow()
// Verify domain2's blocks are preserved
expect(await stat(root2Cid)).toBeTruthy()
expect(await stat(level1Cid2)).toBeTruthy()
expect(await stat(unique2Cid)).toBeTruthy()
// Verify shared content is preserved
expect(await stat(sharedCid)).toBeTruthy()
})
it('should handle empty domain gracefully', async () => {
// Test pruning a domain that has no finalized pins
const emptyDomain = 'empty-domain.eth'
// This should not throw an error
await expect(ipfsService.nukePage(emptyDomain)).resolves.not.toThrow()
// Verify no pins exist for this domain
const pins = await kuboApi.pin.ls({ name: `spg_final_${emptyDomain}` })
const pinResults = []
for await (const pin of pins) {
pinResults.push(pin)
}
expect(pinResults.length).toBe(0)
})
it('should handle domain with only staged pins (no finalized pins)', async () => {
const stagedOnlyDomain = 'staged-only.eth'
// Create staged content but don't finalize
const factory = new CARFactory()
factory.codecs.add(dagCbor)
const car = factory.build()
const rootCid = car.put({ test: 'staged content' }, { isRoot: true, codec: dagCbor })
const stagedCid = await ipfsService.writeCar(car.bytes, stagedOnlyDomain)
// Verify staged pin exists
const stagedPins = await kuboApi.pin.ls({ name: `spg_staged_${stagedOnlyDomain}` })
const stagedPinResults = []
for await (const pin of stagedPins) {
stagedPinResults.push(pin)
}
expect(stagedPinResults.length).toBeGreaterThan(0)
// Prune the page (should not affect staged pins)
await ipfsService.nukePage(stagedOnlyDomain)
// Verify staged pins are still there
const stagedPinsAfter = await kuboApi.pin.ls({ name: `spg_staged_${stagedOnlyDomain}` })
const stagedPinResultsAfter = []
for await (const pin of stagedPinsAfter) {
stagedPinResultsAfter.push(pin)
}
expect(stagedPinResultsAfter.length).toBeGreaterThan(0)
// Verify no final pins exist
const finalPins = await kuboApi.pin.ls({ name: `spg_final_${stagedOnlyDomain}` })
const finalPinResults = []
for await (const pin of finalPins) {
finalPinResults.push(pin)
}
expect(finalPinResults.length).toBe(0)
})
})
describe('Label based ephemeral storage', () => {
it('getLatestBlockNumber and setLatestBlockNumber: should manage latest block number', async () => {
// Initially should be 0
const initialBlock = await ipfsService.getLatestBlockNumber()
expect(initialBlock).toBe(0)
// Set new block number
const newBlockNumber = 12345
await ipfsService.setLatestBlockNumber(newBlockNumber)
// Verify it was set
const currentBlock = await ipfsService.getLatestBlockNumber()
expect(currentBlock).toBe(newBlockNumber)
// Update to new block number
const newerBlockNumber = 12346
await ipfsService.setLatestBlockNumber(newerBlockNumber)
// Verify it was updated
const updatedBlock = await ipfsService.getLatestBlockNumber()
expect(updatedBlock).toBe(newerBlockNumber)
})
describe('List Operations', () => {
it('should handle string lists', async () => {
const listName = 'test-string-list'
const testStrings = ['test1', 'test2', 'test3']
// Add items to list
for (const str of testStrings) {
await ipfsService.addToList(listName, 'string', str)
}
// Get list and verify contents
const list = await ipfsService.getList(listName, 'string')
expect(list.sort()).toEqual(testStrings.sort())
// Remove an item
await ipfsService.removeFromList(listName, 'string', 'test2')
// Verify item was removed
const updatedList = await ipfsService.getList(listName, 'string')
expect(updatedList.sort()).toEqual(['test1', 'test3'].sort())
})
it('should handle address lists', async () => {
const listName = 'test-address-list'
const testAddresses = [
'0x1234567890123456789012345678901234567890',
'0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'
]
// Add addresses to list
for (const addr of testAddresses) {
await ipfsService.addToList(listName, 'address', addr)
}
// Get list and verify contents
const list = await ipfsService.getList(listName, 'address')
expect(list.sort()).toEqual(testAddresses.sort())
// Remove an address
await ipfsService.removeFromList(listName, 'address', testAddresses[0])
// Verify address was removed
const updatedList = await ipfsService.getList(listName, 'address')
expect(updatedList).toEqual([testAddresses[1]])
})
it('should handle number lists', async () => {
const listName = 'test-number-list'
const testNumbers = [1, 2, 3, 4, 5]
// Add numbers to list
for (const num of testNumbers) {
await ipfsService.addToList(listName, 'number', num)
}
// Get list and verify contents
const list = await ipfsService.getList(listName, 'number')
expect(list.sort()).toEqual(testNumbers.sort())
// Remove a number
await ipfsService.removeFromList(listName, 'number', 3)
// Verify number was removed
const updatedList = await ipfsService.getList(listName, 'number')
expect(updatedList.sort()).toEqual([1, 2, 4, 5].sort())
})
it('should not add duplicate items', async () => {
const listName = 'test-duplicate-list'
const testString = 'test-string'
// Add same string twice
await ipfsService.addToList(listName, 'string', testString)
await ipfsService.addToList(listName, 'string', testString)
// Verify only one instance exists
const list = await ipfsService.getList(listName, 'string')
expect(list).toEqual([testString])
})
it('should handle empty lists', async () => {
const listName = 'test-empty-list'
const list = await ipfsService.getList(listName, 'string')
expect(list).toEqual([])
})
it('should throw error for unsupported data types', async () => {
const listName = 'test-unsupported-list'
await expect(ipfsService.addToList(listName, 'unsupported', 'test'))
.rejects.toThrow('Unsupported data type: unsupported')
await expect(ipfsService.getList(listName, 'unsupported'))
.rejects.toThrow('Unsupported data type: unsupported')
})
})
})
})