text:
'use strict'
import { describe, it, before, beforeEach, after } from 'mocha'
import sinon from 'sinon'
import { expect } from 'chai'
import { URL } from 'url'
import browser from 'sinon-chrome'
import { initState } from '../../../add-on/src/lib/state.js'
import createRuntimeChecks from '../../../add-on/src/lib/runtime-checks.js'
import { createRequestModifier, redirectOptOutHint } from '../../../add-on/src/lib/ipfs-request.js'
import createDnslinkResolver from '../../../add-on/src/lib/dnslink.js'
import { createIpfsPathValidator } from '../../../add-on/src/lib/ipfs-path.js'
import { optionDefaults } from '../../../add-on/src/lib/options.js'

const url2request = (string) => {
  return { url: string, type: 'main_frame' }
}

const fakeRequestId = () => {
  return Math.floor(Math.random() * 100000).toString()
}

const expectNoRedirect = (modifyRequest, request) => {
  expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
  expect(modifyRequest.onHeadersReceived(request)).to.equal(undefined)
}

const nodeTypes = ['external', 'embedded']

describe('modifyRequest.onBeforeRequest:', function () {
  let state, dnslinkResolver, ipfsPathValidator, modifyRequest, runtime

  before(function () {
    global.URL = URL
    global.browser = browser
    browser.runtime.id = 'testid'
    browser.runtime.getURL.returns('chrome-extension://testid/')
  })

  beforeEach(async function () {
    state = Object.assign(initState(optionDefaults), {
      ipfsNodeType: 'external',
      peerCount: 1,
      redirect: true,
      dnslinkPolicy: false, // dnslink testi suite is in ipfs-request-dnslink.test.js
      catchUnhandledProtocols: true,
      gwURLString: 'http://localhost:8080',
      gwURL: new URL('http://localhost:8080'),
      pubGwURLString: 'https://ipfs.io',
      pubGwURL: new URL('https://ipfs.io'),
      pubSubdomainGwURLString: 'https://dweb.link',
      pubSubdomainGwURL: new URL('https://dweb.link')
    })
    const getState = () => state
    const getIpfs = () => {}
    dnslinkResolver = createDnslinkResolver(getState)
    runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
    ipfsPathValidator = createIpfsPathValidator(getState, getIpfs, dnslinkResolver)
    modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
  })

  describe('request for a path matching /ipfs/{CIDv0}', function () {
    describe('with external node', function () {
      beforeEach(function () {
        state.ipfsNodeType = 'external'
      })
      it('should be served from custom gateway if redirect is enabled', function () {
        const request = url2request('https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
        expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      })
    })
    describe('with embedded node', function () {
      beforeEach(function () {
        state.ipfsNodeType = 'embedded'
      })
      it('should be served from public gateway if redirect is enabled', function () {
        const request = url2request('https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
        expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      })
    })
    describe('with every node type', function () {
      // tests in which results should be the same for all node types
      nodeTypes.forEach(function (nodeType) {
        beforeEach(function () {
          state.ipfsNodeType = nodeType
        })
        it(`should be left untouched if redirect is disabled (${nodeType} node)`, function () {
          state.redirect = false
          const request = url2request('https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
          expectNoRedirect(modifyRequest, request)
        })
        it(`should be left untouched if redirect is enabled but global active flag is OFF (${nodeType} node)`, function () {
          state.active = false
          state.redirect = true
          const request = url2request('https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
          expectNoRedirect(modifyRequest, request)
        })
        it(`should be left untouched if URL includes opt-out hint (${nodeType} node)`, function () {
          // A safe way for preloading data at arbitrary gateways - it should arrive at original destination
          const request = url2request('https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?x-ipfs-companion-no-redirect#hashTest')
          expectNoRedirect(modifyRequest, request)
          expect(redirectOptOutHint).to.equal('x-ipfs-companion-no-redirect')
        })
        it(`should be left untouched if request is for subresource on a page loaded from URL that includes opt-out hint (${nodeType} node)`, function () {
          // ensure opt-out works for subresources (Firefox only for now)
          const subRequest = {
            type: 'script',
            url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest',
            originUrl: 'https://example.com/?x-ipfs-companion-no-redirect#hashTest'
          }
          expectNoRedirect(modifyRequest, subRequest)
        })
        it(`should be left untouched if CID is invalid (${nodeType} node)`, function () {
          const request = url2request('https://google.com/ipfs/notacid?argTest#hashTest')
          expectNoRedirect(modifyRequest, request)
        })
        it(`should be left untouched if its is a HEAD preload with explicit opt-out in URL hash (${nodeType} node)`, function () {
          // HTTP HEAD is a popular way for preloading data at arbitrary gateways, so we have a dedicated test to make sure it works as expected
          const headRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#x-ipfs-companion-no-redirect', method: 'HEAD' }
          expectNoRedirect(modifyRequest, headRequest)
        })
      })
    })
  })

  describe('XHR request for a path matching /ipfs/{CIDv0} coming from 3rd party Origin', function () {
    describe('with external node', function () {
      beforeEach(function () {
        state.ipfsNodeType = 'external'
      })
      it('should be served from custom gateway if fetched from the same origin and redirect is enabled in Firefox', function () {
        runtime.isFirefox = true
        const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://google.com/' }
        expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      })
      it('should be served from custom gateway if fetched from the same origin and redirect is enabled in Chromium', function () {
        runtime.isFirefox = false
        const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://google.com/' }
        expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      })
      it('should be served from custom gateway if XHR is cross-origin and redirect is enabled in Chromium', function () {
        runtime.isFirefox = false
        const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
        expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      })
      it('should be served from custom gateway if XHR is cross-origin and redirect is enabled in Firefox', function () {
        runtime.isFirefox = true
        const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
        expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      })
    })
    describe('with embedded node', function () {
      beforeEach(function () {
        state.ipfsNodeType = 'embedded'
      })
      it('should be served from public gateway if fetched from the same origin and redirect is enabled in Firefox', function () {
        runtime.isFirefox = true
        const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://google.com/' }
        expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      })
      it('should be served from public gateway if fetched from the same origin and redirect is enabled in Chromium', function () {
        runtime.isFirefox = false
        const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://google.com/' }
        expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      })
      it('should be served from public gateway if XHR is cross-origin and redirect is enabled in Chromium', function () {
        runtime.isFirefox = false
        const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
        expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      })
      it('should be served from public gateway if XHR is cross-origin and redirect is enabled in Firefox', function () {
        runtime.isFirefox = true
        const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
        expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      })
    })
    describe('with external node when runtime.requiresXHRCORSfix', function () {
      beforeEach(function () {
        state.ipfsNodeType = 'external'
        browser.runtime.getBrowserInfo = () => Promise.resolve({ name: 'Firefox', version: '68.0.0' })
      })
      it('should be served from custom gateway if fetched from the same origin and redirect is enabled in Firefox', function () {
        runtime.isFirefox = true
        const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://google.com/' }
        expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      })
      it('should be served from custom gateway if fetched from the same origin and redirect is enabled in non-Firefox', function () {
        runtime.isFirefox = false
        const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://google.com/' }
        expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      })
      it('should be served from custom gateway if XHR is cross-origin and redirect is enabled in non-Firefox', function () {
        runtime.isFirefox = false
        const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
        expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      })
      it('should be served from custom gateway via late redirect in onHeadersReceived if XHR is cross-origin and redirect is enabled in Firefox', function () {
        // Context for CORS XHR problems in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
        runtime.isFirefox = true
        const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
        // onBeforeRequest should not change anything, as it will trigger false-positive CORS error
        expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
        // onHeadersReceived is after CORS validation happens, so its ok to cancel and redirect late
        expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      })
    })
  })

  describe('request for a path matching /ipns/{path}', function () {
    describe('with external node', function () {
      beforeEach(function () {
        state.ipfsNodeType = 'external'
      })
      it('should be served from custom gateway if {path} points to a FQDN with existing dnslink', function () {
        const request = url2request('https://google.com/ipns/en.wikipedia-on-ipfs.org/index.html?argTest#hashTest')
        // stub the existence of valid dnslink
        const fqdn = 'en.wikipedia-on-ipfs.org'
        dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/Qmazvovg6Sic3m9igZMKoAPjkiVZsvbWWc8ZvgjjK1qMss')
        // pretend API is online and we can do dns lookups with it
        state.peerCount = 1
        expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('http://localhost:8080/ipns/en.wikipedia-on-ipfs.org/index.html?argTest#hashTest')
      })
      it('should be served from custom gateway if {path} starts with a valid PeerID', function () {
        const request = url2request('https://google.com/ipns/QmSWnBwMKZ28tcgMFdihD8XS7p6QzdRSGf71cCybaETSsU/index.html?argTest#hashTest')
        dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().returns(false)
        expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('http://localhost:8080/ipns/QmSWnBwMKZ28tcgMFdihD8XS7p6QzdRSGf71cCybaETSsU/index.html?argTest#hashTest')
      })
    })

    describe('with embedded node', function () {
      beforeEach(function () {
        state.ipfsNodeType = 'embedded'
      })
      it('should be served from public gateway if {path} points to a FQDN with existing dnslink', function () {
        const request = url2request('https://google.com/ipns/en.wikipedia-on-ipfs.org/index.html?argTest#hashTest')
        // stub the existence of valid dnslink
        const fqdn = 'en.wikipedia-on-ipfs.org'
        dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/Qmazvovg6Sic3m9igZMKoAPjkiVZsvbWWc8ZvgjjK1qMss')
        // pretend API is online and we can do dns lookups with it
        state.peerCount = 1
        expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipns/en.wikipedia-on-ipfs.org/index.html?argTest#hashTest')
      })
      it('should be served from public gateway if {path} starts with a valid PeerID', function () {
        const request = url2request('https://google.com/ipns/QmSWnBwMKZ28tcgMFdihD8XS7p6QzdRSGf71cCybaETSsU/index.html?argTest#hashTest')
        dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().returns(false)
        expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipns/QmSWnBwMKZ28tcgMFdihD8XS7p6QzdRSGf71cCybaETSsU/index.html?argTest#hashTest')
      })
    })

    describe('with every node type', function () {
      // tests in which results should be the same for all node types
      nodeTypes.forEach(function (nodeType) {
        beforeEach(function () {
          state.ipfsNodeType = nodeType
        })
        it(`should be left untouched if redirect is disabled' (${nodeType} node)`, function () {
          state.redirect = false
          const request = url2request('https://google.com/ipns/ipfs.io?argTest#hashTest')
          expectNoRedirect(modifyRequest, request)
        })
        it(`should be left untouched if FQDN is not a real domain nor a valid CID (${nodeType} node)`, function () {
          const request = url2request('https://google.com/ipns/notafqdnorcid?argTest#hashTest')
          dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().returns(false)
          expectNoRedirect(modifyRequest, request)
        })
      })
    })
  })

  describe('request to a subdomain gateway', function () {
    const cid = 'bafybeigxjv2o4jse2lajbd5c7xxl5rluhyqg5yupln42252e5tcao7hbge'
    const peerid = 'bafzbeigxjv2o4jse2lajbd5c7xxl5rluhyqg5yupln42252e5tcao7hbge'

    // Tests use different CID in X-Ipfs-Path header just to ensure it does not
    // override the one from path
    const fakeXIpfsPathHdrVal = '/ipfs/QmPhnvn747LqwPYMJmQVorMaGbMSgA7mRRoyyZYz3DoZRQ'

    describe('with external node', function () {
      beforeEach(function () {
        state.ipfsNodeType = 'external'
        // dweb.link is the default subdomain gw
      })
      it('should be redirected to localhost gateway (*.ipfs on default gw)', function () {
        state.redirect = true
        const request = url2request(`https://${cid}.ipfs.dweb.link/`)

        // X-Ipfs-Path to ensure value from URL takes a priority
        request.responseHeaders = [{ name: 'X-Ipfs-Path', value: fakeXIpfsPathHdrVal }]

        /// We expect redirect to path-based gateway because go-ipfs >=0.5 will
        // return redirect to a subdomain, and we don't want to break users
        // running older versions of go-ipfs by loading subdomain first and
        // failing.
        expect(modifyRequest.onBeforeRequest(request).redirectUrl)
          .to.equal(`http://localhost:8080/ipfs/${cid}/`)
      })
      it('should be redirected to localhost gateway (*.ipfs on 3rd party gw)', function () {
        state.redirect = true
        const request = url2request(`https://${cid}.ipfs.cf-ipfs.com/`)
        request.responseHeaders = [{ name: 'X-Ipfs-Path', value: fakeXIpfsPathHdrVal }]
        expect(modifyRequest.onBeforeRequest(request).redirectUrl)
          .to.equal(`http://localhost:8080/ipfs/${cid}/`)
      })
      it('should be redirected to localhost gateway and keep URL encoding of original path', function () {
        state.redirect = true
        const request = url2request('https://bafybeigfejjsuq5im5c3w3t3krsiytszhfdc4v5myltcg4myv2n2w6jumy.ipfs.dweb.link/%3Ffilename=test.jpg?arg=val')
        request.responseHeaders = [{ name: 'X-Ipfs-Path', value: fakeXIpfsPathHdrVal }]
        expect(modifyRequest.onBeforeRequest(request).redirectUrl)
          .to.equal('http://localhost:8080/ipfs/bafybeigfejjsuq5im5c3w3t3krsiytszhfdc4v5myltcg4myv2n2w6jumy/%3Ffilename=test.jpg?arg=val')
      })
      it('should be redirected to localhost gateway (*.ipns on default gw)', function () {
        state.redirect = true
        const request = url2request(`https://${peerid}.ipns.dweb.link/`)
        request.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipns/QmPhnvn747LqwPYMJmQVorMaGbMSgA7mRRoyyZYz3DoZRQ' }]
        expect(modifyRequest.onBeforeRequest(request).redirectUrl)
          .to.equal(`http://localhost:8080/ipns/${peerid}/`)
      })
    })

    describe('with embedded node', function () {
      beforeEach(function () {
        state.ipfsNodeType = 'embedded'
        // dweb.link is the default subdomain gw
      })
      it('should be left untouched for *.ipfs at default public subdomain gw', function () {
        state.redirect = true
        const request = url2request(`https://${cid}.ipfs.dweb.link/`)
        request.responseHeaders = [{ name: 'X-Ipfs-Path', value: fakeXIpfsPathHdrVal }]
        expectNoRedirect(modifyRequest, request)
      })
      it('should be redirected to user-prefered public gateway if 3rd party subdomain gw', function () {
        state.redirect = true
        const request = url2request(`https://${cid}.ipfs.cf-ipfs.com/`)
        request.responseHeaders = [{ name: 'X-Ipfs-Path', value: fakeXIpfsPathHdrVal }]
        expect(modifyRequest.onBeforeRequest(request).redirectUrl)
          .to.equal(`https://${cid}.ipfs.dweb.link/`)
      })
      it('should be left untouched for *.ipns at default public subdomain gw', function () {
        state.redirect = true
        const request = url2request(`https://${peerid}.ipns.dweb.link/`)
        request.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipns/QmPhnvn747LqwPYMJmQVorMaGbMSgA7mRRoyyZYz3DoZRQ' }]
        expectNoRedirect(modifyRequest, request)
      })
    })
  })

  // tests in which results should be the same for all node types
  nodeTypes.forEach(function (nodeType) {
    beforeEach(function () {
      state.ipfsNodeType = nodeType
      state.redirect = true
    })
    describe(`with ${nodeType} node:`, function () {
      describe('request for IPFS path at the localhost', function () {
        // we do not touch local requests, as it may interfere with other nodes running at the same machine
        // or could produce false-positives such as redirection from localhost:5001/ipfs/path to localhost:8080/ipfs/path
        it('should fix localhost Kubo RPC hostname to IP', function () {
          const request = url2request('http://localhost:5001/ipfs/QmPhnvn747LqwPYMJmQVorMaGbMSgA7mRRoyyZYz3DoZRQ/')
          // expectNoRedirect(modifyRequest, request)
          expect(modifyRequest.onBeforeRequest(request).redirectUrl)
            .to.equal('http://127.0.0.1:5001/ipfs/QmPhnvn747LqwPYMJmQVorMaGbMSgA7mRRoyyZYz3DoZRQ/')
        })
        it('should be left untouched if localhost Gateway is used', function () {
          const request = url2request('http://localhost:8080/ipfs/QmPhnvn747LqwPYMJmQVorMaGbMSgA7mRRoyyZYz3DoZRQ/')
          expectNoRedirect(modifyRequest, request)
        })
        it('should fix 127.0.0.1 Gateway to localhost', function () {
          const request = url2request('http://127.0.0.1:8080/ipfs/QmPhnvn747LqwPYMJmQVorMaGbMSgA7mRRoyyZYz3DoZRQ/')
          // expectNoRedirect(modifyRequest, request)
          expect(modifyRequest.onBeforeRequest(request).redirectUrl)
            .to.equal('http://localhost:8080/ipfs/QmPhnvn747LqwPYMJmQVorMaGbMSgA7mRRoyyZYz3DoZRQ/')
        })
        it('should fix 0.0.0.0 to localhost IP API', function () {
          // https://github.com/ipfs-shipyard/ipfs-companion/issues/867
          const request = url2request('http://0.0.0.0:5001/ipfs/QmPhnvn747LqwPYMJmQVorMaGbMSgA7mRRoyyZYz3DoZRQ/')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl)
            .to.equal('http://127.0.0.1:5001/ipfs/QmPhnvn747LqwPYMJmQVorMaGbMSgA7mRRoyyZYz3DoZRQ/')
        })
        it('should be left untouched if /webui on localhost Kubo RPC port', function () {
          // https://github.com/ipfs/ipfs-companion/issues/291
          const request = url2request('http://localhost:5001/webui')
          // expectNoRedirect(modifyRequest, request)
          expect(modifyRequest.onBeforeRequest(request).redirectUrl)
            .to.equal('http://127.0.0.1:5001/webui')
        })
        it('should be left untouched if localhost Kubo RPC IP is used, even when x-ipfs-path is present', function () {
          // https://github.com/ipfs-shipyard/ipfs-companion/issues/604
          const request = url2request('http://127.0.0.1:5001/ipfs/QmPhnvn747LqwPYMJmQVorMaGbMSgA7mRRoyyZYz3DoZRQ/')
          request.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipfs/QmPhnvn747LqwPYMJmQVorMaGbMSgA7mRRoyyZYz3DDIFF' }]
          expectNoRedirect(modifyRequest, request)
        })
        it('should be left untouched if [::1] is used', function () {
          // https://github.com/ipfs/ipfs-companion/issues/291
          const request = url2request('http://[::1]:5001/ipfs/QmPhnvn747LqwPYMJmQVorMaGbMSgA7mRRoyyZYz3DoZRQ/')
          expectNoRedirect(modifyRequest, request)
        })
        it('should be redirected to localhost (subdomain in go-ipfs >0.5) if type=main_frame and  127.0.0.1 (path gw) is used un URL', function () {
          state.redirect = true
          state.useSubdomains = true
          expect(state.gwURL.hostname).to.equal('localhost')
          const cid = 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
          const request = url2request(`http://127.0.0.1:8080/ipfs/${cid}?arg=val#hash`)
          request.type = 'main_frame' // explicit
          expect(modifyRequest.onBeforeRequest(request).redirectUrl)
            .to.equal(`http://localhost:8080/ipfs/${cid}?arg=val#hash`)
        })
      })
    })
  })

  describe('request for IPFS Path with redirect to custom gateway at port 80', function () {
    beforeEach(function () {
      state.ipfsNodeType = 'external'
      state.redirect = true
    })
    it('should work for HTTP GW without explicit port in URL', function () {
      state.gwURLString = 'http://foo:80/'
      state.gwURL = new URL(state.gwURLString)
      const request = url2request('https://bar.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('http://foo/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
    })
    it('should work for HTTPS GW without explicit port in URL', function () {
      state.gwURLString = 'https://foo:443/'
      state.gwURL = new URL(state.gwURLString)
      const request = url2request('https://bar.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
      expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://foo/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
    })
  })

  describe('Recovers Page if node is unreachable', function () {
    beforeEach(function () {
      global.browser = browser
      state.ipfsNodeType = 'external'
      state.redirect = true
      state.peerCount = -1 // this simulates Kubo RPC being offline
      state.gwURLString = 'http://localhost:8080'
      state.gwURL = new URL('http://localhost:8080')
      state.pubGwURLString = 'https://ipfs.io'
      state.pubGwURL = new URL('https://ipfs.io')
    })
    it('should present recovery page if node is offline and redirect is enabled', function () {
      expect(state.nodeActive).to.be.equal(false)
      state.redirect = true
      const request = url2request('https://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar')
      expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('chrome-extension://testid/dist/recovery/recovery.html#https%3A%2F%2Fipfs.io%2Fipfs%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%2Ffoo%2Fbar')
    })
    it('should present recovery page if node is offline and redirect is disabled', function () {
      expect(state.nodeActive).to.be.equal(false)
      state.redirect = false
      const request = url2request('https://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar')
      expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('chrome-extension://testid/dist/recovery/recovery.html#https%3A%2F%2Fipfs.io%2Fipfs%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%2Ffoo%2Fbar')
    })
    it('should not show recovery page if node is offline, redirect is enabled, but non-gateway URL failed to load from the same port', function () {
      // this covers https://github.com/ipfs/ipfs-companion/issues/1162 and https://twitter.com/unicomp21/status/1626244123102679041
      state.redirect = true
      expect(state.nodeActive).to.be.equal(false)
      const request = url2request('https://localhost:8080/')
      expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
    })
    it('should not show recovery page if extension is disabled', function () {
      // allows user to quickly avoid anything similar to https://github.com/ipfs/ipfs-companion/issues/1162
      state.active = false
      expect(state.nodeActive).to.be.equal(false)
      const request = url2request('https://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar')
      expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
    })
  })

  after(function () {
    delete global.URL
    delete global.browser
    browser.flush()
  })
})


text:
'use strict'
import { describe, it, before, beforeEach, after, afterEach } from 'mocha'
import sinon from 'sinon'
import { assert } from 'chai'
import { URL } from 'url'
import browser from 'sinon-chrome'
import { initState } from '../../../add-on/src/lib/state.js'
import createRuntimeChecks from '../../../add-on/src/lib/runtime-checks.js'
import { createRequestModifier } from '../../../add-on/src/lib/ipfs-request.js'
import createDnslinkResolver from '../../../add-on/src/lib/dnslink.js'
import { createIpfsPathValidator } from '../../../add-on/src/lib/ipfs-path.js'
import { optionDefaults } from '../../../add-on/src/lib/options.js'

browser.runtime.id = 'testid'

const url2request = (url, type = 'main_frame', tabId = new Date().valueOf()) => {
  return { url, type, tabId }
}
const urlRequestWithStatus = (url, statusCode = 200, type = 'main_frame') => {
  return { ...url2request(url, type), statusCode }
}

const urlRequestWithNetworkError = (url, error = 'net::ERR_CONNECTION_TIMED_OUT', type = 'main_frame') => {
  return { ...url2request(url, type), error }
}

describe('requestHandler.onCompleted:', function () { // HTTP-level errors
  let state, dnslinkResolver, ipfsPathValidator, requestHandler, runtime

  before(function () {
    global.URL = URL
    browser.tabs = { ...browser.tabs, getCurrent: sinon.stub().resolves({ id: 20 }) }
    global.browser = browser
    browser.runtime.id = 'testid'
  })

  beforeEach(async function () {
    state = initState(optionDefaults)
    const getState = () => state
    const getIpfs = () => {}
    dnslinkResolver = createDnslinkResolver(getState)
    runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
    ipfsPathValidator = createIpfsPathValidator(getState, getIpfs, dnslinkResolver)
    requestHandler = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
  })

  describe('with recoverFailedHttpRequests=true', function () {
    beforeEach(function () {
      state.recoverFailedHttpRequests = true
      state.dnslinkPolicy = false
    })
    it('should do nothing if broken request is for the default subdomain gateway', async function () {
      const request = urlRequestWithStatus('https://QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h.ipfs.dweb.link/wiki/', 500)
      await requestHandler.onCompleted(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
    it('should redirect to default subdomain gateway on broken subdomain gateway request', async function () {
      const request = urlRequestWithStatus('http://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq.ipfs.brokenexample.com/wiki/', 500)
      await requestHandler.onCompleted(request)
      assert.ok(browser.tabs.update.withArgs(request.tabId, { url: 'https://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq.ipfs.dweb.link/wiki/', active: true }).calledOnce, 'tabs.update should be called with default subdomain gateway URL')
    })
    it('should do nothing if broken request is a non-IPFS request', async function () {
      const request = urlRequestWithStatus('https://wikipedia.org', 500)
      await requestHandler.onCompleted(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
    it('should do nothing if broken request is a local request to 127.0.0.1/ipfs', async function () {
      const request = urlRequestWithStatus('http://127.0.0.1:8080/ipfs/QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 500)
      await requestHandler.onCompleted(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
    it('should do nothing if broken request is a local request to localhost/ipfs', async function () {
      const request = urlRequestWithStatus('http://localhost:8080/ipfs/QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 500)
      await requestHandler.onCompleted(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
    it('should do nothing if broken request is a local request to *.ipfs.localhost', async function () {
      const request = urlRequestWithStatus('http://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhw.ipfs.localhost:8080/', 500)
      await requestHandler.onCompleted(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
    it('should do nothing if broken request is to the default public gateway', async function () {
      const request = urlRequestWithStatus('https://ipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 500)
      await requestHandler.onCompleted(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
    it('should do nothing if broken request is not a \'main_frame\' request', async function () {
      const request = urlRequestWithStatus('https://nondefaultipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 500, 'stylesheet')
      await requestHandler.onCompleted(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
    it('should recover from unreachable third party public gateway by reopening on the public gateway', async function () {
      const request = urlRequestWithStatus('https://nondefaultipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 500)
      await requestHandler.onCompleted(request)
      assert.ok(browser.tabs.update.withArgs(request.tabId, { url: 'https://ipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', active: true }).calledOnce, 'tabs.update should be called with IPFS default public gateway URL')
    })
  })

  describe('with recoverFailedHttpRequests=false', function () {
    beforeEach(function () {
      state.recoverFailedHttpRequests = false
      state.dnslinkPolicy = false
    })
    it('should do nothing on failed subdomain gateway request', async function () {
      const request = urlRequestWithStatus('https://QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h.ipfs.brokendomain.com/wiki/', 500)
      await requestHandler.onCompleted(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
    it('should do nothing on broken non-default public gateway IPFS request', async function () {
      const request = urlRequestWithStatus('https://nondefaultipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 500)
      await requestHandler.onCompleted(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
  })

  afterEach(function () {
    browser.tabs.update.reset()
  })

  after(function () {
    delete global.url
    delete global.browser
    browser.flush()
  })
})

describe('requestHandler.onErrorOccurred:', function () { // network errors
  let state, dnslinkResolver, ipfsPathValidator, requestHandler, runtime

  before(function () {
    global.URL = URL
    browser.tabs = { ...browser.tabs, query: sinon.stub().resolves([{ id: 20 }]) }
    global.browser = browser
  })

  beforeEach(async function () {
    state = initState(optionDefaults)
    const getState = () => state
    const getIpfs = () => {}
    dnslinkResolver = createDnslinkResolver(getState)
    runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
    ipfsPathValidator = createIpfsPathValidator(getState, getIpfs, dnslinkResolver)
    requestHandler = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
  })

  describe('with recoverFailedHttpRequests=true', function () {
    beforeEach(function () {
      state.recoverFailedHttpRequests = true
      state.dnslinkPolicy = false
    })
    it('should do nothing if failed request is for the default subdomain gateway', async function () {
      const request = urlRequestWithStatus('https://QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h.ipfs.dweb.link/wiki/', 500)
      await requestHandler.onErrorOccurred(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
    it('should redirect to default subdomain gateway on failed subdomain gateway request', async function () {
      const request = urlRequestWithStatus('http://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq.ipfs.brokenexample.com/wiki/', 500)
      await requestHandler.onErrorOccurred(request)
      assert.ok(browser.tabs.update.withArgs(request.tabId, { url: 'https://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq.ipfs.dweb.link/wiki/', active: true }).calledOnce, 'tabs.update should be called with default subdomain gateway URL')
    })
    it('should do nothing if failed request is a non-IPFS request', async function () {
      const request = urlRequestWithNetworkError('https://wikipedia.org')
      await requestHandler.onErrorOccurred(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
    it('should do nothing if failed request is a non-public IPFS request', async function () {
      const request = urlRequestWithNetworkError('http://127.0.0.1:8080/ipfs/QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h')
      await requestHandler.onErrorOccurred(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
    it('should do nothing if failed request is to the default public gateway', async function () {
      const request = urlRequestWithNetworkError('https://ipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h')
      await requestHandler.onErrorOccurred(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
    it('should do nothing if failed request is not a \'main_frame\' request', async function () {
      const requestType = 'stylesheet'
      const request = urlRequestWithNetworkError('https://nondefaultipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 'net::ERR_NAME_NOT_RESOLVED', requestType)
      await requestHandler.onErrorOccurred(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
    it('should recover from unreachable third party public gateway by reopening on the public gateway', async function () {
      const request = urlRequestWithNetworkError('https://nondefaultipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h')
      await requestHandler.onErrorOccurred(request)
      assert.ok(browser.tabs.update.withArgs(request.tabId, { url: 'https://ipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', active: true }).calledOnce, 'tabs.update should be called with IPFS default public gateway URL')
    })
    it('should recover from unreachable HTTP server by reopening DNSLink on the active gateway', async function () {
      state.dnslinkPolicy = 'best-effort'
      dnslinkResolver.setDnslink('en.wikipedia-on-ipfs.org', '/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco')
      // avoid DNS failures when recovering to local gateweay (if available)
      const expectedUrl = 'http://localhost:8080/ipns/en.wikipedia-on-ipfs.org/'
      const request = urlRequestWithNetworkError('https://en.wikipedia-on-ipfs.org/')
      await requestHandler.onErrorOccurred(request)
      assert.ok(browser.tabs.update.withArgs(request.tabId, { url: expectedUrl, active: true }).calledOnce, 'tabs.update should be called with DNSLink on local gateway URL')
      dnslinkResolver.clearCache()
    })
    it('should recover from failed DNS for .eth opening it on EthDNS gateway at .eth.link', async function () {
      state.dnslinkPolicy = 'best-effort'
      dnslinkResolver.setDnslink('almonit.eth', false)
      dnslinkResolver.setDnslink('almonit.eth.link', '/ipfs/QmPH7VMnfFKvrr7kLXNRwuxjYRLWnfcxPvnWs8ipyWAQK2')
      const dnsFailure = 'net::ERR_NAME_NOT_RESOLVED' // chrome code
      const expectedUrl = 'https://almonit.eth.link/'
      const request = urlRequestWithNetworkError('https://almonit.eth', dnsFailure)
      await requestHandler.onErrorOccurred(request)
      assert.ok(browser.tabs.update.withArgs(request.tabId, { url: expectedUrl, active: true }).calledOnce, 'tabs.update should be called with ENS resource on local gateway URL')
      dnslinkResolver.clearCache()
    })
  })

  describe('with recoverFailedHttpRequests=false', function () {
    beforeEach(function () {
      state.recoverFailedHttpRequests = false
      state.dnslinkPolicy = false
    })
    it('should do nothing on unreachable third party public gateway', async function () {
      const request = urlRequestWithNetworkError('https://nondefaultipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h')
      await requestHandler.onErrorOccurred(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
    it('should do nothing on failed subdomain gateway request', async function () {
      const request = urlRequestWithStatus('https://QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h.ipfs.brokendomain.com/wiki/', 500)
      await requestHandler.onErrorOccurred(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
    })
    it('should do nothing on unreachable HTTP server with DNSLink', async function () {
      state.dnslinkPolicy = 'best-effort'
      dnslinkResolver.setDnslink('en.wikipedia-on-ipfs.org', '/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco')
      const request = urlRequestWithNetworkError('https://en.wikipedia-on-ipfs.org')
      await requestHandler.onErrorOccurred(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
      dnslinkResolver.clearCache()
    })
    it('should do nothing on failed non-default public gateway IPFS request', async function () {
      state.dnslinkPolicy = 'best-effort'
      dnslinkResolver.setDnslink('almonit.eth', false)
      dnslinkResolver.setDnslink('almonit.eth.link', '/ipfs/QmPH7VMnfFKvrr7kLXNRwuxjYRLWnfcxPvnWs8ipyWAQK2')
      const dnsFailure = 'net::ERR_NAME_NOT_RESOLVED' // chrome code
      const request = urlRequestWithNetworkError('https://almonit.eth', dnsFailure)
      await requestHandler.onErrorOccurred(request)
      assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
      dnslinkResolver.clearCache()
    })
  })

  afterEach(function () {
    browser.tabs.update.reset()
  })

  after(function () {
    delete global.url
    delete global.browser
    browser.flush()
  })
})

repo_name: ipfs/ipfs-companion

repo_language: JavaScript

mime_type: text/x-diff


text:
'use strict'
import { describe, it, before, beforeEach, after } from 'mocha'
import { expect } from 'chai'
import { URL } from 'url'
import browser from 'sinon-chrome'
import { initState } from '../../../add-on/src/lib/state.js'
import createRuntimeChecks from '../../../add-on/src/lib/runtime-checks.js'
import { createRequestModifier } from '../../../add-on/src/lib/ipfs-request.js'
import createDnslinkResolver from '../../../add-on/src/lib/dnslink.js'
import { createIpfsPathValidator } from '../../../add-on/src/lib/ipfs-path.js'
import { optionDefaults } from '../../../add-on/src/lib/options.js'

const url2request = (string) => {
  return { url: string, type: 'main_frame' }
}

const nodeTypes = ['external', 'embedded']

describe('modifyRequest.onBeforeRequest:', function () {
  let state, dnslinkResolver, ipfsPathValidator, modifyRequest, runtime

  before(function () {
    global.URL = URL
    global.browser = browser
    browser.runtime.id = 'testid'
  })

  beforeEach(async function () {
    state = Object.assign(initState(optionDefaults), {
      ipfsNodeType: 'external',
      peerCount: 1,
      redirect: true,
      dnslinkPolicy: false, // dnslink test suite is in ipfs-request-dnslink.test.js
      catchUnhandledProtocols: true,
      gwURLString: 'http://127.0.0.1:8080',
      pubGwURLString: 'https://ipfs.io'
    })
    const getState = () => state
    const getIpfs = () => {}
    dnslinkResolver = createDnslinkResolver(getState)
    runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
    ipfsPathValidator = createIpfsPathValidator(getState, getIpfs, dnslinkResolver)
    modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
  })

  // tests in which results should be the same for all node types
  nodeTypes.forEach(function (nodeType) {
    beforeEach(function () {
      state.ipfsNodeType = nodeType
    })
    describe(`with ${nodeType} node:`, function () {
      describe('request made via redirect-based protocol handler from manifest.json/protocol_handlers', function () {
        // Note: requests done with custom protocol handler are always  normalized to public gateway
        // (if external node is enabled, redirect will happen in next iteration of modifyRequest)
        beforeEach(function () {
          // ..however to make tests easier we disable redirect from public to custom gateway
          // (with this modifyRequest will return undefined for invalid URIs)
          state.redirect = false
        })

        // without web+ prefix (Firefox > 59: https://github.com/ipfs-shipyard/ipfs-companion/issues/164#issuecomment-356301174)
        it('should not be normalized if ipfs:/{CID}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=ipfs%3A%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should be normalized if ipfs://{CID}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=ipfs%3A%2F%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
        })
        it('should not be normalized if ipns:/{fqdn}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=ipns%3A%2Fipfs.io%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should be normalized if ipns://{fqdn}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=ipns%3A%2F%2Fipfs.io%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipns/ipfs.io?argTest#hashTest')
        })
        it('should be normalized if ipfs://{fqdn}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=ipfs%3A%2F%2Fipfs.io%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipns/ipfs.io?argTest#hashTest')
        })
        it('should be normalized if dweb:/ipfs/{CID}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=dweb%3A%2Fipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
        })
        it('should not be normalized if dweb://ipfs/{CID}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=dweb%3A%2F%2Fipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should be normalized if dweb:/ipns/{foo}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=dweb%3A%2Fipns/ipfs.io%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).equal('https://ipfs.io/ipns/ipfs.io?argTest#hashTest')
        })
        it('should not be normalized if dweb://ipns/{foo}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=dweb%3A%2F%2Fipns/ipfs.io%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })

        // web+ prefixed versions (Firefox < 59 and Chrome)
        it('should not be normalized if web+ipfs:/{CID}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=web%2Bipfs%3A%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should be normalized if web+ipfs://{CID}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=web%2Bipfs%3A%2F%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
        })
        it('should not be normalized if web+ipns:/{foo}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=web%2Bipns%3A%2Fipfs.io%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should be normalized if web+ipns://{foo}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=web%2Bipns%3A%2F%2Fipfs.io%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipns/ipfs.io?argTest#hashTest')
        })
        it('should be normalized if web+dweb:/ipfs/{CID}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=web%2Bdweb%3A%2Fipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
        })
        it('should not be normalized if web+dweb://ipfs/{CID}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=web%2Bdweb%3A%2F%2Fipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should be normalized if web+dweb:/ipns/{foo}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=web%2Bdweb%3A%2Fipns/ipfs.io%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).equal('https://ipfs.io/ipns/ipfs.io?argTest#hashTest')
        })
        it('should not be normalized if web+dweb://ipns/{foo}', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=web%2Bdweb%3A%2F%2Fipns/ipfs.io%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should not be normalized if web+{foo}:/bar', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=web%2Bfoo%3A%2Fbar%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should not be normalized if web+{foo}://bar', function () {
          const request = url2request('https://dweb.link/ipfs/?uri=web%2Bfoo%3A%2F%2Fbar%3FargTest%23hashTest')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
      })

      describe('catching unhandled custom protocol request', function () {
        it('should not be normalized if ipfs:/{CID}', function () {
          const request = url2request('https://duckduckgo.com/?q=ipfs%3A%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3FargTest%23hashTest&foo=bar')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should be normalized if ipfs://{CID}', function () {
          const request = url2request('https://duckduckgo.com/?q=ipfs%3A%2F%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3FargTest%23hashTest&foo=bar')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
        })
        it('should not be normalized if ipns:/{foo}', function () {
          const request = url2request('https://duckduckgo.com/?q=ipns%3A%2Fipns.io%2Findex.html%3Farg%3Dfoo%26bar%3Dbuzz%23hashTest')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should be normalized if ipns://{foo}', function () {
          const request = url2request('https://duckduckgo.com/?q=ipns%3A%2F%2Fipns.io%2Findex.html%3Farg%3Dfoo%26bar%3Dbuzz%23hashTest')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipns/ipns.io/index.html?arg=foo&bar=buzz#hashTest')
        })
        it('should be normalized if ipfs://{fqdn}', function () {
          const request = url2request('https://duckduckgo.com/?q=ipfs%3A%2F%2Fipns.io%2Findex.html%3Farg%3Dfoo%26bar%3Dbuzz%23hashTest')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipns/ipns.io/index.html?arg=foo&bar=buzz#hashTest')
        })
        it('should be normalized if dweb:/ipfs/{CID}', function () {
          const request = url2request('https://duckduckgo.com/?q=dweb%3A%2Fipfs%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3Farg%3Dfoo%26bar%3Dbuzz%23hash&ia=software')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?arg=foo&bar=buzz#hash')
        })
        it('should not be normalized if dweb://ipfs/{CID}', function () {
          const request = url2request('https://duckduckgo.com/?q=dweb%3A%2F%2Fipfs%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3Farg%3Dfoo%26bar%3Dbuzz%23hash&ia=software')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should be normalized if dweb:/ipns/{foo}', function () {
          const request = url2request('https://duckduckgo.com/?q=dweb%3A%2Fipns%2Fipfs.io%2Findex.html%3Farg%3Dfoo%26bar%3Dbuzz%23hash&ia=web')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipns/ipfs.io/index.html?arg=foo&bar=buzz#hash')
        })
        it('should not be normalized if dweb://ipns/{foo}', function () {
          const request = url2request('https://duckduckgo.com/?q=dweb%3A%2F%2Fipns%2Fipfs.io%2Findex.html%3Farg%3Dfoo%26bar%3Dbuzz%23hash&ia=web')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })

        it('should not be normalized if web+ipfs:/{CID}', function () {
          const request = url2request('https://duckduckgo.com/?q=web%2Bipfs%3A%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3FargTest%23hashTest&foo=bar')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should be normalized if web+ipfs://{CID}', function () {
          const request = url2request('https://duckduckgo.com/?q=web%2Bipfs%3A%2F%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3FargTest%23hashTest&foo=bar')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
        })
        it('should not be normalized if web+ipns:/{foo}', function () {
          const request = url2request('https://duckduckgo.com/?q=web%2Bipns%3A%2Fipns.io%2Findex.html%3Farg%3Dfoo%26bar%3Dbuzz%23hashTest')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should be normalized if web+ipns://{foo}', function () {
          const request = url2request('https://duckduckgo.com/?q=web%2Bipns%3A%2F%2Fipns.io%2Findex.html%3Farg%3Dfoo%26bar%3Dbuzz%23hashTest')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipns/ipns.io/index.html?arg=foo&bar=buzz#hashTest')
        })
        it('should be normalized if web+dweb:/ipfs/{CID}', function () {
          const request = url2request('https://duckduckgo.com/?q=web%2Bdweb%3A%2Fipfs%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3Farg%3Dfoo%26bar%3Dbuzz%23hash&ia=software')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?arg=foo&bar=buzz#hash')
        })
        it('should not be normalized if web+dweb://ipfs/{CID}', function () {
          const request = url2request('https://duckduckgo.com/?q=web%2Bdweb%3A%2F%2Fipfs%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3Farg%3Dfoo%26bar%3Dbuzz%23hash&ia=software')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should be normalized if web+dweb:/ipns/{foo}', function () {
          const request = url2request('https://duckduckgo.com/?q=web%2Bdweb%3A%2Fipns%2Fipfs.io%2Findex.html%3Farg%3Dfoo%26bar%3Dbuzz%23hash&ia=web')
          expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('https://ipfs.io/ipns/ipfs.io/index.html?arg=foo&bar=buzz#hash')
        })
        it('should not be normalized if web+dweb://ipns/{foo}', function () {
          const request = url2request('https://duckduckgo.com/?q=web%2Bdweb%3A%2F%2Fipns%2Fipfs.io%2Findex.html%3Farg%3Dfoo%26bar%3Dbuzz%23hash&ia=web')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })

        it('should not be normalized if disabled in Preferences', function () {
          state.catchUnhandledProtocols = false
          const request = url2request('https://duckduckgo.com/?q=ipfs%3A%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3FargTest%23hashTest&foo=bar')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should not be normalized if CID is invalid', function () {
          state.catchUnhandledProtocols = false
          const request = url2request('https://duckduckgo.com/?q=ipfs%3A%2FnotARealIpfsPathWithCid%3FargTest%23hashTest&foo=bar')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should not be normalized if presence of %3A%2F is a false-positive', function () {
          state.catchUnhandledProtocols = false
          const request = url2request('https://duckduckgo.com/?q=foo%3A%2Fbar%3FargTest%23hashTest&foo=bar')
          expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        })
        it('should not be normalized if request.type != main_frame', function () {
          const mediaRequest = { url: 'https://duckduckgo.com/?q=ipfs%3A%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%3FargTest%23hashTest&foo=bar', type: 'media' }
          expect(modifyRequest.onBeforeRequest(mediaRequest)).to.equal(undefined)
        })
      })
    })
  })

  after(function () {
    delete global.URL
    delete global.browser
    browser.flush()
  })
})


text:
'use strict'
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { useFakeTimers } from 'sinon'
import browser from 'sinon-chrome'
import { formatImportDirectory } from '../../../add-on/src/lib/ipfs-import.js'

describe('ipfs-import.js', function () {
  before(function () {
    browser.runtime.id = 'testid'
    global.document = {}
    global.browser = browser
    // ipfs-import depends on webextension/polyfill which can't be imported
    // in a non-browser environment unless global.browser is stubbed

    // need to force Date to return a particular date
    global.clock = useFakeTimers({
      now: new Date(2017, 10, 5, 12, 1, 1)
    })
  })

  describe('formatImportDirectory', function () {
    it('should change nothing if path is properly formatted and date wildcards are not provided', function () {
      const path = '/ipfs-companion-imports/my-directory/'
      expect(formatImportDirectory(path)).to.equal('/ipfs-companion-imports/my-directory/')
    })

    it('should replace two successive slashes with a single slash', function () {
      const path = '/ipfs-companion-imports//my-directory/'
      expect(formatImportDirectory(path)).to.equal('/ipfs-companion-imports/my-directory/')
    })

    it('should replace multiple slashes with a single slash', function () {
      const path = '/ipfs-companion-imports/////////my-directory/'
      expect(formatImportDirectory(path)).to.equal('/ipfs-companion-imports/my-directory/')
    })

    it('should add trailing slash if not present', function () {
      const path = '/ipfs-companion-imports/my-directory'
      expect(formatImportDirectory(path)).to.equal('/ipfs-companion-imports/my-directory/')
    })

    it('should replace date wildcards with padded dates', function () {
      const path = '/ipfs-companion-imports/%Y-%M-%D_%h%m%s/'
      expect(formatImportDirectory(path)).to.equal('/ipfs-companion-imports/2017-11-05_120101/')
    })
  })
  // TODO: complete tests
  // describe('openFilesAtWebUI', function () {
  // })
  //
  // describe('openFilesAtGateway', function () {
  // })
  //
  // describe('importFiles', function () {
  // })

  after(function () {
    global.browser.flush()
    global.clock.restore()
    delete global.document
    delete global.browser
  })
})


text:
'use strict'
import { describe, it, beforeEach, after } from 'mocha'
import { expect } from 'chai'
import sinon from 'sinon'
import browser from 'sinon-chrome'
import { storeMissingOptions, optionDefaults, isHostname, hostTextToArray, hostArrayToText } from '../../../add-on/src/lib/options.js'
import { URL } from 'url'

describe('storeMissingOptions()', function () {
  beforeEach(() => {
    global.URL = URL
    browser.runtime.id = 'testid'
    browser.flush()
  })

  it('should save all defaults during first run when no value is present in storage', done => {
    browser.storage.local.get.returns(Promise.resolve({})) // simulates empty user storage (clean install)
    browser.storage.local.set.returns(Promise.resolve({}))
    const read = Object.assign({}, optionDefaults)
    storeMissingOptions(read, optionDefaults, browser.storage.local)
      .then(changes => {
        sinon.assert.calledWith(browser.storage.local.set, changes)
        expect(changes).to.be.deep.equal(optionDefaults)
        done()
      })
      .catch(error => { done(error) })
  })
  it('should not touch non-default value present in storage', done => {
    const read = Object.assign({}, optionDefaults)
    const userModifiedKey = 'linkify'
    read[userModifiedKey] = !optionDefaults[userModifiedKey] // simulate custom option set by user
    browser.storage.local.get.returns(Promise.resolve(read)) // simulate existing user data read from storage
    browser.storage.local.set.returns(Promise.resolve({}))
    storeMissingOptions(read, optionDefaults, browser.storage.local)
      .then(changes => {
        sinon.assert.calledWith(browser.storage.local.set, changes)
        expect(changes).to.be.deep.equal({})
        done()
      })
      .catch(error => { done(error) })
  })

  it('should initialize default value for key missing from both read options and storage', done => {
    const read = Object.assign({}, optionDefaults)
    const userMissingKey = 'customGatewayUrl'
    delete read[userMissingKey] // simulate the key not being in storage (eg. after extension update)
    const expectedChanges = {}
    expectedChanges[userMissingKey] = optionDefaults[userMissingKey]
    browser.storage.local.get.returns(Promise.resolve(read)) // simulate existing user data read from storage
    browser.storage.local.set.returns(Promise.resolve({}))
    storeMissingOptions(read, optionDefaults, browser.storage.local)
      .then(changes => {
        sinon.assert.calledWith(browser.storage.local.set, changes)
        expect(changes).to.be.deep.equal(expectedChanges)
        done()
      })
      .catch(error => { done(error) })
  })

  after(() => {
    browser.flush()
  })
})

describe('isHostname()', function () {
  it('should return false for invalid URL.hostname', () => {
    expect(isHostname('random text with whitespaces')).to.equal(false)
  })
  it('should return true for a valid URL.hostname (FQDN)', () => {
    expect(isHostname('example.com')).to.equal(true)
  })
  it('should return true for a valid URL.hostname (ipv4)', () => {
    expect(isHostname('192.168.1.1')).to.equal(true)
  })
  it('should return true for valid URL.hostname (ipv6 in brackets)', () => {
    expect(isHostname('[fe80::bb67:770c:8a97:1]')).to.equal(true)
  })
  it('should return false for invalid URL.hostname (ipv6 without brackets)', () => {
    expect(isHostname('fe80::bb67:770c:8a97:1')).to.equal(false)
  })
  it('should return false for ipv6 with a missing bracket', () => {
    expect(
      isHostname('[fe80::bb67:770c:8a97:1') ||
      isHostname('fe80::bb67:770c:8a97:1]')
    ).to.equal(false)
  })
  it('should return false for ipv6 with malformed brackets', () => {
    expect(isHostname('[fe80::bb67:770c:8a97]:1]')).to.equal(false)
  })
})

describe('hostTextToArray()', function () {
  it('should sort, dedup hostnames, drop non-FQDNs and produce an array', () => {
    const text = 'zombo.com\n TwO.com \n 192.168.1.1:58080 \n192.168.1.2\n[fe80::bb67:770c:8a97:1]:58080\nfe80::bb67:770c:8a97:2\n[fe80::bb67:770c:8a97:3]\n totally not a FQDN \none.pl \nTWO.com\n\n'
    const array = ['192.168.1.1', '192.168.1.2', '[fe80::bb67:770c:8a97:1]', '[fe80::bb67:770c:8a97:2]', '[fe80::bb67:770c:8a97:3]', 'one.pl', 'two.com', 'zombo.com']
    expect(hostTextToArray(text)).to.be.an('array').to.have.ordered.members(array)
  })
})

describe('hostArrayToText()', function () {
  it('should sort, deduplicate, drop non-FQDNs and produce multiline string', () => {
    const array = ['zombo.com ', 'two.com  ', 'ONE.pl ', 'one.pl', 'totall not a FQDN', 'zombo.com']
    const text = 'one.pl\ntwo.com\nzombo.com'
    expect(hostArrayToText(array)).to.be.a('string').equal(text)
  })
})


text:
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import browser from 'sinon-chrome'
import AbortController from 'abort-controller'
import { URL } from 'url'
import { optionDefaults } from '../../../add-on/src/lib/options.js'
browser.runtime.id = 'testid'
global.browser = browser
global.AbortController = AbortController
global.chrome = browser
global.navigator = {
  clipboard: {
    writeText: () => {}
  }
}
// We need to do this because global is not mapped otherwise, we need to stub browser and chrome runtime
// so that the webextension-polyfill does not complain about the test runner not being a browser instance.
const init = async () => (await import('../../../add-on/src/lib/ipfs-companion.js')).default()

describe('lib/ipfs-companion.js', function () {
  describe('init', function () {
    before(function () {
      global.localStorage = global.localStorage || {}
      global.URL = global.URL || URL
      global.screen = { width: 1024, height: 720 }

      browser.runtime.getManifest.returns({ version: '0.0.0' }) // on-installed.js
    })

    it('should query local storage for options with hardcoded defaults for fallback', async function () {
      this.timeout(10000)
      browser.storage.local.get.resolves(optionDefaults)
      browser.storage.local.set.resolves()
      const ipfsCompanion = await init()
      expect(browser.storage.local.get.calledWith(optionDefaults)).to.equal(true)
      return await ipfsCompanion.destroy()
    })

    after(function () {
      browser.flush()
    })
  })

  describe.skip('onStorageChange()', function () {
    before(function () {
      global.window = {}
      global.browser = browser
      global.URL = URL
    })

    it('should update ipfs API instance on IPFS API URL change', async function () {
      browser.storage.local.get.resolves(optionDefaults)
      browser.storage.local.set.resolves()
      browser.browserAction.setBadgeBackgroundColor.resolves()
      browser.browserAction.setBadgeText.resolves()
      browser.browserAction.setIcon.resolves()
      browser.tabs.query.resolves([{ id: 'TEST' }])
      browser.contextMenus.update.resolves()
      browser.idle.queryState.resolves('active')

      const ipfsCompanion = await init()

      const oldIpfsApiUrl = 'http://127.0.0.1:5001'
      const newIpfsApiUrl = 'http://1.2.3.4:8080'
      const changes = { ipfsApiUrl: { oldValue: oldIpfsApiUrl, newValue: newIpfsApiUrl } }
      const area = 'local'
      const ipfs = global.window.ipfs
      browser.storage.onChanged.dispatch(changes, area)
      expect(ipfs).to.not.equal(window.ipfs)
      return ipfsCompanion.destroy()
    })

    after(function () {
      browser.flush()
    })
  })
})


text:
'use strict'
import { describe, it, before, beforeEach, after } from 'mocha'
import { expect, assert } from 'chai'
import { URL } from 'url' // URL implementation with support for .origin attribute
import browser from 'sinon-chrome'
import { initState } from '../../../add-on/src/lib/state.js'
import createRuntimeChecks from '../../../add-on/src/lib/runtime-checks.js'
import { createRequestModifier } from '../../../add-on/src/lib/ipfs-request.js'
import createDNSLinkResolver from '../../../add-on/src/lib/dnslink.js'
import { createIpfsPathValidator } from '../../../add-on/src/lib/ipfs-path.js'
import { optionDefaults } from '../../../add-on/src/lib/options.js'
import { braveNodeType } from '../../../add-on/src/lib/ipfs-client/brave.js'
import { spoofDnsTxtRecord } from './dnslink.test.js'

describe('modifyRequest processing', function () {
  let state, getState, dnslinkResolver, ipfsPathValidator, modifyRequest, runtime

  before(function () {
    global.URL = URL
    global.browser = browser
    browser.runtime.id = 'testid'
  })

  beforeEach(async function () {
    browser.runtime.getURL.flush()
    state = initState(optionDefaults)
    getState = () => state
    const getIpfs = () => {}
    dnslinkResolver = createDNSLinkResolver(getState)
    runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
    ipfsPathValidator = createIpfsPathValidator(getState, getIpfs, dnslinkResolver)
    modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
  })

  // Additional handling is required for redirected IPFS subresources on regular HTTPS pages
  // (eg. image embedded from public gateway on HTTPS website)
  describe('a subresource request on HTTPS website', function () {
    const cid = 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
    it('should be routed to "127.0.0.1" gw in Chromium if type is image', function () {
      runtime.isFirefox = false
      const request = {
        method: 'GET',
        type: 'image',
        url: `https://ipfs.io/ipfs/${cid}`,
        initiator: 'https://some-website.example.com' // Chromium
      }
      expect(modifyRequest.onBeforeRequest(request).redirectUrl)
        .to.equal(`http://127.0.0.1:8080/ipfs/${cid}`)
    })
    it('should be routed to "localhost" gw in Chromium if not a subresource', function () {
      runtime.isFirefox = false
      const request = {
        method: 'GET',
        type: 'main_frame',
        url: `https://ipfs.io/ipfs/${cid}`,
        initiator: 'https://some-website.example.com' // Chromium
      }
      expect(modifyRequest.onBeforeRequest(request).redirectUrl)
        .to.equal(`http://localhost:8080/ipfs/${cid}`)
    })
    it('should be routed to "127.0.0.1" gw to avoid mixed content warning in Firefox', function () {
      runtime.isFirefox = true
      const request = {
        method: 'GET',
        type: 'image',
        url: `https://ipfs.io/ipfs/${cid}`,
        originUrl: 'https://some-website.example.com/some/page.html' // FF only
      }
      expect(modifyRequest.onBeforeRequest(request).redirectUrl)
        .to.equal(`http://127.0.0.1:8080/ipfs/${cid}`)
    })
    it('should be routed to "localhost" gw in Firefox if not a subresource', function () {
      runtime.isFirefox = true
      const request = {
        method: 'GET',
        type: 'main_frame',
        url: `https://ipfs.io/ipfs/${cid}`,
        originUrl: 'https://some-website.example.com/some/page.html' // FF only
      }
      expect(modifyRequest.onBeforeRequest(request).redirectUrl)
        .to.equal(`http://localhost:8080/ipfs/${cid}`)
    })
  })

  describe('a request to <apiURL>/api/v0/add with stream-channels=true', function () {
    const expectHeader = { name: 'Expect', value: '100-continue' }
    it('should apply the "Expect: 100-continue" fix for https://github.com/ipfs/go-ipfs/issues/5168 ', function () {
      const request = {
        method: 'POST',
        requestHeaders: [
          { name: 'Content-Type', value: 'multipart/form-data; boundary=--------------------------015695779813832455524979' }
        ],
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/add?progress=true&wrapWithDirectory=true&pin=true&wrap-with-directory=true&stream-channels=true`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders).to.deep.include(expectHeader)
    })
  })

  // The Origin header set by browser for requests coming from within a browser
  // extension has been a mess for years, and by now we simply have zero trust
  // in stability of this header. Instead, we use WebExtension's webRequest API
  // to tell if a request comes from our browser extension and manually set
  // Origin to look like a request coming from the same Origin as IPFS API.
  //
  // The full context can be found in ipfs-request.js, where isCompanionRequest
  // check is executed.
  describe('Origin header in a request to <apiURL>/api/v0/', function () {
    it('set to API if request comes from Companion in Firefox <85', async function () {
      // Context: Firefox 65 started setting this header
      // set vendor-specific Origin for WebExtension context
      browser.runtime.getURL.withArgs('/').returns('moz-extension://0f334731-19e3-42f8-85e2-03dbf50026df/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const originalOriginHeader = { name: 'Origin', value: 'moz-extension://0f334731-19e3-42f8-85e2-03dbf50026df' }
      const apiOriginHeader = { name: 'Origin', value: getState().apiURL.origin }
      const request = {
        requestHeaders: [originalOriginHeader],
        originUrl: 'moz-extension://0f334731-19e3-42f8-85e2-03dbf50026df/path/to/background.html', // FF specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(apiOriginHeader)
    })

    it('set to API if request comes from Companion in Firefox 85', async function () {
      // Context: https://github.com/ipfs-shipyard/ipfs-companion/issues/955#issuecomment-753413988
      browser.runtime.getURL.withArgs('/').returns('moz-extension://0f334731-19e3-42f8-85e2-03dbf50026df/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const originalOriginHeader = { name: 'Origin', value: 'null' }
      const apiOriginHeader = { name: 'Origin', value: getState().apiURL.origin }
      const request = {
        requestHeaders: [originalOriginHeader],
        originUrl: 'moz-extension://0f334731-19e3-42f8-85e2-03dbf50026df/path/to/background.html', // FF specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(apiOriginHeader)
    })

    it('set to API if request comes from Companion in Chromium <72', async function () {
      // set vendor-specific Origin for WebExtension context
      browser.runtime.getURL.withArgs('/').returns('chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const bogusOriginHeader = { name: 'Origin', value: 'null' }
      const apiOriginHeader = { name: 'Origin', value: getState().apiURL.origin }
      const request = {
        requestHeaders: [bogusOriginHeader],
        initiator: 'chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch/', // Chromium specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(apiOriginHeader)
    })

    it('set to API if request comes from Companion in Chromium 72', async function () {
      // Context: Chromium 72 started setting this header to chrome-extension:// URI
      // set vendor-specific Origin for WebExtension context
      browser.runtime.getURL.withArgs('/').returns('chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const bogusOriginHeader = { name: 'Origin', value: 'chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch' }
      const apiOriginHeader = { name: 'Origin', value: getState().apiURL.origin }
      const request = {
        requestHeaders: [bogusOriginHeader],
        initiator: 'chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch/', // Chromium specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(apiOriginHeader)
    })

    it('keep Origin as-is if request does not come from Companion (Chromium)', async function () {
      browser.runtime.getURL.withArgs('/').returns('chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const originHeader = { name: 'Origin', value: 'https://some.website.example.com' }
      const expectedOriginHeader = { name: 'Origin', value: 'https://some.website.example.com' }
      const request = {
        requestHeaders: [originHeader],
        initiator: 'https://some.website.example.com', // Chromium specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(expectedOriginHeader)
    })

    it('keep Origin as-is if request does not come from Companion (Firefox)', async function () {
      browser.runtime.getURL.withArgs('/').returns('moz-extension://0f334731-19e3-42f8-85e2-03dbf50026df/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const originHeader = { name: 'Origin', value: 'https://some.website.example.com' }
      const expectedOriginHeader = { name: 'Origin', value: 'https://some.website.example.com' }
      const request = {
        requestHeaders: [originHeader],
        originUrl: 'https://some.website.example.com/some/path.html', // Firefox specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(expectedOriginHeader)
    })

    it('keep the "Origin: null" if request does not come from Companion (Chromium)', async function () {
      // Presence of Origin header is important as it protects API from XSS via sandboxed iframe
      // NOTE: Chromium <72 was setting this header in requests sent by browser extension,
      // but they fixed it since then, and we switched to reading origin via webRequest API,
      // which is independent from the HTTP header.
      browser.runtime.getURL.withArgs('/').returns('chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const nullOriginHeader = { name: 'Origin', value: 'null' }
      const expectedOriginHeader = { name: 'Origin', value: 'null' }
      const request = {
        requestHeaders: [nullOriginHeader],
        initiator: 'https://random.website.example.com', // Chromium specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(expectedOriginHeader)
    })

    it('keep the "Origin: null" if request does not come from Companion (Firefox)', async function () {
      // Presence of Origin header is important as it protects API from XSS via sandboxed iframe
      browser.runtime.getURL.withArgs('/').returns('moz-extension://0f334731-19e3-42f8-85e2-03dbf50026df/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const nullOriginHeader = { name: 'Origin', value: 'null' }
      const expectedOriginHeader = { name: 'Origin', value: 'null' }
      const request = {
        requestHeaders: [nullOriginHeader],
        originUrl: 'https://random.website.example.com/some/path.html', // Firefox specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(expectedOriginHeader)
    })
  })

  // We've moved blog to blog.ipfs.io but links to ipfs.io/blog/*
  // are still around due to the way Discourse integration was done for comments.
  // https://github.com/ipfs/blog/issues/360
  describe('a failed main_frame request to /ipns/ipfs.io/blog', function () {
    it('should be updated to /ipns/blog.ipfs.io', async function () {
      const brokenDNSLinkUrl = 'http://example.com/ipns/ipfs.io/blog/some-post'
      const fixedDNSLinkUrl = 'http://example.com/ipns/blog.ipfs.io/some-post'
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const request = {
        tabId: 42404,
        statusCode: 404,
        type: 'main_frame',
        url: brokenDNSLinkUrl
      }
      browser.tabs.update.flush()
      assert.ok(browser.tabs.update.withArgs(request.tabId, { url: fixedDNSLinkUrl }).notCalled)
      modifyRequest.onCompleted(request)
      assert.ok(browser.tabs.update.withArgs(request.tabId, { url: fixedDNSLinkUrl }).calledOnce)
      browser.tabs.update.flush()
    })
  })

  // Brave seems to ignore redirect to ipfs:// and ipns://, but if we force tab update via tabs API,
  // then address bar is correct
  describe('redirect of main_frame request to local gateway when Brave node is used', function () {
    it('should force native URI in address bar via tabs.update API', async function () {
      const httpDNSLinkUrl = 'https://example.com/ipns/docs.ipfs.io/some/path?query=val'
      const nativeDNSLinkUri = 'ipns://docs.ipfs.io/some/path?query=val'
      spoofDnsTxtRecord('docs.ipfs.io', dnslinkResolver, '/ipfs/bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss')
      state.ipfsNodeType = braveNodeType
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const request = {
        tabId: 42,
        type: 'main_frame',
        url: httpDNSLinkUrl
      }
      browser.tabs.update.flush()
      assert.ok(browser.tabs.update.withArgs(request.tabId, { url: nativeDNSLinkUri }).notCalled)
      await modifyRequest.onBeforeRequest(request)
      assert.ok(browser.tabs.update.withArgs(request.tabId, { url: nativeDNSLinkUri }).calledOnce)
      browser.tabs.update.flush()
    })
  })

  // https://github.com/ipfs-shipyard/ipfs-companion/issues/962
  describe('redirect of IPFS resource to local gateway in Brave', function () {
    it('should be redirected if not a  subresource (not impacted by Brave Shields)', function () {
      runtime.isFirefox = false
      runtime.brave = { thisIsFakeBraveRuntime: true }
      const request = {
        method: 'GET',
        type: 'image',
        url: 'https://ipfs.io/ipfs/bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss',
        initiator: 'https://some-website.example.com' // Brave (built on Chromium)
      }
      expect(modifyRequest.onBeforeRequest(request))
        .to.equal(undefined)
    })
    it('should be left untouched if subresource (would be blocked by Brave Shields)', function () {
      runtime.isFirefox = false
      runtime.brave = { thisIsFakeBraveRuntime: true }
      const cid = 'bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss'
      const request = {
        method: 'GET',
        type: 'main_frame',
        url: `https://ipfs.io/ipfs/${cid}`,
        initiator: 'https://some-website.example.com' // Brave (built on Chromium)
      }
      expect(modifyRequest.onBeforeRequest(request).redirectUrl)
        .to.equal(`http://localhost:8080/ipfs/${cid}`)
    })
  })

  after(function () {
    delete global.URL
    delete global.browser
    browser.flush()
  })
})