text:
# Collected telemetry data

Copied from https://github.com/ipfs-shipyard/ignite-metrics/blob/main/docs/telemetry/COLLECTED_DATA.template.md

## What metrics data do we NOT collect

We do not collect any of the below listed items in our metric data:

1. User identifiable data (names, email addresses, aliases, handles, etc.)
2. CIDs
3. IP addresses

Telemetry is sent to Countly instance at `countly.ipfs.tech`. You can read how they protect your data at [Security, Privacy, and Access FAQ](https://support.count.ly/hc/en-us/articles/360037501372-Security-Privacy-and-Access-FAQ)

## What metrics data DO we collect

As a general rule, we collect only application data; no user data. Some examples of application data we collect are:

| Metric data name    | Metric feature name | Metric trigger                                          | Analytics use                                                     | Notes         |
|:-------------------:|---------------------|---------------------------------------------------------|-------------------------------------------------------------------|---------------|
| begin_session       | sessions            | At the start of the extension being active and enabled  | Count unique active users (daily/weekly/monthly) & session length | see [Countly's session flow documentation](https://support.count.ly/hc/en-us/articles/360037753291-SDK-development-guide#session-flow) |
| end_session         | sessions            | Whenever extension is disabled or idle                  | Count unique active users (daily/weekly/monthly) & session length | see [Countly's session flow documentation](https://support.count.ly/hc/en-us/articles/360037753291-SDK-development-guide#session-flow) |
| session_duration    | sessions            | At the end of the extension being active and enabled    | Count unique active users (daily/weekly/monthly) & session length | see [Countly's session flow documentation](https://support.count.ly/hc/en-us/articles/360037753291-SDK-development-guide#session-flow) |
| view:background     | views               | When the background view is shown                       | View count                                                        |               |
| view:welcome        | views               | When the welcome view is shown                          | View count                                                        |               |
| view:options        | views               | When the options view is shown                          | View count                                                        |               |
| view:quick-import   | views               | When the quick-import view is shown                     | View count                                                        |               |
| view:browser-action | views               | When the browser-action view is shown                   | View count                                                        |               |

* "Metric data name" - The app-specific metric/event name we're using for this metric data. (e.g. APP_BOOTSTRAP_START)
* "Metric feature name" - The metric feature the event/metric data correlates to. The group the metric feature belongs to is defined in our [COLLECTION_POLICY](https://github.com/ipfs-shipyard/ignite-metrics/blob/main/docs/telemetry/COLLECTION_POLICY.md#metric-features-and-their-groupings). (e.g. Minimal)
* "Metric trigger" - An explanation covering when this metric data is triggered. (e.g. On Application init)
* "Analytics use" - An explanation about how this metric data is used for analytics. (e.g. Input to load time calculations)
* "Notes" - Any additional notes. (e.g. Used as a timestamp identifier for when an application is first loaded)

## Other related documents

* [COLLECTION_POLICY](https://github.com/ipfs-shipyard/ignite-metrics/blob/main/docs/telemetry/COLLECTION_POLICY.md)
* [PRIVACY_POLICY](https://github.com/ipfs-shipyard/ignite-metrics/blob/main/docs/telemetry/PRIVACY_POLICY.md)
* [FAQs](https://github.com/ipfs-shipyard/ignite-metrics/blob/main/docs/telemetry/FAQs.md)


text:
# Configuration for welcome - https://github.com/behaviorbot/welcome

# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome
# Comment to be posted to on first time issues
newIssueWelcomeComment: >
  Thank you for submitting your first issue to this repository! A maintainer
  will be here shortly to triage and review.

  In the meantime, please double-check that you have provided all the
  necessary information to make this process easy! Any information that can
  help save additional round trips is useful! We currently aim to give
  initial feedback within **seven business days**. If this does not happen, feel
  free to leave a comment.

  Please keep an eye on how this issue will be labeled, as labels give an
  overview of priorities, assignments and additional actions requested by the
  maintainers:

    - "Priority" labels will show how urgent this is for the team.
    - "Status" labels will show if this is ready to be worked on, blocked, or in progress.
    - "Need" labels will indicate if additional input or analysis is required.

  Finally, remember to use https://discuss.ipfs.io if you just need general
  support.

# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome
# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: >
  Thank you for submitting this PR!

  A maintainer will be here shortly to review it.

  We are super grateful, but we are also overloaded! Help us by making sure
  that:

    * The context for this PR is clear, with relevant discussion, decisions
      and stakeholders linked/mentioned.

    * Your contribution itself is clear (code comments, self-review for the
      rest) and in its best form. Follow the [code contribution
      guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md#code-contribution-guidelines)
      if they apply.

  Getting other community members to do a review would be great help too on
  complex PRs (you can ask in the chats/forums). If you are unsure about
  something, just leave us a comment.

  Next steps:

    * A maintainer will triage and assign priority to this PR, commenting on
      any missing things and potentially assigning a reviewer for high
      priority items.

    * The PR gets reviews, discussed and approvals as needed.

    * The PR is merged by maintainers when it has been approved and comments addressed.

  We currently aim to provide initial feedback/triaging within **seven business
  days**. Please keep an eye on any labelling actions, as these will indicate
  priorities and status of your contribution.

  We are very grateful for your contribution!

# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge
# Comment to be posted to on pull requests merged by a first time user
# Currently disabled
#firstPRMergeComment: ""

repo_name: ipfs/ipfs-companion

repo_language: JavaScript

mime_type: text/x-diff


text: * @lidel @whizzzkid @ipfs/gui-dev


text:
---
name: Bug report
about: Create a bug report
title: ''
labels: need/triage
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser: [e.g. chrome, safari]
 - Version: [e.g. 22]

**Smartphone (please complete the following information):**
 - Device: [e.g. iPhone6]
 - OS: [e.g. iOS8.1]
 - Browser: [e.g. stock browser, safari]
 - Version: [e.g. 22]

**Additional context**
Add any other context about the problem here.


text:
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: need/triage
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


text:
blank_issues_enabled: false
contact_links:
 - name: Getting Help on IPFS
   url: https://ipfs.io/help
   about: All information about how and where to get help on IPFS.
 - name: IPFS Official Forum
   url: https://discuss.ipfs.io
   about: Please post general questions, support requests, and discussions here.


text:
name: Close and mark stale issue

on:
  schedule:
    - cron: '0 0 * * *'

permissions:
  issues: write
  pull-requests: write

jobs:
  stale:
    uses: pl-strflt/.github/.github/workflows/reusable-stale-issue.yml@v0.3


text:
name: ci
on:
  workflow_dispatch:
  push:
    branches:
      - main
    tags:
      - '*'
  pull_request:
    branches:
      - main

env:
  XDG_CACHE_HOME: ${{ github.workspace }}/.cache

jobs:

  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [macos-latest, ubuntu-latest, windows-latest]

    steps:
      - name: Check out Git repository
        uses: actions/checkout@v3.3.0

      - name: Install Node.js
        uses: actions/setup-node@v3.6.0
        with:
          node-version: 18

      - name: patch node gyp on windows to support Visual Studio 2019
        shell: powershell
        if: runner.os == 'Windows'
        run: |
          npm install --global node-gyp@latest
          npm prefix -g | % {npm config set node_gyp "$_\node_modules\node-gyp\bin\node-gyp.js"}

      - name: Restore .cache
        uses: actions/cache@v3.2.4
        id: cache
        with:
          path: ${{ github.workspace }}/.cache
          key: ${{ runner.os }}-${{ hashFiles('package*json', 'package-lock.json', '*config.js') }}
          restore-keys: |
            ${{ runner.os }}-${{ hashFiles('package*json', 'package-lock.json', '*config.js') }}
            ${{ runner.os }}-

      - name: Restore node_modules
        id: yarn-cache
        uses: actions/cache@v3.2.4
        with:
          path: node_modules
          key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

      - name: Install dependencies
        if: steps.yarn-cache.outputs.cache-hit != 'true'
        run: npm run ci:install

      - name: Build
        run: npm run ci:build

      - name: Show build/
        run: du -h build/*

      - name: Show dist/bundles/*
        run: du -h add-on/dist/bundles/*

      - name: Test
        run: npm run ci:test
        # tests should finish <30s, if it takes longer its a bug - fail fast
        timeout-minutes: 5

      - name: Lint
        # bundles are too big on Windows and fail addon-lint
        # but as long we don't build releases on Windows, can be ignored
        if: runner.os != 'Windows'
        run: npm run ci:lint

      # Persist produced binaries and effective config used for building them
      # - this is not for releases, but for quick testing during the dev
      # - action artifacts can be downloaded for 90 days, then are removed by github
      # - binaries in PRs from forks won't be signed
      - name: Attach produced packages to Github Action
        uses: actions/upload-artifact@v3.1.2
        with:
          name: built-on-${{ matrix.os }}
          path: build/ipfs_companion*.*
          if-no-files-found: error

      - name: Show Cache
        run: du -h ${{ github.workspace }}/.cache/* || true

  release-pr:
    runs-on: ubuntu-latest
    needs: [test]
    if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
    steps:
      - uses: google-github-actions/release-please-action@v3.7.3
        with:
          command: manifest-pr
          changelog-notes-type: github

  release-assets:
    runs-on: ubuntu-latest
    needs: [test]
    if: contains(github.ref, 'refs/tags/') && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
    steps:
      - name: Check out Git repository
        uses: actions/checkout@v3.3.0

      - name: Install Node.js
        uses: actions/setup-node@v3.6.0
        with:
          node-version: 18

      - name: Restore .cache
        uses: actions/cache@v3.2.4
        id: cache
        with:
          path: ${{ github.workspace }}/.cache
          key: ${{ runner.os }}-${{ hashFiles('package*json', 'package-lock.json', '*config.js') }}
          restore-keys: |
            ${{ runner.os }}-${{ hashFiles('package*json', 'package-lock.json', '*config.js') }}
            ${{ runner.os }}-

      - name: Restore node_modules
        id: yarn-cache
        uses: actions/cache@v3.2.4
        with:
          path: node_modules
          key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

      - name: Install dependencies
        if: steps.yarn-cache.outputs.cache-hit != 'true'
        run: npm run ci:install

      - name: Build Stable Release
        run: npm run ci:build:stable

      - name: Release
        uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844
        with:
          files: build/*.zip
          fail_on_unmatched_files: true
          generate_release_notes: true
          draft: true
          append_body: true
          body: 'Automated Release, please upload artifacts to respective webstores and mark this draft as released.'


text:
name: e2e
on:
  workflow_dispatch:
    inputs:
      firefox-version:
        description: The version of selenium/standalone-firefox image to use
        default: latest
        required: true
      chromium-version:
        description: The version of selenium/standalone-chrome image to use
        default: latest
        required: true
      kubo-version:
        description: The version of ipfs/kubo image to use
        default: latest
        required: true
      ipfs-companion-version:
        description: The version of ipfs-companion extension to use (defaults to building the extension from source)
        default: ''
        required: false

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repo
        uses: actions/checkout@v3
      - name: Set up node
        uses: actions/setup-node@v3
        with:
          node-version: 18
      - name: Download ipfs-companion
        if: inputs.ipfs-companion-version != ''
        env:
          IPFS_COMPANION_VERSION: ${{ inputs.ipfs-companion-version }}
        run: ./ci/download-release-artifacts.sh
      - name: Build ipfs-companion
        if: inputs.ipfs-companion-version == ''
        run: npm run release-build
      - name: Prepare E2E env
        run: npm run compose:e2e:prepare
        env:
          FIREFOX_VERSION: ${{ inputs.firefox-version }}
          CHROMIUM_VERSION: ${{ inputs.chromium-version }}
          KUBO_VERSION: ${{ inputs.kubo-version }}
      - name: Start E2E env
        run: npm run compose:e2e:up
        env:
          FIREFOX_VERSION: ${{ inputs.firefox-version }}
          CHROMIUM_VERSION: ${{ inputs.chromium-version }}
          KUBO_VERSION: ${{ inputs.kubo-version }}
      - name: Wait for E2E env set up to complete
        run: sleep 60
      - name: Run E2E tests
        run: npm run compose:e2e:test
        env:
          IPFS_COMPANION_VERSION: ${{ inputs.ipfs-companion-version }}
      - name: Stop E2E env
        run: npm run compose:e2e:down


text:
/* This is a fallback script used when ipfs cli fails or is not available
 * More details: https://github.com/ipfs-shipyard/ipfs-webui/issues/843
 * See also why this is not used anymore: https://github.com/ipfs-shipyard/ipfs-companion/issues/679
 */
import tar from 'tar'
import request from 'request'
import progress from 'request-progress'

const cid = process.argv[2]
const destination = process.argv[3]

// use public gw
const url = 'https://ipfs.io/api/v0/get?arg=' + cid + '&archive=true&compress=true'

console.log('Fallback to HTTP GET from: ' + url)

progress(request(url), { lengthHeader: 'x-content-length', delay: 10000 })
  .on('progress', (state) => console.log(`progress: ${Math.round(state.percent)} %, transferred: ${state.size.transferred}`, state))
  .on('response', (response) => console.log('Status Code', response.statusCode))
  .on('error', (error) => console.log('Download Error', error))
  .on('close', () => console.log('Done! webui extracted to: ' + destination))
  .pipe(
    tar.extract({
      strip: 1,
      C: destination
    })
  )


text:
#!/bin/bash
BUILD_TYPE=${1:-$FIREFOX_RELEASE}
echo "Looking up latest URL for $BUILD_TYPE"
BUILD_ROOT="/pub/firefox/tinderbox-builds/mozilla-${BUILD_TYPE}/"
ROOT="https://archive.mozilla.org"
LATEST=$(curl -s "$ROOT$BUILD_ROOT" | grep $BUILD_TYPE | grep -Po '\K[[:digit:]]+' | sort -n | tail -1)
FILE=$(curl -s "$ROOT$BUILD_ROOT$LATEST/" | grep '.tar.' | grep -Po '<a href="\K[^"]*')
echo "URL: $ROOT$FILE"
exec wget -O "firefox-${BUILD_TYPE}.tar.bz2" "$ROOT$FILE"


text:
#!/bin/bash

read -p "Key: " key
read -p "Message: " message

dir=$(dirname $0)

cat $dir/../add-on/_locales/en/messages.json | \
    jq --arg foo bar ". + {\"${key}\": {\"message\": \"${message}\"}}" | \
    sponge $dir/../add-on/_locales/en/messages.json

tail -10 $dir/../add-on/_locales/en/messages.json


text:
#!/usr/bin/env node

import fs from 'fs'
import glob from 'glob'

const files = glob.sync('build/*/*.zip')

files.map(async file => {
  const path = file.split('/')
  const name = path.pop().split('.zip').shift()
  const target = path.pop()
  const newFile = `build/${name}_${target}.zip`
  // remove old artifact, if exists
  if (fs.existsSync(newFile)) fs.unlinkSync(newFile)
  // rename artifact
  console.log(`${file} → ${newFile}`)
  fs.renameSync(file, newFile)
  // remove empty dir
  fs.rmdirSync(`build/${target}`, { recursive: true })
})


text:
#!/bin/bash
dir=$(dirname $0)
for svg in $dir/../add-on/icons/*.svg; do
    for size in 19 38 128; do
        png=$dir/../add-on/icons/png/$(basename -s .svg ${svg})_${size}.png
        inkscape -z -e $png -w $size -h $size $svg
        optipng -o7 -i0 $png
    done
done


text:
'use strict'

// This is a basic stub to build node-like js-ipfs
// for use in Brave, but without prometheus libs which are not compatible
// with browser context
function Gauge () {}
module.exports = {
  Gauge,
  register: {
    clear () { }
  }
}


text:
#!/bin/bash
# This script is used by CI setup to update add-on name in manifest
# to explicitly state that produced artifact is a dev build.
set -e

MANIFEST=add-on/manifest.common.json

# restore original in case it was modified manually
test -d .git && git checkout -- $MANIFEST

# skip all manifest mutations when building for stable channel
if [ "$RELEASE_CHANNEL" = "stable" ]; then
    echo "Skipping manifest modification (RELEASE_CHANNEL=${RELEASE_CHANNEL})"
    exit 0
fi

# Use jq for JSON operations
function set-manifest {
  jq -M --indent 2 "$1" < $MANIFEST > tmp.json && mv tmp.json $MANIFEST
}

## Set NAME
# Name includes git revision to make QA and bug reporting easier for users :-)
REVISION=$(git show-ref --head HEAD | head -c 7)
if [ "$RELEASE_CHANNEL" = "beta" ]; then
    set-manifest ".name = \"IPFS Companion (Beta @ $REVISION)\""
else
    set-manifest ".name = \"IPFS Companion (Dev Build @ $REVISION)\""
fi
grep $REVISION $MANIFEST

## Set VERSION
# Browsers do not accept non-numeric values in version string
# so we calculate some sub-versions based on number of commits in master and  current branch
# mozilla/addons-linter: Version string must be a string comprising one to four dot-separated integers (0-65535). E.g: 1.2.3.4"
BUILD_VERSION=$(git rev-list --count --first-parent HEAD)
set-manifest ".version = (.version + \".${BUILD_VERSION}\")"
grep \"version\" $MANIFEST


text:
#!/bin/sh
set -ex

IPFS_COMPANION_VERSION=${IPFS_COMPANION_VERSION:-$(jq -r '.version' ./add-on/manifest.common.json)}

id="$(curl --retry 5 --no-progress-meter "https://api.github.com/repos/ipfs/ipfs-companion/releases/tags/v$IPFS_COMPANION_VERSION" | jq '.id')"
assets="$(curl --retry 5 --no-progress-meter --location "https://api.github.com/repos/ipfs/ipfs-companion/releases/$id/assets" | jq -r '.[].name')"

if [ ! -d build ]; then
  mkdir build
fi

for asset in $assets; do
  curl --retry 5 --no-progress-meter --location --output "build/$asset" "https://github.com/ipfs/ipfs-companion/releases/download/v$IPFS_COMPANION_VERSION/$asset"
done


text:
#!/bin/sh
set -ex

ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin "${ACCESS_CONTROL_ALLOW_ORIGIN:-[\"*\"]}"
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods "${ACCESS_CONTROL_ALLOW_METHODS:-[\"*\"]}"


text:
[main]
host = https://www.transifex.com

[o:ipfs:p:ipfs-companion:r:messages-json]
file_filter  = add-on/_locales/<lang>/messages.json
source_file  = add-on/_locales/en/messages.json
source_lang  = en
type         = CHROME
minimum_perc = 40


text:
import { Builder, By, Key } from 'selenium-webdriver'
import { describe, it, before } from 'mocha'
import { expect } from 'chai'
import fs from 'fs'
import chrome from 'selenium-webdriver/chrome.js'
import firefox from 'selenium-webdriver/firefox.js'
import { fail, equal, notEqual } from 'assert'
import { backOff } from 'exponential-backoff'

function getExtension (browserName) {
  const version = process.env.IPFS_COMPANION_VERSION || JSON.parse(fs.readFileSync('add-on/manifest.common.json')).version
  return `build/ipfs_companion-${version}_${browserName}.zip`
}

async function openChromium (extension) {
  console.info('Opening Chromium browser')
  const options = new chrome.Options()
  if (extension !== undefined) {
    options.addExtensions(extension)
  }
  options.addArguments('--lang=en-GB,en-US')
  if (process.env.TEST_HEADLESS === 'true') {
    options.addArguments('--headless=chrome')
  }
  const builder = new Builder()
    .forBrowser('chrome')
    .setChromeOptions(options)
  if (process.env.SELENIUM_REMOTE_CHROMIUM_URL !== undefined) {
    console.info(`Using remote webdriver: ${process.env.SELENIUM_REMOTE_CHROMIUM_URL}`)
    builder.usingServer(process.env.SELENIUM_REMOTE_CHROMIUM_URL)
  }
  console.info('Starting Chromium')
  const browser = await builder.build()
  console.info('Chromium is ready')
  return browser
}

async function openFirefox (extension) {
  console.info('Opening Firefox browser')
  const options = new firefox.Options()
  options.setPreference('intl.accept_languages', 'en-gb,en-us')
  options.setPreference('intl.locale.requested', 'en-GB,en-US')
  if (process.env.TEST_HEADLESS === 'true') {
    options.addArguments('--headless')
  }
  const builder = new Builder()
    .forBrowser('firefox')
    .setFirefoxOptions(options)
  if (process.env.SELENIUM_REMOTE_FIREFOX_URL !== undefined) {
    console.info(`Using remote webdriver: ${process.env.SELENIUM_REMOTE_FIREFOX_URL}`)
    builder.usingServer(process.env.SELENIUM_REMOTE_FIREFOX_URL)
  }
  console.info('Starting Firefox')
  const browser = await builder.build()
  if (extension !== undefined) {
    console.info('Installing the extension')
    await browser.installAddon(extension, true)
  }
  console.info('Firefox is ready')
  return browser
}

const ExtensionURLRegex = /^(moz|chrome)-extension:\/\/[^\/]+/ // eslint-disable-line no-useless-escape
async function findExtensionUrl (browser) {
  console.info('Looking for an open extension tab')
  const handles = await browser.getAllWindowHandles()
  console.info(`Found ${handles.length} candidates`)

  for (const handle of handles) {
    console.info('Switching tabs')
    await browser.switchTo().window(handle)
    const url = await browser.getCurrentUrl()
    console.info(`The current URL is: ${url}`)
    const extensionURL = ExtensionURLRegex.exec(url)?.at(0)
    if (extensionURL !== undefined) {
      console.info(`Found the extension URL: ${extensionURL}`)
      return extensionURL
    }
  }
  console.warn('No extension URL found')
}

async function updateExtensionSettings (browser, url, id, value) {
  console.info('Updating extension setting')
  console.info(`Going to: ${url}/dist/options/options.html`)
  await browser.get(`${url}/dist/options/options.html`)
  console.info(`Looking for an element: ${id}`)
  const element = browser.findElement(By.id(id))
  console.info(`Setting new value to: ${value}`)
  await element.sendKeys('')
  await backOff(async () => {
    const activeElement = await browser.switchTo().activeElement()
    const activeElementID = await activeElement.getAttribute('id')
    equal(activeElementID, id, 'The element is not focused yet')
  }, {
    numOfAttempts: 5,
    startingDelay: 500
  })
  await element.clear()
  await backOff(async () => {
    const v = await element.getAttribute('value')
    equal(v, '', 'The element is not cleared yet')
  }, {
    numOfAttempts: 5,
    startingDelay: 500
  })
  await element.sendKeys(value)
  await element.sendKeys(Key.TAB)
  console.info('Checking if the update worked')
  backOff(async () => {
    const e = browser.findElement(By.id(id))
    const v = await e.getAttribute('value')
    equal(v, value, 'The element is not updated yet')
  }, {
    delayFirstAttempt: true,
    numOfAttempts: 5,
    startingDelay: 500
  })
  console.info('The setting update is complete')
}

async function getNumberOfConnectedPeers (browser, url) {
  console.info('Checking the number of connected peers')
  console.info(`Going to: ${url}/dist/landing-pages/welcome/index.html`)
  await browser.get(`${url}/dist/landing-pages/welcome/index.html`)
  console.info('Looking for an element with text: \'Your node is connected to ...\'')
  const peers = await backOff(async () => {
    const p = browser.findElement(By.xpath("//p[text()='Your node is connected to ']"))
    const span = p.findElement(By.css('span'))
    return span.getText()
  }, {
    delayFirstAttempt: true,
    numOfAttempts: 5,
    startingDelay: 500
  })
  console.info(`There are ${peers} connected peers`)
  return parseInt(peers)
}

async function runBrowserTest (browserName, testFunc) {
  const extension = getExtension(browserName)

  console.info(`Checking if ${extension} exists`)
  expect(fs.existsSync(extension)).to.be.true // eslint-disable-line no-unused-expressions

  let browser
  if (browserName === 'chromium') {
    browser = await openChromium(extension)
  } else if (browserName === 'firefox') {
    browser = await openFirefox(extension)
  } else {
    fail(`unknown browser name: ${browserName}`)
  }

  try {
    const url = await backOff(async () => {
      const u = await findExtensionUrl(browser)
      notEqual(u, undefined, 'Extension URL not found yet')
      return u
    }, {
      delayFirstAttempt: true,
      numOfAttempts: 5,
      startingDelay: 500
    })

    const ipfsApiUrl = process.env.IPFS_API_URL || 'http://127.0.0.1:5001'
    await updateExtensionSettings(browser, url, 'ipfsApiUrl', ipfsApiUrl)

    const customGatewayUrl = process.env.CUSTOM_GATEWAY_URL || 'http://localhost:8080'
    await updateExtensionSettings(browser, url, 'customGatewayUrl', customGatewayUrl)

    return await testFunc(browser, url)
  } finally {
    await browser.quit()
  }
}

async function checkNumberOfConnectedPeers (browser, url) {
  const peers = await getNumberOfConnectedPeers(browser, url)
  expect(peers).not.to.equal(0)
}

describe('ipfs-companion', function () {
  before(function () {
    if (process.env.TEST_E2E !== 'true') {
      this.skip()
    }
  })
  it('should be able to discover peers in Chromium', async function () {
    await runBrowserTest('chromium', checkNumberOfConnectedPeers)
  })
  it('should be able to discover peers in Firefox', async function () {
    await runBrowserTest('firefox', checkNumberOfConnectedPeers)
  })
})


text:
'use strict'
/* eslint-env browser, webextensions */

import { describe, it, before, beforeEach, after } from 'mocha'
import { expect } from 'chai'
import browser from 'sinon-chrome'
import createRuntimeChecks from '../../../add-on/src/lib/runtime-checks.js'
const promiseStub = (result) => () => Promise.resolve(result)

describe('runtime-checks.js', function () {
  before(() => {
    global.browser = browser
    browser.runtime.id = 'testid'
    global.chrome = {}
  })

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

  describe('isFirefox', function () {
    beforeEach(function () {
      browser.flush()
    })

    it('should return true when in Firefox runtime', async function () {
      browser.runtime.getBrowserInfo = promiseStub({ name: 'Firefox' })
      const runtime = await createRuntimeChecks(browser)
      expect(runtime.isFirefox).to.equal(true)
    })

    it('should return false when not in Firefox runtime', async function () {
      browser.runtime.getBrowserInfo = promiseStub({ name: 'SomethingElse' })
      const runtime = await createRuntimeChecks(browser)
      expect(runtime.isFirefox).to.equal(false)
    })
  })

  describe('requiresXHRCORSfix', function () {
    beforeEach(function () {
      browser.flush()
    })

    it('should return true when in Firefox runtime < 69', async function () {
      browser.runtime.getBrowserInfo = promiseStub({ name: 'Firefox', version: '68.0.0' })
      const runtime = await createRuntimeChecks(browser)
      expect(runtime.requiresXHRCORSfix).to.equal(true)
    })

    it('should return false when in Firefox runtime >= 69', async function () {
      browser.runtime.getBrowserInfo = promiseStub({ name: 'Firefox', version: '69.0.0' })
      const runtime = await createRuntimeChecks(browser)
      expect(runtime.requiresXHRCORSfix).to.equal(false)
    })

    it('should return false when  if getBrowserInfo is not present', async function () {
      browser.runtime.getBrowserInfo = undefined
      const runtime = await createRuntimeChecks(browser)
      expect(runtime.requiresXHRCORSfix).to.equal(false)
    })
  })

  describe('isAndroid', function () {
    beforeEach(function () {
      browser.flush()
    })

    it('should return true when in Android runtime', async function () {
      browser.runtime.getPlatformInfo.returns({ os: 'android' })
      const runtime = await createRuntimeChecks(browser)
      expect(runtime.isAndroid).to.equal(true)
    })

    it('should return false when not in Android runtime', async function () {
      browser.runtime.getPlatformInfo.returns({ name: 'SomethingElse' })
      const runtime = await createRuntimeChecks(browser)
      expect(runtime.isAndroid).to.equal(false)
    })
  })

  describe('hasNativeProtocolHandler', function () {
    beforeEach(function () {
      browser.flush()
    })

    it('should return true when browser.protocol namespace is present', async function () {
      // pretend API is in place
      browser.protocol = {}
      browser.protocol.registerStringProtocol = () => Promise.resolve({})
      const runtime = await createRuntimeChecks(browser)
      expect(runtime.hasNativeProtocolHandler).to.equal(true)
    })

    it('should return false browser.protocol APIs are missing', async function () {
      // API is missing
      browser.protocol = undefined
      const runtime = await createRuntimeChecks(browser)
      expect(runtime.hasNativeProtocolHandler).to.equal(false)
    })
  })

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


text:
'use strict'
import { describe, it, before, beforeEach, after } from 'mocha'
import { expect } from 'chai'
import { URL } from 'url'
import sinon from 'sinon'
import createDnslinkResolver from '../../../add-on/src/lib/dnslink.js'
import { initState } from '../../../add-on/src/lib/state.js'
import { optionDefaults } from '../../../add-on/src/lib/options.js'

const testOptions = Object.assign({}, optionDefaults, {
  customGatewayUrl: 'http://127.0.0.1:8080',
  publicGatewayUrl: 'https://gateway.foobar.io'
})

const dnslinkValue = '/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'

export function spoofDnsTxtRecord (fqdn, dnslinkResolver, value) {
  // spoofs existence of valid DNS TXT record (used on cache miss)
  dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns(value)
}

export function spoofCachedDnslink (fqdn, dnslinkResolver, value) {
  // spoofs existence of valid DNS TXT record (used on cache hit)
  dnslinkResolver.setDnslink(fqdn, value)
}

function expectNoDnsTxtRecordLookup (fqdn, dnslinkResolver) {
  dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).throws('unexpected DNS TXT lookup', 'only cache should be used')
}

// dnslinkPolicy-agnostic tests
describe('dnslinkResolver', function () {
  before(() => {
    global.URL = URL
  })

  const getState = () => Object.assign(initState(testOptions), {
    peerCount: 1
  })
  const getExternalNodeState = () => Object.assign({}, getState(), { ipfsNodeType: 'external' })
  // const getEmbeddedNodeState = () => Object.assign({}, getState(), { ipfsNodeType: 'embedded' })

  describe('convertToIpnsPath(url)', function () {
    it('should return IPNS path', function () {
      const url = new URL('http://en.wikipedia-on-ipfs.org/sketches/ipld_intro.html?a=b#c=d')
      const dnslinkResolver = createDnslinkResolver(getExternalNodeState)
      expect(dnslinkResolver.convertToIpnsPath(url))
        .to.equal('/ipns/en.wikipedia-on-ipfs.org/sketches/ipld_intro.html?a=b#c=d')
    })
  })
})

// in this mode lookups are triggered externally, so all methods should work 'offline' using data from internal DNSLink cache
describe('dnslinkResolver (dnslinkPolicy=detectIpfsPathHeader)', function () {
  before(() => {
    global.URL = URL
  })

  let getState
  beforeEach(() => {
    // ensure each case uses clean state
    getState = () => Object.assign(initState(testOptions), {
      ipfsNodeType: 'external',
      dnslinkPolicy: 'detectIpfsPathHeader',
      redirect: true,
      peerCount: 1
    })
  })
  const getExternalNodeState = () => Object.assign(getState(), { ipfsNodeType: 'external' })
  const getEmbeddedNodeState = () => Object.assign(getState(), { ipfsNodeType: 'embedded' })

  describe('dnslinkAtGateway(url)', function () {
    ['/api/v0/foo', '/ipfs/foo', '/ipns/foo'].forEach(path => {
      it('should return nothing if dnslink is present in cache but path starts with ' + path, function () {
        const url = new URL('https://dnslinksite1.io' + path)
        const dnslinkResolver = createDnslinkResolver(getState)
        dnslinkResolver.setDnslink(url.hostname, '/ipfs/bafybeigxjv2o4jse2lajbd5c7xxl5rluhyqg5yupln42252e5tcao7hbge')
        expectNoDnsTxtRecordLookup(url.hostname, dnslinkResolver)
        expect(dnslinkResolver.dnslinkAtGateway(url.toString())).to.equal(undefined)
      })
    })
    it('[external node] should return redirect to custom gateway if dnslink is present in cache', function () {
      const url = new URL('https://dnslinksite4.io/foo/barl?a=b#c=d')
      const dnslinkResolver = createDnslinkResolver(getExternalNodeState)
      dnslinkResolver.setDnslink(url.hostname, '/ipfs/bafybeigxjv2o4jse2lajbd5c7xxl5rluhyqg5yupln42252e5tcao7hbge')
      expectNoDnsTxtRecordLookup(url.hostname, dnslinkResolver)
      // note: locahost will redirect to subdomain if its go-ipfs >0.5,
      // so companion does not need to handle that
      expect(dnslinkResolver.dnslinkAtGateway(url.toString()))
        .to.equal('http://localhost:8080/ipns/dnslinksite4.io/foo/barl?a=b#c=d')
    })
    it('[external node] should return redirect to public gateway if dnslink is present in cache but redirect to local gw is off', function () {
      const oldState = getState
      getState = () => Object.assign(oldState(), { redirect: false })
      const url = new URL('https://dnslinksite4.io/foo/barl?a=b#c=d')
      const dnslinkResolver = createDnslinkResolver(getExternalNodeState)
      dnslinkResolver.setDnslink(url.hostname, '/ipfs/bafybeigxjv2o4jse2lajbd5c7xxl5rluhyqg5yupln42252e5tcao7hbge')
      expectNoDnsTxtRecordLookup(url.hostname, dnslinkResolver)
      // note: locahost will redirect to subdomain if its go-ipfs >0.5,
      // so companion does not need to handle that
      expect(dnslinkResolver.dnslinkAtGateway(url.toString()))
        .to.equal('https://gateway.foobar.io/ipns/dnslinksite4.io/foo/barl?a=b#c=d')
    })
    it('[embedded node] should return redirect to public gateway if dnslink is present in cache', function () {
      const url = new URL('https://dnslinksite4.io/foo/barl?a=b#c=d')
      const dnslinkResolver = createDnslinkResolver(getEmbeddedNodeState)
      dnslinkResolver.setDnslink(url.hostname, '/ipfs/bafybeigxjv2o4jse2lajbd5c7xxl5rluhyqg5yupln42252e5tcao7hbge')
      expectNoDnsTxtRecordLookup(url.hostname, dnslinkResolver)
      expect(dnslinkResolver.dnslinkAtGateway(url.toString()))
        .to.equal('https://gateway.foobar.io/ipns/dnslinksite4.io/foo/barl?a=b#c=d')
    })
    it('[external node] should not return redirect to custom gateway if dnslink is not in cache and path does not belong to a gateway', function () {
      const url = new URL('https://dnslinksite4.io/foo/barl?a=b#c=d')
      const dnslinkResolver = createDnslinkResolver(getExternalNodeState)
      dnslinkResolver.clearCache()
      expectNoDnsTxtRecordLookup(url.hostname, dnslinkResolver)
      expect(dnslinkResolver.dnslinkAtGateway(url.toString())).to.equal(undefined)
    })
    it('[embedded node] should not return redirect to public gateway if dnslink is not in cache and path does not belong to a gateway', function () {
      const url = new URL('https://dnslinksite4.io/foo/barl?a=b#c=d')
      const dnslinkResolver = createDnslinkResolver(getEmbeddedNodeState)
      dnslinkResolver.clearCache()
      expectNoDnsTxtRecordLookup(url.hostname, dnslinkResolver)
      expect(dnslinkResolver.dnslinkAtGateway(url.toString())).to.equal(undefined)
    })
  })

  describe('canRedirectToIpns(url)', function () {
    const httpGatewayPaths = ['/api/v0/foo', '/ipfs/foo', '/ipns/foo']
    httpGatewayPaths.forEach(path => {
      it('should return false if dnslink is present in cache but path starts with /api/v0/', function () {
        const url = new URL('https://dnslinksite1.io' + path)
        const dnslinkResolver = createDnslinkResolver(getState)
        dnslinkResolver.setDnslink(url.hostname, '/ipfs/bafybeigxjv2o4jse2lajbd5c7xxl5rluhyqg5yupln42252e5tcao7hbge')
        expectNoDnsTxtRecordLookup(url.hostname, dnslinkResolver)
        expect(dnslinkResolver.canRedirectToIpns(url)).to.equal(false)
      })
    })
    const dnslinkCacheStates = [true, false]
    dnslinkCacheStates.forEach(cached => {
      it(`should return ${cached} if dnslink is ${cached ? 'present' : 'not'} in cache and path does not belong to an HTTP gateway`, function () {
        const url = new URL('https://dnslinksite4.io/foo/bar')
        const dnslinkResolver = createDnslinkResolver(getState)
        if (cached) {
          dnslinkResolver.setDnslink(url.hostname, '/ipfs/bafybeigxjv2o4jse2lajbd5c7xxl5rluhyqg5yupln42252e5tcao7hbge')
        } else {
          dnslinkResolver.clearCache()
        }
        expectNoDnsTxtRecordLookup(url.hostname, dnslinkResolver)
        expect(dnslinkResolver.canRedirectToIpns(url)).to.equal(cached)
      })
    })
  })
})

// this policy triggers actual DNS TXT lookups
describe('dnslinkResolver (dnslinkPolicy=enabled)', function () {
  before(() => {
    global.URL = URL
  })

  const getState = () => Object.assign(initState(testOptions), {
    ipfsNodeType: 'external',
    dnslinkPolicy: 'enabled',
    peerCount: 1
  })
  const getExternalNodeState = () => Object.assign(getState(), { ipfsNodeType: 'external' })
  const getEmbeddedNodeState = () => Object.assign(getState(), { ipfsNodeType: 'embedded' })

  describe('dnslinkAtGateway(url)', function () {
    ['/api/v0/foo', '/ipfs/foo', '/ipns/foo'].forEach(path => {
      it('should return nothing if DNS TXT record is present but path starts with ' + path, function () {
        const url = new URL('https://dnslinksite1.io' + path)
        const dnslinkResolver = createDnslinkResolver(getState)
        spoofDnsTxtRecord(url.hostname, dnslinkResolver, dnslinkValue)
        expect(dnslinkResolver.dnslinkAtGateway(url.toString())).to.equal(undefined)
      })
    })
    it('[external node] should return redirect to custom gateway if DNS TXT record is present and path does not belong to a gateway', function () {
      const url = new URL('https://dnslinksite4.io/foo/barl?a=b#c=d')
      const dnslinkResolver = createDnslinkResolver(getExternalNodeState)
      spoofDnsTxtRecord(url.hostname, dnslinkResolver, dnslinkValue)
      // note: locahost will redirect to subdomain if its go-ipfs >0.5,
      // so companion does not need to handle that
      expect(dnslinkResolver.dnslinkAtGateway(url.toString()))
        .to.equal('http://localhost:8080/ipns/dnslinksite4.io/foo/barl?a=b#c=d')
    })
    it('[embedded node] should return redirect to public gateway if DNS TXT record is present and path does not belong to a gateway', function () {
      const url = new URL('https://dnslinksite4.io/foo/barl?a=b#c=d')
      const dnslinkResolver = createDnslinkResolver(getEmbeddedNodeState)
      spoofDnsTxtRecord(url.hostname, dnslinkResolver, dnslinkValue)
      expect(dnslinkResolver.dnslinkAtGateway(url.toString()))
        .to.equal('https://gateway.foobar.io/ipns/dnslinksite4.io/foo/barl?a=b#c=d')
    })
  })

  describe('canRedirectToIpns(url)', function () {
    it('should return false if dnslink is present but path starts with /api/v0/', function () {
      const url = new URL('https://dnslinksite1.io/api/v0/dns/ipfs.io')
      const dnslinkResolver = createDnslinkResolver(getState)
      spoofDnsTxtRecord(url.hostname, dnslinkResolver, dnslinkValue)
      expect(dnslinkResolver.canRedirectToIpns(url)).to.equal(false)
    })
    it('should return false if dnslink is present but path starts with /ipfs/', function () {
      const url = new URL('https://dnslinksite2.io/ipfs/foo/bar')
      const dnslinkResolver = createDnslinkResolver(getState)
      spoofDnsTxtRecord(url.hostname, dnslinkResolver, dnslinkValue)
      expect(dnslinkResolver.canRedirectToIpns(url)).to.equal(false)
    })
    it('should return false if dnslink is present but path starts with /ipfs/', function () {
      const url = new URL('https://dnslinksite3.io/ipns/foo/bar')
      const dnslinkResolver = createDnslinkResolver(getState)
      spoofDnsTxtRecord(url.hostname, dnslinkResolver, dnslinkValue)
      expect(dnslinkResolver.canRedirectToIpns(url)).to.equal(false)
    })
    const dnsTxtRecordPresence = [undefined, '/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR']
    dnsTxtRecordPresence.forEach(present => {
      it(`should return ${Boolean(present)} if DNS TXT record is ${present ? 'present' : 'missing'} and path does not belong to an HTTP gateway`, function () {
        const url = new URL('https://dnslinksite4.io/foo/bar')
        const dnslinkResolver = createDnslinkResolver(getState)
        spoofDnsTxtRecord(url.hostname, dnslinkResolver, present)
        expect(dnslinkResolver.canRedirectToIpns(url)).to.equal(Boolean(present))
      })
    })
  })

  describe('findDNSLinkHostname(url)', function () {
    it('should match <fqdn> directly', function () {
      const fqdn = 'dnslink-site.com'
      const url = new URL(`https://${fqdn}/some/path?ds=sdads#dfsdf`)
      const dnslinkResolver = createDnslinkResolver(getState)
      spoofDnsTxtRecord(fqdn, dnslinkResolver, dnslinkValue)
      expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn)
    })
    it('should return nothing if no DNSLink record', function () {
      const url = new URL('https://no-dnslink.example.com/some/path?ds=sdads#dfsdf')
      const dnslinkResolver = createDnslinkResolver(getState)
      expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(undefined)
    })
    it('should match /ipns/<fqdn> on path gateway', function () {
      const fqdn = 'dnslink-site.com'
      const url = `https://path-gateway.com/ipns/${fqdn}/some/path?ds=sdads#dfsdf`
      const dnslinkResolver = createDnslinkResolver(getState)
      spoofDnsTxtRecord(fqdn, dnslinkResolver, dnslinkValue)
      expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn)
    })
    it('should match <fqdn>.ipns on local subdomain gateway', function () {
      const fqdn = 'dnslink-site.com'
      const url = `https://${fqdn}.ipns.localhost:8080/some/path?ds=sdads#dfsdf`
      const dnslinkResolver = createDnslinkResolver(getState)
      spoofDnsTxtRecord(fqdn, dnslinkResolver, dnslinkValue)
      expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn)
    })
    it('should match <fqdn>.ipns on public subdomain gateway', function () {
      const fqdn = 'dnslink-site.com'
      const url = `https://${fqdn}.ipns.dweb.link/some/path?ds=sdads#dfsdf`
      const dnslinkResolver = createDnslinkResolver(getState)
      spoofDnsTxtRecord(fqdn, dnslinkResolver, dnslinkValue)
      expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn)
    })
    it('should match <dns-label-inlined-fqdn>.ipns on public subdomain gateway', function () {
      // Context: https://github.com/ipfs/in-web-browsers/issues/169
      const fqdn = 'dnslink-site.com'
      const fqdnInDNSLabel = 'dnslink--site-com'
      const url = `https://${fqdnInDNSLabel}.ipns.dweb.link/some/path?ds=sdads#dfsdf`
      const dnslinkResolver = createDnslinkResolver(getState)
      spoofCachedDnslink(fqdnInDNSLabel, dnslinkResolver, false)
      spoofCachedDnslink(fqdn, dnslinkResolver, dnslinkValue)
      expect(dnslinkResolver.findDNSLinkHostname(url)).to.equal(fqdn)
    })
  })

  after(() => {
    delete global.URL
  })
})


text:
'use strict'
import { stub } from 'sinon'
import { describe, it, beforeEach, afterEach } from 'mocha'
import { expect } from 'chai'
import { URL } from 'url'
import { ipfsUri, ipfsContentPath, createIpfsPathValidator, sameGateway, safeHostname } from '../../../add-on/src/lib/ipfs-path.js'
import { initState } from '../../../add-on/src/lib/state.js'
import createDnslinkResolver from '../../../add-on/src/lib/dnslink.js'
import { optionDefaults } from '../../../add-on/src/lib/options.js'
import { spoofCachedDnslink } from './dnslink.test.js'

function spoofIpfsResolve (ipfs, path, value) {
  const resolve = stub(ipfs, 'resolve')
  resolve.withArgs(path).resolves(value)
  resolve.throws((arg) => new Error(`Unexpected stubbed call ipfs.resolve(${arg})`))
}

// https://github.com/ipfs/ipfs-companion/issues/303
describe('ipfs-path.js', function () {
  let ipfs, state, dnslinkResolver, ipfsPathValidator

  beforeEach(function () {
    global.URL = URL
    state = Object.assign(initState(optionDefaults), { peerCount: 1 })
    ipfs = {
      name: {
        resolve (arg) {
          throw new Error(`Unexpected call ipfs.name.resolve(${arg})`)
        }
      },
      resolve (arg) {
        throw new Error(`Unexpected call ipfs.resolve(${arg})`)
      }
    }
    dnslinkResolver = createDnslinkResolver(() => state)
    ipfsPathValidator = createIpfsPathValidator(() => state, () => ipfs, dnslinkResolver)
  })

  afterEach(function () {
    if (ipfs.name.resolve.reset) ipfs.name.resolve.reset()
    if (ipfs.resolve.reset) ipfs.resolve.reset()
  })

  // TODO: move to some lib?
  describe('ipfsContentPath', function () {
    it('should detect /ipfs/ path in URL from a public gateway', function () {
      const url = 'https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
      expect(ipfsContentPath(url)).to.equal('/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar')
    })
    it('should detect /ipfs/ path in detached IPFS path', function () {
      const path = '/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
      expect(ipfsContentPath(path)).to.equal('/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar')
    })
    it('should detect /ipns/ path in URL from a public gateway', function () {
      const url = 'https://ipfs.io/ipns/libp2p.io/bundles/'
      expect(ipfsContentPath(url)).to.equal('/ipns/libp2p.io/bundles/')
    })
    it('should detect /ipns/ path in detached IPFS path', function () {
      const path = '/ipns/libp2p.io/bundles/'
      expect(ipfsContentPath(path)).to.equal('/ipns/libp2p.io/bundles/')
    })
    it('should drop search and hash from URL by default', function () {
      const url = 'https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest'
      expect(ipfsContentPath(url)).to.equal('/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR')
    })
    it('should keep search and hash from URL if keepURIParams=true', function () {
      const url = 'https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest'
      expect(ipfsContentPath(url, { keepURIParams: true })).to.equal('/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
    })
    it('should drop search and hash in detached IPFS path by default', function () {
      const path = '/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest'
      expect(ipfsContentPath(path)).to.equal('/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR')
    })
    it('should preserve search and hash in detached IPFS path if keepURIParams=true', function () {
      const path = '/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest'
      expect(ipfsContentPath(path, { keepURIParams: true })).to.equal('/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
    })
    it('should decode special characters in URL', function () {
      const url = 'https://ipfs.io/ipfs/Qmb8wsGZNXt5VXZh1pEmYynjB6Euqpq3HYyeAdw2vScTkQ/1%20-%20Barrel%20-%20Part%201'
      expect(ipfsContentPath(url)).to.equal('/ipfs/Qmb8wsGZNXt5VXZh1pEmYynjB6Euqpq3HYyeAdw2vScTkQ/1 - Barrel - Part 1')
    })
    it('should decode special characters in path', function () {
      const path = '/ipfs/Qmb8wsGZNXt5VXZh1pEmYynjB6Euqpq3HYyeAdw2vScTkQ/1%20-%20Barrel%20-%20Part%201'
      expect(ipfsContentPath(path)).to.equal('/ipfs/Qmb8wsGZNXt5VXZh1pEmYynjB6Euqpq3HYyeAdw2vScTkQ/1 - Barrel - Part 1')
    })
    it('should resolve CID-in-subdomain URL to IPFS path', function () {
      const url = 'https://bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa.ipfs.dweb.link/wiki/Mars.html?argTest#hashTest'
      expect(ipfsContentPath(url)).to.equal('/ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html')
    })
    it('should resolve CID(libp2p-key)-in-subdomain URL to IPNS path', function () {
      const url = 'https://k2k4r8ncs1yoluq95unsd7x2vfhgve0ncjoggwqx9vyh3vl8warrcp15.ipns.dweb.link/wiki/Mars.html?argTest#hashTest'
      expect(ipfsContentPath(url)).to.equal('/ipns/k2k4r8ncs1yoluq95unsd7x2vfhgve0ncjoggwqx9vyh3vl8warrcp15/wiki/Mars.html')
    })
    it('should resolve dnslink-in-subdomain URL to IPNS path', function () {
      const url = 'http://en.wikipedia-on-ipfs.org.ipns.localhost:8080/wiki/Mars.html?argTest#hashTest'
      expect(ipfsContentPath(url)).to.equal('/ipns/en.wikipedia-on-ipfs.org/wiki/Mars.html')
    })
    it('should resolve inlined-dnslink-in-subdomain URL to IPNS path', function () {
      const url = 'https://en-wikipedia--on--ipfs-org.ipns.dweb.link/wiki/Mars.html?argTest#hashTest'
      expect(ipfsContentPath(url)).to.equal('/ipns/en.wikipedia-on-ipfs.org/wiki/Mars.html')
    })
    it('should return null if there is no valid path for input URL', function () {
      const url = 'https://foo.io/invalid/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest'
      expect(ipfsContentPath(url)).to.equal(null)
    })
    it('should return null if there is no valid path for input path', function () {
      const path = '/invalid/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
      expect(ipfsContentPath(path)).to.equal(null)
    })
  })

  describe('safeHostname', function () {
    it('should return URL.hostname on http URL', function () {
      const url = 'https://example.com:8080/path/file.txt'
      expect(safeHostname(url)).to.equal('example.com')
    })
    it('should return null on error', function () {
      const url = ''
      expect(safeHostname(url)).to.equal(null)
    })
  })

  // TODO: move to some lib?
  describe('ipfsUri', function () {
    it('should detect /ipfs/ path in URL from a public gateway', function () {
      const url = 'https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
      expect(ipfsUri(url)).to.equal('ipfs://QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar')
    })
    it('should detect /ipfs/ path in detached IPFS path', function () {
      const path = '/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
      expect(ipfsUri(path)).to.equal('ipfs://QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar')
    })
    it('should detect /ipns/ path in URL from a public gateway', function () {
      const url = 'https://ipfs.io/ipns/libp2p.io/bundles/'
      expect(ipfsUri(url)).to.equal('ipns://libp2p.io/bundles/')
    })
    it('should detect /ipns/ path in detached IPFS path', function () {
      const path = '/ipns/libp2p.io/bundles/'
      expect(ipfsUri(path)).to.equal('ipns://libp2p.io/bundles/')
    })
    it('should keep search and hash from original URL', function () {
      const url = 'https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest'
      expect(ipfsUri(url)).to.equal('ipfs://QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
    })
    it('should preserve search and hash in detached IPFS path', function () {
      const path = '/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest'
      expect(ipfsUri(path)).to.equal('ipfs://QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
    })
    it('should decode special characters in URL', function () {
      const url = 'https://ipfs.io/ipfs/Qmb8wsGZNXt5VXZh1pEmYynjB6Euqpq3HYyeAdw2vScTkQ/1%20-%20Barrel%20-%20Part%201'
      expect(ipfsUri(url)).to.equal('ipfs://Qmb8wsGZNXt5VXZh1pEmYynjB6Euqpq3HYyeAdw2vScTkQ/1 - Barrel - Part 1')
    })
    it('should decode special characters in path', function () {
      const path = '/ipfs/Qmb8wsGZNXt5VXZh1pEmYynjB6Euqpq3HYyeAdw2vScTkQ/1%20-%20Barrel%20-%20Part%201'
      expect(ipfsUri(path)).to.equal('ipfs://Qmb8wsGZNXt5VXZh1pEmYynjB6Euqpq3HYyeAdw2vScTkQ/1 - Barrel - Part 1')
    })
    it('should resolve CID-in-subdomain URL to IPFS path', function () {
      const url = 'https://bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa.ipfs.dweb.link/wiki/Mars.html?argTest#hashTest'
      expect(ipfsUri(url)).to.equal('ipfs://bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html?argTest#hashTest')
    })
    it('should return null if there is no valid path for input URL', function () {
      const url = 'https://foo.io/invalid/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest'
      expect(ipfsUri(url)).to.equal(null)
    })
    it('should return null if there is no valid path for input path', function () {
      const path = '/invalid/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
      expect(ipfsUri(path)).to.equal(null)
    })
  })

  describe('sameGateway', function () {
    it('should return true on direct host match', function () {
      const url = 'https://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
      const gw = 'http://127.0.0.1:8080'
      expect(sameGateway(url, gw)).to.equal(true)
    })
    it('should return true on localhost/127.0.0.1 host match', function () {
      const url = 'https://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
      const gw = 'http://127.0.0.1:8080'
      expect(sameGateway(url, gw)).to.equal(true)
    })
    it('should return true on 127.0.0.1/localhost host match', function () {
      const url = 'https://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
      const gw = 'http://localhost:8080'
      expect(sameGateway(url, gw)).to.equal(true)
    })
    it('should return true on localhost subdomain host match', function () {
      const url = 'http://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi.ipfs.localhost:8080/foo/bar'
      const gw = 'http://localhost:8080'
      expect(sameGateway(url, gw)).to.equal(true)
    })
    it('should return true on RPC webui path 127.0.0.1/0.0.0.0 host match', function () {
      // this is misconfiguration, but handling it in sameGateway ensures users who do this dont waste our time debugging
      const url = 'http://0.0.0.0:5001/webui'
      const api = 'http://127.0.0.1:5001'
      expect(sameGateway(url, api)).to.equal(true)
    })
    it('should return true on RPC /api/v0 path 127.0.0.1/0.0.0.0 host match', function () {
      // this is misconfiguration, but handling it in sameGateway ensures users who do this dont waste our time debugging
      const url = 'http://0.0.0.0:5001/api/v0/id'
      const api = 'http://127.0.0.1:5001'
      expect(sameGateway(url, api)).to.equal(true)
    })
    it('should return false on hostname match but different port', function () {
      const url = 'https://localhost:8081/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
      const gw = 'http://localhost:8080'
      expect(sameGateway(url, gw)).to.equal(false)
    })
  })

  describe('validIpfsOrIpns', function () {
    // this is just a smoke test, extensive tests are in is-ipfs package
    it('should return true for IPFS NURI', function () {
      const path = '/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest'
      expect(ipfsPathValidator.validIpfsOrIpns(path)).to.equal(true)
    })
    it('should return false for non-IPFS NURI', function () {
      const path = '/ipfs/NotAValidCid'
      expect(ipfsPathValidator.validIpfsOrIpns(path)).to.equal(false)
    })
    // this is just a smoke test, extensive tests are in is-ipfs package
    it('should return true for URL at IPFS Gateway', function () {
      const url = 'https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest'
      expect(ipfsPathValidator.validIpfsOrIpns(url)).to.equal(true)
    })
    it('should return false for non-IPFS URL', function () {
      const url = 'https://ipfs.io/ipfs/NotACid?argTest#hashTest'
      expect(ipfsPathValidator.validIpfsOrIpns(url)).to.equal(false)
    })
  })

  describe('publicIpfsOrIpnsResource', function () {
    it('should return true for URL at Public IPFS Gateway', function () {
      const url = `${state.pubGwURL}ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest`
      expect(ipfsPathValidator.publicIpfsOrIpnsResource(url)).to.equal(true)
    })
    it('should return false for URL at Local IPFS Gateway', function () {
      const url = `${state.gwURL}ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest`
      expect(ipfsPathValidator.publicIpfsOrIpnsResource(url)).to.equal(false)
    })
    it('should return false for IPFS URL at API port', function () {
      const url = `${state.apiURL}ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest`
      expect(ipfsPathValidator.publicIpfsOrIpnsResource(url)).to.equal(false)
      expect(ipfsPathValidator.validIpfsOrIpns(url)).to.equal(true)
    })
    it('should return false for non-IPFS URL', function () {
      const url = 'https://ipfs.io/ipfs/NotACid?argTest#hashTest'
      expect(ipfsPathValidator.publicIpfsOrIpnsResource(url)).to.equal(false)
      expect(ipfsPathValidator.validIpfsOrIpns(url)).to.equal(false)
    })
    describe('isIpfsPageActionsContext', function () {
      it('should return true for URL at Public IPFS Gateway', function () {
        const url = `${state.pubGwURL}ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest`
        expect(ipfsPathValidator.isIpfsPageActionsContext(url)).to.equal(true)
      })
      it('should return true for URL at Local IPFS Gateway', function () {
        const url = `${state.gwURL}ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest`
        expect(ipfsPathValidator.isIpfsPageActionsContext(url)).to.equal(true)
      })
      it('should return false for IPFS URL at API port', function () {
        const url = `${state.apiURL}ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest`
        expect(ipfsPathValidator.isIpfsPageActionsContext(url)).to.equal(false)
        expect(ipfsPathValidator.validIpfsOrIpns(url)).to.equal(true)
      })
      it('should return false for non-IPFS URL', function () {
        const url = 'https://ipfs.io/ipfs/NotACid?argTest#hashTest'
        expect(ipfsPathValidator.publicIpfsOrIpnsResource(url)).to.equal(false)
        expect(ipfsPathValidator.validIpfsOrIpns(url)).to.equal(false)
      })
    })
  })

  describe('isIpfsPageActionsContext', function () {
    it('should return true for URL at a Gateway', function () {
      const url = 'https://example.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest'
      expect(ipfsPathValidator.isIpfsPageActionsContext(url)).to.equal(true)
    })
    it('should return true for URL at a Gateway with Base32 CIDv1 in subdomain', function () {
      // context-actions are shown on publick gateways that use CID in subdomain as well
      const url = 'http://bafkreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy.ipfs.dweb.link/'
      expect(ipfsPathValidator.isIpfsPageActionsContext(url)).to.equal(true)
    })
    it('should return false for URL at a Gateway with Base58 CIDv0 in subdomain', function () {
      // should not be allowed, but who knows
      const url = 'http://QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR.ipfs.dweb.link/'
      expect(ipfsPathValidator.isIpfsPageActionsContext(url)).to.equal(false)
    })
    it('should return false for non-IPFS URL', function () {
      const url = 'https://example.com/ipfs/NotACid?argTest#hashTest'
      expect(ipfsPathValidator.isIpfsPageActionsContext(url)).to.equal(false)
    })
  })

  describe('isRedirectPageActionsContext', function () {
    it('should return true for /ipfs/ URL at a Gateway', function () {
      const url = 'https://example.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest'
      expect(ipfsPathValidator.isRedirectPageActionsContext(url)).to.equal(true)
    })
    it('should return true for /ipns/ URL at Local Gateway', function () {
      const url = `${state.gwURL}ipns/docs.ipfs.io/?argTest#hashTest`
      expect(ipfsPathValidator.isRedirectPageActionsContext(url)).to.equal(true)
    })
    it('should return false for /ipfs/ URL at Local Gateway', function () {
      const url = `${state.gwURL}ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest`
      expect(ipfsPathValidator.isRedirectPageActionsContext(url)).to.equal(false)
    })
    it('should return false for IPFS content loaded from IPFS API port', function () {
      const url = `${state.apiURL}ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest`
      expect(ipfsPathValidator.isRedirectPageActionsContext(url)).to.equal(false)
    })
    it('should return true for URL at IPFS Gateway with Base32 CIDv1 in subdomain', function () {
      // context-actions are shown on publick gateways that use CID in subdomain as well
      const url = 'http://bafkreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy.ipfs.dweb.link/'
      expect(ipfsPathValidator.isRedirectPageActionsContext(url)).to.equal(true)
    })
    it('should return true for non-IPFS HTTP URL', function () {
      const url = 'http://en.wikipedia.org/wiki/Main_Page'
      expect(ipfsPathValidator.isRedirectPageActionsContext(url)).to.equal(true)
    })
    it('should return true for non-IPFS HTTPS URL', function () {
      const url = 'https://en.wikipedia.org/wiki/Main_Page'
      expect(ipfsPathValidator.isRedirectPageActionsContext(url)).to.equal(true)
    })
    it('should return false for non-HTTP URL', function () {
      const url = 'moz-extension://85076b5e-900c-428f-4bf6-e6c1a33042a7/blank-page.html'
      expect(ipfsPathValidator.isRedirectPageActionsContext(url)).to.equal(false)
    })
  })

  describe('resolveToPublicUrl', function () {
    it('should resolve URL with CID-in-subdomain to itself', function () {
      const url = 'https://bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa.ipfs.dweb.link/wiki/Mars.html?argTest#hashTest'
      expect(ipfsPathValidator.resolveToPublicUrl(url)).to.equal(url)
    })
    it('should resolve URL with /ipfs/ path to the default public gateway', function () {
      const url = 'https://example.com/ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html?argTest#hashTest'
      expect(ipfsPathValidator.resolveToPublicUrl(url)).to.equal(`${state.pubGwURL}ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html?argTest#hashTest`)
    })
    it('should resolve /ipfs/ path to itself attached to the default public gateway', function () {
      const path = '/ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html?argTest#hashTest'
      expect(ipfsPathValidator.resolveToPublicUrl(path)).to.equal(`${state.pubGwURL}ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html?argTest#hashTest`)
    })
    it('should resolve URL with /ipns/ path to the default public gateway', function () {
      const url = 'https://example.com/ipns/docs.ipfs.io/?argTest#hashTest'
      expect(ipfsPathValidator.resolveToPublicUrl(url)).to.equal(`${state.pubGwURL}ipns/docs.ipfs.io/?argTest#hashTest`)
    })
    it('should resolve /ipns/ path to itself at the default public gateway', function () {
      const path = '/ipns/docs.ipfs.io/?argTest#hashTest'
      expect(ipfsPathValidator.resolveToPublicUrl(path)).to.equal(`${state.pubGwURL}ipns/docs.ipfs.io/?argTest#hashTest`)
    })
    it('should resolve non-IPFS URL to itself (DNSLink websites)', function () {
      const url = 'https://example.com/foo/bar/?argTest#hashTest'
      expect(ipfsPathValidator.resolveToPublicUrl(url)).to.equal(url)
    })
    it('should resolve to null if input is an invalid path', function () {
      const path = '/foo/bar/?argTest#hashTest'
      expect(ipfsPathValidator.resolveToPublicUrl(path)).to.equal(null)
    })
    it('should resolve to null if input is an invalid URL', function () {
      const url = 'example.com/foo/bar/?argTest#hashTest'
      expect(ipfsPathValidator.resolveToPublicUrl(url)).to.equal(null)
    })
  })

  describe('resolveToIpfsPath', function () {
    it('should resolve URL with CID-in-subdomain to /ipfs/<cid> path', function () {
      const url = 'https://bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa.ipfs.dweb.link/wiki/Mars.html?argTest#hashTest'
      expect(ipfsPathValidator.resolveToIpfsPath(url)).to.equal('/ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html?argTest#hashTest')
    })
    it('should resolve URL with /ipfs/ path to the path itself', function () {
      const url = 'https://example.com/ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html?argTest#hashTest'
      expect(ipfsPathValidator.resolveToIpfsPath(url)).to.equal('/ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html?argTest#hashTest')
    })
    it('should resolve /ipfs/ path to itself', function () {
      const path = '/ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html?argTest#hashTest'
      expect(ipfsPathValidator.resolveToIpfsPath(path)).to.equal(path)
    })
    it('should resolve URL with /ipns/ path to the path itself', function () {
      const url = 'https://example.com/ipns/docs.ipfs.io/?argTest#hashTest'
      expect(ipfsPathValidator.resolveToIpfsPath(url)).to.equal('/ipns/docs.ipfs.io/?argTest#hashTest')
    })
    it('should resolve /ipns/ path to itself', function () {
      const path = '/ipns/docs.ipfs.io/?argTest#hashTest'
      expect(ipfsPathValidator.resolveToIpfsPath(path)).to.equal(path)
    })
    it('should resolve URL of a DNSLink website to null if the value if DNSLink is not in cache', function () {
      const url = 'https://docs.ipfs.io/guides/concepts/dnslink/?argTest#hashTest'
      spoofCachedDnslink(new URL(url).hostname, dnslinkResolver, undefined)
      expect(ipfsPathValidator.resolveToIpfsPath(url)).to.equal(null)
    })
    it('should resolve URL of a DNSLink website to /ipns/<fqdn> path if DNSLink is in cache', function () {
      const url = 'https://docs.ipfs.io/guides/concepts/dnslink/?argTest#hashTest'
      const dnslinkValue = '/ipns/QmRV5iNhGoxBaAcbucMAW9WtVHbeehXhAdr5CZQDhL55Xk'
      spoofCachedDnslink(new URL(url).hostname, dnslinkResolver, dnslinkValue)
      expect(ipfsPathValidator.resolveToIpfsPath(url)).to.equal('/ipns/docs.ipfs.io/guides/concepts/dnslink/?argTest#hashTest')
    })
    it('should resolve to null if input is an invalid path', function () {
      const path = '/foo/bar/?argTest#hashTest'
      expect(ipfsPathValidator.resolveToIpfsPath(path)).to.equal(null)
    })
    it('should resolve to null if input is an invalid URL', function () {
      const url = 'example.com/foo/bar/?argTest#hashTest'
      expect(ipfsPathValidator.resolveToIpfsPath(url)).to.equal(null)
    })
  })

  describe('async resolveToImmutableIpfsPath', function () {
    it('should resolve URL with CID-in-subdomain to the same value as resolveToIpfsPath', async function () {
      const url = 'https://bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa.ipfs.dweb.link/wiki/Mars.html?argTest#hashTest'
      expect(await ipfsPathValidator.resolveToImmutableIpfsPath(url)).to.equal(ipfsPathValidator.resolveToIpfsPath(url))
    })
    it('should resolve URL with /ipfs/ path to the same value as resolveToIpfsPath', async function () {
      const url = 'https://example.com/ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html?argTest#hashTest'
      expect(await ipfsPathValidator.resolveToImmutableIpfsPath(url)).to.equal(ipfsPathValidator.resolveToIpfsPath(url))
    })
    it('should resolve /ipfs/ path to the same value as resolveToIpfsPath', async function () {
      const path = '/ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html?argTest#hashTest'
      expect(await ipfsPathValidator.resolveToImmutableIpfsPath(path)).to.equal(ipfsPathValidator.resolveToIpfsPath(path))
    })
    it('should resolve URL with /ipns/ path to the immutable /ipfs/ path', async function () {
      const url = 'https://example.com/ipns/docs.ipfs.io/?argTest#hashTest'
      const ipnsPointer = '/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
      spoofIpfsResolve(ipfs, '/ipns/docs.ipfs.io', ipnsPointer)
      expect(await ipfsPathValidator.resolveToImmutableIpfsPath(url)).to.equal(ipnsPointer + '/?argTest#hashTest')
    })
    it('should resolve /ipns/ path to the immutable /ipfs/ one', async function () {
      const ipnsPointer = '/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
      spoofIpfsResolve(ipfs, '/ipns/libp2p.io', ipnsPointer)
      const path = '/ipns/libp2p.io/?argTest#hashTest'
      expect(await ipfsPathValidator.resolveToImmutableIpfsPath(path)).to.equal(ipnsPointer + '/?argTest#hashTest')
    })
    it('should resolve URL of a DNSLink website to null if the value if DNSLink is not in cache', async function () {
      const url = 'https://docs.ipfs.io/guides/concepts/dnslink/?argTest#hashTest'
      spoofCachedDnslink(new URL(url).hostname, dnslinkResolver, undefined)
      expect(await ipfsPathValidator.resolveToImmutableIpfsPath(url)).to.equal(null)
      expect(await ipfsPathValidator.resolveToImmutableIpfsPath(url)).to.equal(ipfsPathValidator.resolveToIpfsPath(url))
    })
    it('should resolve URL of a DNSLink website to the immutable /ipfs/ address behind mutable /ipns/ DNSLink in cache', async function () {
      const url = 'https://docs.ipfs.io/guides/concepts/dnslink/?argTest#hashTest'
      // Use IPNS in DNSLINK to ensure resolveToImmutableIpfsPath does resursive resolv to immutable address
      const dnslinkValue = '/ipns/QmRV5iNhGoxBaAcbucMAW9WtVHbeehXhAdr5CZQDhL55Xk'
      const ipnsPointer = '/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
      const { hostname } = new URL(url)
      spoofCachedDnslink(hostname, dnslinkResolver, dnslinkValue)
      // We need to spoof IPNS lookup for /ipns/<fqdn> because value from DNSLink cache
      // may be out of date and resolveToImmutableIpfsPath does additional resolv
      // to return latest IPNS value
      spoofIpfsResolve(ipfs, `/ipns/${hostname}`, ipnsPointer)
      expect(await ipfsPathValidator.resolveToImmutableIpfsPath(url)).to.equal(ipnsPointer + '/guides/concepts/dnslink/?argTest#hashTest')
    })
    it('should resolve to null if input is an invalid path', async function () {
      const path = '/foo/bar/?argTest#hashTest'
      expect(await ipfsPathValidator.resolveToImmutableIpfsPath(path)).to.equal(null)
      expect(await ipfsPathValidator.resolveToImmutableIpfsPath(path)).to.equal(ipfsPathValidator.resolveToIpfsPath(path))
    })
    it('should resolve to null if input is an invalid URL', async function () {
      const url = 'example.com/foo/bar/?argTest#hashTest'
      expect(await ipfsPathValidator.resolveToImmutableIpfsPath(url)).to.equal(null)
      expect(await ipfsPathValidator.resolveToImmutableIpfsPath(url)).to.equal(ipfsPathValidator.resolveToIpfsPath(url))
    })
  })

  describe('async resolveToCid', function () {
    it('should resolve URL with CID-in-subdomain', async function () {
      const url = 'https://bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa.ipfs.dweb.link/wiki/Mars.html?argTest#hashTest'
      const expectedCid = 'bafkreig6ltd5dojmrhlsweuiaxt3ag2p5644wwcyjmqt6gajmbrbylke4m'
      spoofIpfsResolve(ipfs, '/ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html', '/ipfs/' + expectedCid)
      expect(await ipfsPathValidator.resolveToCid(url)).to.equal(expectedCid)
    })
    it('should resolve URL with /ipfs/ path', async function () {
      const url = 'https://example.com/ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html?argTest#hashTest'
      const expectedCid = 'bafkreig6ltd5dojmrhlsweuiaxt3ag2p5644wwcyjmqt6gajmbrbylke4m'
      spoofIpfsResolve(ipfs, '/ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html', '/ipfs/' + expectedCid)
      expect(await ipfsPathValidator.resolveToCid(url)).to.equal(expectedCid)
    })
    it('should resolve /ipfs/ path', async function () {
      const path = '/ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html?argTest#hashTest'
      const expectedCid = 'bafkreig6ltd5dojmrhlsweuiaxt3ag2p5644wwcyjmqt6gajmbrbylke4m'
      spoofIpfsResolve(ipfs, '/ipfs/bafybeicgmdpvw4duutrmdxl4a7gc52sxyuk7nz5gby77afwdteh3jc5bqa/wiki/Mars.html', '/ipfs/' + expectedCid)
      expect(await ipfsPathValidator.resolveToCid(path)).to.equal(expectedCid)
    })
    it('should resolve URL with /ipns/ path', async function () {
      const url = 'https://example.com/ipns/docs.ipfs.io/foo/bar?argTest#hashTest'
      const expectedCid = 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
      spoofIpfsResolve(ipfs, '/ipns/docs.ipfs.io/foo/bar', `/ipfs/${expectedCid}`)
      expect(await ipfsPathValidator.resolveToCid(url)).to.equal(expectedCid)
    })
    it('should resolve /ipns/ path to the immutable /ipfs/ one', async function () {
      const path = '/ipns/libp2p.io/foo/bar?argTest#hashTest'
      const expectedCid = 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
      spoofIpfsResolve(ipfs, '/ipns/libp2p.io/foo/bar', `/ipfs/${expectedCid}`)
      expect(await ipfsPathValidator.resolveToCid(path)).to.equal(expectedCid)
    })
    it('should resolve URL of a DNSLink website to null if the value if DNSLink is not in cache', async function () {
      const url = 'https://docs.ipfs.io/guides/concepts/dnslink/?argTest#hashTest'
      spoofCachedDnslink(new URL(url).hostname, dnslinkResolver, undefined)
      expect(await ipfsPathValidator.resolveToCid(url)).to.equal(null)
    })
    it('should resolve URL of a DNSLink website if DNSLink is in cache', async function () {
      const url = 'https://docs.ipfs.io/guides/concepts/dnslink/?argTest#hashTest'
      // Use IPNS in DNSLINK to ensure resolveToImmutableIpfsPath does resursive resolv to immutable address
      const expectedCid = 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
      const dnslinkValue = '/ipns/QmRV5iNhGoxBaAcbucMAW9WtVHbeehXhAdr5CZQDhL55Xk'
      const { hostname } = new URL(url)
      spoofCachedDnslink(hostname, dnslinkResolver, dnslinkValue)
      // Note the DNSLink value is ignored, and /ipns/<fqdn> is passed to ipfs.resolv internally
      // This ensures the latest pointer is returned, instead of stale value from DNSLink cache
      spoofIpfsResolve(ipfs, '/ipns/docs.ipfs.io/guides/concepts/dnslink/', `/ipfs/${expectedCid}`)
      expect(await ipfsPathValidator.resolveToCid(url)).to.equal(expectedCid)
    })
    // TODO: remove when https://github.com/ipfs/js-ipfs/issues/1918 is addressed
    it('should resolve URL of a DNSLink website if DNSLink is in cache (js-ipfs fallback)', async function () {
      const url = 'https://docs.ipfs.io/guides/concepts/dnslink/?argTest#hashTest'
      // Use IPNS in DNSLINK to ensure resolveToImmutableIpfsPath does resursive resolv to immutable address
      const expectedCid = 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
      const dnslinkValue = '/ipns/QmRV5iNhGoxBaAcbucMAW9WtVHbeehXhAdr5CZQDhL55Xk'
      const { hostname } = new URL(url)
      spoofCachedDnslink(hostname, dnslinkResolver, dnslinkValue)
      // Note the DNSLink value is ignored, and /ipns/<fqdn> is passed to ipfs.resolv internally
      // This ensures the latest pointer is returned, instead of stale value from DNSLink cache
      // js-ipfs v0.34 does not support DNSLinks in ipfs.name.resolve: https://github.com/ipfs/js-ipfs/issues/1918
      const resolve = stub(ipfs, 'resolve')
      resolve.withArgs('/ipns/docs.ipfs.io/guides/concepts/dnslink/').throws(new Error('resolve non-IPFS names is not implemented'))
      // until it is implemented, we have a workaround that falls back to value from dnslink
      resolve.withArgs('/ipns/QmRV5iNhGoxBaAcbucMAW9WtVHbeehXhAdr5CZQDhL55Xk/guides/concepts/dnslink/').resolves(`/ipfs/${expectedCid}`)
      resolve.throws((arg) => new Error(`Unexpected stubbed call ipfs.resolve(${arg})`))
      expect(await ipfsPathValidator.resolveToCid(url)).to.equal(expectedCid)
    })
    it('should resolve to null if input is an invalid path', async function () {
      const path = '/foo/bar/?argTest#hashTest'
      expect(await ipfsPathValidator.resolveToCid(path)).to.equal(null)
    })
    it('should resolve to null if input is an invalid URL', async function () {
      const url = 'example.com/foo/bar/?argTest#hashTest'
      expect(await ipfsPathValidator.resolveToCid(url)).to.equal(null)
    })
  })
})