diff --git a/packages/verified-fetch/package.json b/packages/verified-fetch/package.json index 08b1affc..aa0ef5cf 100644 --- a/packages/verified-fetch/package.json +++ b/packages/verified-fetch/package.json @@ -90,6 +90,7 @@ "@helia/json": "^3.0.1", "@helia/utils": "^0.0.2", "@ipld/car": "^5.3.0", + "@libp2p/interface-compliance-tests": "^5.3.2", "@libp2p/logger": "^4.0.7", "@libp2p/peer-id-factory": "^4.0.7", "@sgtpooki/file-type": "^1.0.1", diff --git a/packages/verified-fetch/src/utils/parse-url-string.ts b/packages/verified-fetch/src/utils/parse-url-string.ts index d869f77c..1d20f218 100644 --- a/packages/verified-fetch/src/utils/parse-url-string.ts +++ b/packages/verified-fetch/src/utils/parse-url-string.ts @@ -98,15 +98,19 @@ export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStrin resolvedPath = resolveResult?.path log.trace('resolved %s to %c', cidOrPeerIdOrDnsLink, cid) ipnsCache.set(cidOrPeerIdOrDnsLink, resolveResult, 60 * 1000 * 2) - } catch (err) { + } catch (err: any) { log.error('Could not resolve DnsLink for "%s"', cidOrPeerIdOrDnsLink, err) - errors.push(err as Error) + errors.push(err) } } } } if (cid == null) { + if (errors.length === 1) { + throw errors[0] + } + throw new AggregateError(errors, `Invalid resource. Cannot determine CID from URL "${urlString}"`) } @@ -129,26 +133,37 @@ export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStrin } } - /** - * join the path from resolve result & given path. - * e.g. /ipns// that is resolved to /ipfs//, when requested as /ipns//, should be - * resolved to /ipfs/// - */ - const pathParts = [] + return { + protocol, + cid, + path: joinPaths(resolvedPath, urlPath), + query + } +} - if (urlPath.length > 0) { - pathParts.push(urlPath) +/** + * join the path from resolve result & given path. + * e.g. /ipns// that is resolved to /ipfs//, when requested as /ipns//, should be + * resolved to /ipfs/// + */ +function joinPaths (resolvedPath: string | undefined, urlPath: string): string { + let path = '' + + if (resolvedPath != null) { + path += resolvedPath } - if (resolvedPath != null && resolvedPath.length > 0) { - pathParts.push(resolvedPath) + if (urlPath.length > 0) { + path = `${path.length > 0 ? `${path}/` : path}${urlPath}` } - const path = pathParts.join('/') - return { - protocol, - cid, - path, - query + // replace duplicate forward slashes + path = path.replace(/\/(\/)+/g, '/') + + // strip trailing forward slash if present + if (path.startsWith('/')) { + path = path.substring(1) } + + return path } diff --git a/packages/verified-fetch/test/parse-url-string.spec.ts b/packages/verified-fetch/test/parse-url-string.spec.ts deleted file mode 100644 index 6c56f112..00000000 --- a/packages/verified-fetch/test/parse-url-string.spec.ts +++ /dev/null @@ -1,323 +0,0 @@ -import { type PeerId } from '@libp2p/interface' -import { defaultLogger } from '@libp2p/logger' -import { createEd25519PeerId } from '@libp2p/peer-id-factory' -import { expect } from 'aegir/chai' -import { CID } from 'multiformats/cid' -import { stubInterface } from 'sinon-ts' -import { parseUrlString } from '../src/utils/parse-url-string.js' -import type { IPNS } from '@helia/ipns' - -describe('parseUrlString', () => { - describe('invalid URLs', () => { - it('throws for invalid URLs', async () => { - const ipns = stubInterface({}) - try { - await parseUrlString({ - urlString: 'invalid', - ipns, - logger: defaultLogger() - }) - throw new Error('Should have thrown') - } catch (err) { - expect((err as Error).message).to.equal('Invalid URL: invalid, please use ipfs:// or ipns:// URLs only.') - } - }) - - it('throws for invalid protocols', async () => { - const ipns = stubInterface({}) - try { - await parseUrlString({ - urlString: 'http://mydomain.com', - ipns, - logger: defaultLogger() - }) - throw new Error('Should have thrown') - } catch (err) { - expect((err as Error).message).to.equal('Invalid URL: http://mydomain.com, please use ipfs:// or ipns:// URLs only.') - } - }) - - it('throws an error if resulting CID is invalid', async () => { - const ipns = stubInterface({ - // @ts-expect-error - purposefully invalid response - resolveDns: async (_: string) => { - return null - } - }) - try { - await parseUrlString({ - urlString: 'ipns://mydomain.com', - ipns, - logger: defaultLogger() - }) - throw new Error('Should have thrown') - } catch (err) { - expect((err as Error).message).to.equal('Invalid resource. Cannot determine CID from URL "ipns://mydomain.com"') - } - }) - }) - - describe('ipfs:// URLs', () => { - it('handles invalid CIDs', async () => { - const ipns = stubInterface({}) - try { - await parseUrlString({ - urlString: 'ipfs://QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4i', - ipns, - logger: defaultLogger() - }) - throw new Error('Should have thrown') - } catch (aggErr) { - expect(aggErr).to.have.property('message', 'Invalid resource. Cannot determine CID from URL "ipfs://QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4i"') - expect(aggErr).to.have.property('errors').with.lengthOf(1).that.deep.equals([ - new TypeError('Invalid CID for ipfs:// URL') - ]) - } - }) - - it('can parse a URL with CID only', async () => { - const ipns = stubInterface({}) - const result = await parseUrlString({ - urlString: 'ipfs://QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', - ipns, - logger: defaultLogger() - }) - expect(result.protocol).to.equal('ipfs') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('') - }) - - it('can parse URL with CID+path', async () => { - const ipns = stubInterface({}) - const result = await parseUrlString({ - urlString: 'ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', - ipns, - logger: defaultLogger() - }) - expect(result.protocol).to.equal('ipfs') - expect(result.cid.toString()).to.equal('QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm') - expect(result.path).to.equal('1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt') - }) - - it('can parse URL with CID+queryString', async () => { - const ipns = stubInterface({}) - const result = await parseUrlString({ - urlString: 'ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm?format=car', - ipns, - logger: defaultLogger() - }) - expect(result.protocol).to.equal('ipfs') - expect(result.cid.toString()).to.equal('QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm') - expect(result.path).to.equal('') - expect(result.query).to.deep.equal({ format: 'car' }) - }) - - it('can parse URL with CID+path+queryString', async () => { - const ipns = stubInterface({}) - const result = await parseUrlString({ - urlString: 'ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt?format=tar', - ipns, - logger: defaultLogger() - }) - expect(result.protocol).to.equal('ipfs') - expect(result.cid.toString()).to.equal('QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm') - expect(result.path).to.equal('1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt') - expect(result.query).to.deep.equal({ format: 'tar' }) - }) - }) - - describe('ipns:// URLs', () => { - let ipns: IPNS - - beforeEach(async () => { - ipns = stubInterface({ - resolveDns: async (dnsLink: string) => { - expect(dnsLink).to.equal('mydomain.com') - return { - cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), - path: '' - } - } - }) - }) - - it('handles invalid DNSLinkDomains', async () => { - ipns = stubInterface({ - resolve: async (peerId: PeerId) => { - throw new Error('Unexpected failure from ipns resolve method') - }, - resolveDns: async (_: string) => { - return Promise.reject(new Error('Unexpected failure from dns query')) - } - }) - - try { - await parseUrlString({ urlString: 'ipns://mydomain.com', ipns, logger: defaultLogger() }) - throw new Error('Should have thrown') - } catch (aggErr) { - expect(aggErr).to.have.property('message', 'Invalid resource. Cannot determine CID from URL "ipns://mydomain.com"') - expect(aggErr).to.have.property('errors').with.lengthOf(2).that.deep.equals([ - new TypeError('Could not parse PeerId in ipns url "mydomain.com", Non-base64 character'), - new Error('Unexpected failure from dns query') - ]) - } - }) - - it('can parse a URL with DNSLinkDomain only', async () => { - const result = await parseUrlString({ - urlString: 'ipns://mydomain.com', - ipns, - logger: defaultLogger() - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('') - }) - - it('can parse a URL with DNSLinkDomain+path', async () => { - const result = await parseUrlString({ - urlString: 'ipns://mydomain.com/some/path/to/file.txt', - ipns, - logger: defaultLogger() - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('some/path/to/file.txt') - }) - - it('can parse a URL with DNSLinkDomain+queryString', async () => { - const result = await parseUrlString({ - urlString: 'ipns://mydomain.com?format=json', - ipns, - logger: defaultLogger() - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('') - expect(result.query).to.deep.equal({ format: 'json' }) - }) - - it('can parse a URL with DNSLinkDomain+path+queryString', async () => { - const result = await parseUrlString({ - urlString: 'ipns://mydomain.com/some/path/to/file.txt?format=json', - ipns, - logger: defaultLogger() - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('some/path/to/file.txt') - expect(result.query).to.deep.equal({ format: 'json' }) - }) - }) - - describe('ipns:// URLs', () => { - let ipns: IPNS - let testPeerId: PeerId - - beforeEach(async () => { - testPeerId = await createEd25519PeerId() - ipns = stubInterface({ - resolve: async (peerId: PeerId) => { - expect(peerId.toString()).to.equal(testPeerId.toString()) - return { - cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), - path: '' - } - } - }) - }) - - it('handles invalid PeerIds', async () => { - ipns = stubInterface({ - resolve: async (peerId: PeerId) => { - throw new Error('Unexpected failure from ipns resolve method') - }, - resolveDns: async (_: string) => { - return Promise.reject(new Error('Unexpected failure from dns query')) - } - }) - - try { - await parseUrlString({ urlString: 'ipns://123PeerIdIsFake456', ipns, logger: defaultLogger() }) - throw new Error('Should have thrown') - } catch (aggErr) { - expect(aggErr).to.have.property('message', 'Invalid resource. Cannot determine CID from URL "ipns://123PeerIdIsFake456"') - expect(aggErr).to.have.property('errors').with.lengthOf(2).that.deep.equals([ - new TypeError('Could not parse PeerId in ipns url "123PeerIdIsFake456", Non-base58btc character'), - new Error('Unexpected failure from dns query') - ]) - } - }) - - it('handles valid PeerId resolve failures', async () => { - ipns = stubInterface({ - resolve: async (_: PeerId) => { - return Promise.reject(new Error('Unexpected failure from ipns resolve method')) - }, - resolveDns: async (_: string) => { - return Promise.reject(new Error('Unexpected failure from dns query')) - } - }) - - // await expect(parseUrlString({ urlString: `ipns://${testPeerId.toString()}`, ipns })).to.eventually.be.rejected() - // .with.property('message', `Could not resolve PeerId "${testPeerId.toString()}", Unexpected failure from ipns resolve method`) - - try { - await parseUrlString({ urlString: `ipns://${testPeerId.toString()}`, ipns, logger: defaultLogger() }) - throw new Error('Should have thrown') - } catch (aggErr) { - expect(aggErr).to.have.property('message', `Invalid resource. Cannot determine CID from URL "ipns://${testPeerId.toString()}"`) - expect(aggErr).to.have.property('errors').with.lengthOf(2).that.deep.equals([ - new TypeError(`Could not resolve PeerId "${testPeerId.toString()}", Unexpected failure from ipns resolve method`), - new Error('Unexpected failure from dns query') - ]) - } - }) - - it('can parse a URL with PeerId only', async () => { - const result = await parseUrlString({ - urlString: `ipns://${testPeerId.toString()}`, - ipns, - logger: defaultLogger() - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('') - }) - - it('can parse a URL with PeerId+path', async () => { - const result = await parseUrlString({ - urlString: `ipns://${testPeerId.toString()}/some/path/to/file.txt`, - ipns, - logger: defaultLogger() - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('some/path/to/file.txt') - }) - - it('can parse a URL with PeerId+queryString', async () => { - const result = await parseUrlString({ - urlString: `ipns://${testPeerId.toString()}?fomat=dag-cbor`, - ipns, - logger: defaultLogger() - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('') - expect(result.query).to.deep.equal({ fomat: 'dag-cbor' }) - }) - - it('can parse a URL with PeerId+path+queryString', async () => { - const result = await parseUrlString({ - urlString: `ipns://${testPeerId.toString()}/some/path/to/file.txt?fomat=dag-cbor`, - ipns, - logger: defaultLogger() - }) - expect(result.protocol).to.equal('ipns') - expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') - expect(result.path).to.equal('some/path/to/file.txt') - expect(result.query).to.deep.equal({ fomat: 'dag-cbor' }) - }) - }) -}) diff --git a/packages/verified-fetch/test/utils/parse-url-string.spec.ts b/packages/verified-fetch/test/utils/parse-url-string.spec.ts new file mode 100644 index 00000000..a77ff430 --- /dev/null +++ b/packages/verified-fetch/test/utils/parse-url-string.spec.ts @@ -0,0 +1,372 @@ +import { matchPeerId } from '@libp2p/interface-compliance-tests/matchers' +import { defaultLogger } from '@libp2p/logger' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { CID } from 'multiformats/cid' +import { stubInterface } from 'sinon-ts' +import { parseUrlString } from '../../src/utils/parse-url-string.js' +import type { IPNS } from '@helia/ipns' +import type { ComponentLogger, PeerId } from '@libp2p/interface' +import type { StubbedInstance } from 'sinon-ts' + +describe('parseUrlString', () => { + let logger: ComponentLogger + let ipns: StubbedInstance + + beforeEach(() => { + logger = defaultLogger() + ipns = stubInterface() + }) + + describe('invalid URLs', () => { + it('throws for invalid URLs', async () => { + await expect( + parseUrlString({ + urlString: 'invalid', + ipns, + logger + }) + ).to.eventually.be.rejected + .with.property('message', 'Invalid URL: invalid, please use ipfs:// or ipns:// URLs only.') + }) + + it('throws for invalid protocols', async () => { + await expect( + parseUrlString({ + urlString: 'invalid', + ipns, + logger + }) + ).to.eventually.be.rejected + .with.property('message', 'Invalid URL: invalid, please use ipfs:// or ipns:// URLs only.') + }) + + it('throws an error if resulting CID is invalid', async () => { + // @ts-expect-error - purposefully invalid response + ipns.resolveDns.returns(null) + + await expect( + parseUrlString({ + urlString: 'ipns://mydomain.com', + ipns, + logger + }) + ).to.eventually.be.rejected + .with.property('message', 'Could not parse PeerId in ipns url "mydomain.com", Non-base64 character') + }) + }) + + describe('ipfs:// URLs', () => { + it('handles invalid CIDs', async () => { + await expect( + parseUrlString({ + urlString: 'ipfs://QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4i', + ipns, + logger + }) + ).to.eventually.be.rejected + .with.property('message', 'Invalid CID for ipfs:// URL') + }) + + it('can parse a URL with CID only', async () => { + const result = await parseUrlString({ + urlString: 'ipfs://QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr', + ipns, + logger + }) + expect(result.protocol).to.equal('ipfs') + expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') + expect(result.path).to.equal('') + }) + + it('can parse URL with CID+path', async () => { + const result = await parseUrlString({ + urlString: 'ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt', + ipns, + logger + }) + expect(result.protocol).to.equal('ipfs') + expect(result.cid.toString()).to.equal('QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm') + expect(result.path).to.equal('1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt') + }) + + it('can parse URL with CID+queryString', async () => { + const result = await parseUrlString({ + urlString: 'ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm?format=car', + ipns, + logger + }) + expect(result.protocol).to.equal('ipfs') + expect(result.cid.toString()).to.equal('QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm') + expect(result.path).to.equal('') + expect(result.query).to.deep.equal({ format: 'car' }) + }) + + it('can parse URL with CID+path+queryString', async () => { + const result = await parseUrlString({ + urlString: 'ipfs://QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt?format=tar', + ipns, + logger + }) + expect(result.protocol).to.equal('ipfs') + expect(result.cid.toString()).to.equal('QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm') + expect(result.path).to.equal('1 - Barrel - Part 1/1 - Barrel - Part 1 - alt.txt') + expect(result.query).to.deep.equal({ format: 'tar' }) + }) + }) + + describe('ipns:// URLs', () => { + it('handles invalid DNSLinkDomains', async () => { + ipns.resolve.rejects(new Error('Unexpected failure from ipns resolve method')) + ipns.resolveDns.rejects(new Error('Unexpected failure from ipns dns query')) + + await expect(parseUrlString({ urlString: 'ipns://mydomain.com', ipns, logger })).to.eventually.be.rejected + .with.property('errors').that.deep.equals([ + new TypeError('Could not parse PeerId in ipns url "mydomain.com", Non-base64 character'), + new Error('Unexpected failure from ipns dns query') + ]) + }) + + it('can parse a URL with DNSLinkDomain only', async () => { + ipns.resolveDns.withArgs('mydomain.com').resolves({ + cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), + path: '' + }) + + const result = await parseUrlString({ + urlString: 'ipns://mydomain.com', + ipns, + logger + }) + expect(result.protocol).to.equal('ipns') + expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') + expect(result.path).to.equal('') + }) + + it('can parse a URL with DNSLinkDomain+path', async () => { + ipns.resolveDns.withArgs('mydomain.com').resolves({ + cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), + path: '' + }) + + const result = await parseUrlString({ + urlString: 'ipns://mydomain.com/some/path/to/file.txt', + ipns, + logger + }) + expect(result.protocol).to.equal('ipns') + expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') + expect(result.path).to.equal('some/path/to/file.txt') + }) + + it('can parse a URL with DNSLinkDomain+queryString', async () => { + ipns.resolveDns.withArgs('mydomain.com').resolves({ + cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), + path: '' + }) + + const result = await parseUrlString({ + urlString: 'ipns://mydomain.com?format=json', + ipns, + logger + }) + expect(result.protocol).to.equal('ipns') + expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') + expect(result.path).to.equal('') + expect(result.query).to.deep.equal({ format: 'json' }) + }) + + it('can parse a URL with DNSLinkDomain+path+queryString', async () => { + ipns.resolveDns.withArgs('mydomain.com').resolves({ + cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), + path: '' + }) + + const result = await parseUrlString({ + urlString: 'ipns://mydomain.com/some/path/to/file.txt?format=json', + ipns, + logger: defaultLogger() + }) + expect(result.protocol).to.equal('ipns') + expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') + expect(result.path).to.equal('some/path/to/file.txt') + expect(result.query).to.deep.equal({ format: 'json' }) + }) + }) + + describe('ipns:// URLs', () => { + let testPeerId: PeerId + + beforeEach(async () => { + testPeerId = await createEd25519PeerId() + }) + + it('handles invalid PeerIds', async () => { + ipns.resolve.rejects(new Error('Unexpected failure from ipns resolve method')) + ipns.resolveDns.rejects(new Error('Unexpected failure from ipns dns query')) + + await expect(parseUrlString({ urlString: 'ipns://123PeerIdIsFake456', ipns, logger })).to.eventually.be.rejected + .with.property('errors').that.deep.equals([ + new TypeError('Could not parse PeerId in ipns url "123PeerIdIsFake456", Non-base58btc character'), + new Error('Unexpected failure from ipns dns query') + ]) + }) + + it('handles valid PeerId resolve failures', async () => { + ipns.resolve.rejects(new Error('Unexpected failure from ipns resolve method')) + ipns.resolveDns.rejects(new Error('Unexpected failure from ipns dns query')) + + await expect(parseUrlString({ urlString: `ipns://${testPeerId}`, ipns, logger })).to.eventually.be.rejected + .with.property('errors').that.deep.equals([ + new TypeError(`Could not resolve PeerId "${testPeerId}", Unexpected failure from ipns resolve method`), + new Error('Unexpected failure from ipns dns query') + ]) + }) + + it('can parse a URL with PeerId only', async () => { + ipns.resolve.withArgs(matchPeerId(testPeerId)).resolves({ + cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), + path: '' + }) + const result = await parseUrlString({ + urlString: `ipns://${testPeerId}`, + ipns, + logger + }) + expect(result.protocol).to.equal('ipns') + expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') + expect(result.path).to.equal('') + }) + + it('can parse a URL with PeerId+path', async () => { + ipns.resolve.withArgs(matchPeerId(testPeerId)).resolves({ + cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), + path: '' + }) + const result = await parseUrlString({ + urlString: `ipns://${testPeerId}/some/path/to/file.txt`, + ipns, + logger + }) + expect(result.protocol).to.equal('ipns') + expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') + expect(result.path).to.equal('some/path/to/file.txt') + }) + + it('can parse a URL with PeerId+path with a trailing slash', async () => { + ipns.resolve.withArgs(matchPeerId(testPeerId)).resolves({ + cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), + path: '' + }) + const result = await parseUrlString({ + urlString: `ipns://${testPeerId}/some/path/to/dir/`, + ipns, + logger + }) + expect(result.protocol).to.equal('ipns') + expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') + expect(result.path).to.equal('some/path/to/dir/') + }) + + it('can parse a URL with PeerId+queryString', async () => { + ipns.resolve.withArgs(matchPeerId(testPeerId)).resolves({ + cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), + path: '' + }) + const result = await parseUrlString({ + urlString: `ipns://${testPeerId}?fomat=dag-cbor`, + ipns, + logger + }) + expect(result.protocol).to.equal('ipns') + expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') + expect(result.path).to.equal('') + expect(result.query).to.deep.equal({ fomat: 'dag-cbor' }) + }) + + it('can parse a URL with PeerId+path+queryString', async () => { + ipns.resolve.withArgs(matchPeerId(testPeerId)).resolves({ + cid: CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'), + path: '' + }) + const result = await parseUrlString({ + urlString: `ipns://${testPeerId}/some/path/to/file.txt?fomat=dag-cbor`, + ipns, + logger + }) + expect(result.protocol).to.equal('ipns') + expect(result.cid.toString()).to.equal('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') + expect(result.path).to.equal('some/path/to/file.txt') + expect(result.query).to.deep.equal({ fomat: 'dag-cbor' }) + }) + + it('should parse an ipns:// url with a path that resolves to a CID with a path', async () => { + const cid = CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') + const peerId = await createEd25519PeerId() + const recordPath = 'foo' + const requestPath = 'bar/baz.txt' + + ipns.resolve.withArgs(matchPeerId(peerId)).resolves({ + cid, + path: recordPath + }) + + await expect(parseUrlString({ + urlString: `ipns://${peerId}/${requestPath}`, + ipns, + logger + })).to.eventually.deep.equal({ + protocol: 'ipns', + path: `${recordPath}/${requestPath}`, + cid, + query: {} + }) + }) + + it('should parse an ipns:// url with a path that resolves to a CID with a path with a trailing slash', async () => { + const cid = CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') + const peerId = await createEd25519PeerId() + const recordPath = 'foo/' + const requestPath = 'bar/baz.txt' + + ipns.resolve.withArgs(matchPeerId(peerId)).resolves({ + cid, + path: recordPath + }) + + await expect(parseUrlString({ + urlString: `ipns://${peerId}/${requestPath}`, + ipns, + logger + })).to.eventually.deep.equal({ + protocol: 'ipns', + path: 'foo/bar/baz.txt', + cid, + query: {} + }) + }) + + it('should parse an ipns:// url with a path that resolves to a CID with a path with a trailing slash', async () => { + const cid = CID.parse('QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr') + const peerId = await createEd25519PeerId() + const recordPath = '/foo/////bar//' + const requestPath = '///baz///qux.txt' + + ipns.resolve.withArgs(matchPeerId(peerId)).resolves({ + cid, + path: recordPath + }) + + await expect(parseUrlString({ + urlString: `ipns://${peerId}/${requestPath}`, + ipns, + logger + })).to.eventually.deep.equal({ + protocol: 'ipns', + path: 'foo/bar/baz/qux.txt', + cid, + query: {} + }) + }) + }) +})