From 2be475c954b8f1c2d2c8f5da1173aecf46e81144 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Fri, 24 May 2024 11:33:29 -0700 Subject: [PATCH] test: add handle-redirects tests --- .../src/utils/handle-redirects.ts | 13 ++- .../test/utils/handle-redirects.spec.ts | 84 +++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 packages/verified-fetch/test/utils/handle-redirects.spec.ts diff --git a/packages/verified-fetch/src/utils/handle-redirects.ts b/packages/verified-fetch/src/utils/handle-redirects.ts index 90fba6f8..b1234445 100644 --- a/packages/verified-fetch/src/utils/handle-redirects.ts +++ b/packages/verified-fetch/src/utils/handle-redirects.ts @@ -10,6 +10,10 @@ interface GetRedirectResponse { options?: Omit & AbortOptions logger: ComponentLogger + /** + * Only used in testing. + */ + fetch?: typeof globalThis.fetch } function maybeAddTraillingSlash (path: string): string { @@ -21,7 +25,7 @@ function maybeAddTraillingSlash (path: string): string { } // See https://specs.ipfs.tech/http-gateways/path-gateway/#location-response-header -export async function getRedirectResponse ({ resource, options, logger, cid }: GetRedirectResponse): Promise { +export async function getRedirectResponse ({ resource, options, logger, cid, fetch = globalThis.fetch }: GetRedirectResponse): Promise { const log = logger.forComponent('helia:verified-fetch:get-redirect-response') if (typeof resource !== 'string' || options == null || ['ipfs://', 'ipns://'].some((prefix) => resource.startsWith(prefix))) { @@ -81,7 +85,12 @@ export async function getRedirectResponse ({ resource, options, logger, cid }: G throw new Error('subdomain not supported') } } catch (err: any) { - log('subdomain not supported, redirecting to path', err) + log('subdomain not supported', err) + if (pathUrl.href === reqUrl.href) { + log('path url is the same as the request url, not setting location header') + return null + } + // pathUrl is different from request URL (maybe even with just a trailing slash) return movedPermanentlyResponse(resource.toString(), pathUrl.href) } } catch (e) { diff --git a/packages/verified-fetch/test/utils/handle-redirects.spec.ts b/packages/verified-fetch/test/utils/handle-redirects.spec.ts new file mode 100644 index 00000000..173cf4c6 --- /dev/null +++ b/packages/verified-fetch/test/utils/handle-redirects.spec.ts @@ -0,0 +1,84 @@ +import { prefixLogger } from '@libp2p/logger' +import { expect } from 'aegir/chai' +import { CID } from 'multiformats/cid' +import Sinon from 'sinon' +import { getRedirectResponse } from '../../src/utils/handle-redirects.js' + +const logger = prefixLogger('test:handle-redirects') +describe('handle-redirects', () => { + describe('getRedirectResponse', () => { + const sandbox = Sinon.createSandbox() + const cid = CID.parse('bafkqabtimvwgy3yk') + + let fetchStub: Sinon.SinonStub + + beforeEach(() => { + fetchStub = sandbox.stub(globalThis, 'fetch') + }) + + afterEach(() => { + sandbox.restore() + }) + + const nullResponses = [ + { resource: cid, options: {}, logger, cid, testTitle: 'should return null if resource is not a string' }, + { resource: 'http://ipfs.io/ipfs/bafkqabtimvwgy3yk', options: undefined, logger, cid, testTitle: 'should return null if options is undefined' }, + { resource: 'ipfs://', options: {}, logger, cid, testTitle: 'should return null for ipfs:// protocol urls' }, + { resource: 'ipns://', options: {}, logger, cid, testTitle: 'should return null for ipns:// protocol urls' } + ] + + nullResponses.forEach(({ resource, options, logger, cid, testTitle }) => { + it(testTitle, async () => { + const response = await getRedirectResponse({ resource, options, logger, cid }) + expect(response).to.be.null() + }) + }) + + it('should attempt to get the current host from the headers', async () => { + const resource = 'http://ipfs.io/ipfs/bafkqabtimvwgy3yk' + const options = { headers: new Headers({ 'x-forwarded-host': 'localhost:3931' }) } + fetchStub.returns(Promise.resolve(new Response(null, { status: 200 }))) + + const response = await getRedirectResponse({ resource, options, logger, cid, fetch: fetchStub }) + expect(fetchStub.calledOnce).to.be.true() + expect(response).to.not.be.null() + expect(response).to.have.property('status', 301) + const location = response?.headers.get('location') + expect(location).to.equal('http://bafkqabtimvwgy3yk.ipfs.localhost:3931/') + }) + + it('should return redirect response to requested host with trailing slash when HEAD fetch fails', async () => { + const resource = 'http://ipfs.io/ipfs/bafkqabtimvwgy3yk' + const options = { headers: new Headers({ 'x-forwarded-host': 'localhost:3931' }) } + fetchStub.returns(Promise.reject(new Response(null, { status: 404 }))) + + const response = await getRedirectResponse({ resource, options, logger, cid, fetch: fetchStub }) + expect(fetchStub.calledOnce).to.be.true() + expect(response).to.not.be.null() + expect(response).to.have.property('status', 301) + const location = response?.headers.get('location') + // note that the URL returned in location header has trailing slash. + expect(location).to.equal('http://ipfs.io/ipfs/bafkqabtimvwgy3yk/') + }) + + it('should not return redirect response to x-forwarded-host if HEAD fetch fails', async () => { + const resource = 'http://ipfs.io/ipfs/bafkqabtimvwgy3yk/file.txt' + const options = { headers: new Headers({ 'x-forwarded-host': 'localhost:3931' }) } + fetchStub.returns(Promise.reject(new Response(null, { status: 404 }))) + + const response = await getRedirectResponse({ resource, options, logger, cid, fetch: fetchStub }) + expect(fetchStub.calledOnce).to.be.true() + expect(response).to.be.null() + }) + + it('should not return redirect response to x-forwarded-host when HEAD fetch fails and trailing slash already exists', async () => { + const resource = 'http://ipfs.io/ipfs/bafkqabtimvwgy3yk/' + const options = { headers: new Headers({ 'x-forwarded-host': 'localhost:3931' }) } + fetchStub.returns(Promise.reject(new Response(null, { status: 404 }))) + + const response = await getRedirectResponse({ resource, options, logger, cid, fetch: fetchStub }) + expect(fetchStub.calledOnce).to.be.true() + expect(response).to.be.null() + }) + }) +})