From 8418e311f44363904fefdae41948102cd8177542 Mon Sep 17 00:00:00 2001 From: galargh Date: Mon, 19 Dec 2022 17:20:50 +0100 Subject: [PATCH] feat: add E2E tests for chromium and firefox --- Dockerfile | 9 +- ci/access-control-allow-all.sh | 5 + ci/download-release-artifacts.sh | 11 +++ docker-compose.e2e.yml | 38 ++++++++ package-lock.json | 56 ++++++++++++ package.json | 8 +- test/e2e/ipfs-companion.test.js | 152 +++++++++++++++++++++++++++++++ 7 files changed, 275 insertions(+), 4 deletions(-) create mode 100755 ci/access-control-allow-all.sh create mode 100755 ci/download-release-artifacts.sh create mode 100644 docker-compose.e2e.yml create mode 100644 test/e2e/ipfs-companion.test.js diff --git a/Dockerfile b/Dockerfile index 57ebc305d..8100ddc36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,19 +6,22 @@ ARG GROUP_ID RUN curl -s https://ipfs.io/ipfs/QmbukYcmtyU6ZEKt6fepnvrTNa9F6VqsUPMUgNxQjEmphH > /usr/local/bin/jq && chmod +x /usr/local/bin/jq RUN mkdir -p /home/node/app -WORKDIR /home/node/app - RUN if [ ${USER_ID:-0} -ne 0 ] && [ ${GROUP_ID:-0} -ne 0 ]; then \ userdel -f node && \ if getent group node ; then groupdel node; fi && \ groupadd -g ${GROUP_ID} node && \ useradd -l -u ${USER_ID} -g node node && \ chown -fhR ${USER_ID}:${GROUP_ID} /home/node; fi +RUN chown node:node /home/node/app -COPY --chown=${USER_ID}:${GROUP_ID} . /home/node/app +WORKDIR /home/node/app + +COPY --chown=node:node ./package.json ./package-lock.json /home/node/app/ USER node RUN npm run ci:install +COPY --chown=node:node . /home/node/app + ENV PATH="/home/node/app/node_modules/.bin:${PATH}" diff --git a/ci/access-control-allow-all.sh b/ci/access-control-allow-all.sh new file mode 100755 index 000000000..4b7426327 --- /dev/null +++ b/ci/access-control-allow-all.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -ex + +ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]' +ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["*"]' diff --git a/ci/download-release-artifacts.sh b/ci/download-release-artifacts.sh new file mode 100755 index 000000000..d4615c400 --- /dev/null +++ b/ci/download-release-artifacts.sh @@ -0,0 +1,11 @@ +#!/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://github.com/gitapi/repos/ipfs/ipfs-companion/releases/tags/v$IPFS_COMPANION_VERSION" | jq '.id')" +assets="$(curl --retry 5 --no-progress-meter --location "https://github.com/gitapi/repos/ipfs/ipfs-companion/releases/$id/assets" | jq -r '.[].name')" + +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 diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml new file mode 100644 index 000000000..b492adff5 --- /dev/null +++ b/docker-compose.e2e.yml @@ -0,0 +1,38 @@ +version: "3.9" +services: + firefox: + image: selenium/standalone-firefox:${FIREFOX_VERSION:-latest} + shm_size: 2g + ports: + - 4444 + - 7900 + chromium: + # WARN: `standalone-chrome` does NOT work on ARM-based machines; + # see https://github.com/SeleniumHQ/docker-selenium#experimental-mult-arch-aarch64armhfamd64-images; + # try using `seleniarm/standalone-chromium` instead + # export CHROMIUM_IMAGE=seleniarm/standalone-chromium + image: ${CHROMIUM_IMAGE:-selenium/standalone-chrome}:${CHROMIUM_VERSION:-latest} + shm_size: 2g + ports: + - 4444 + - 7900 + kubo: + image: ipfs/kubo:${KUBO_VERSION:-latest} + ports: + - 4001 + - 5001 + - 8080 + volumes: + - ./ci/access-control-allow-all.sh:/container-init.d/001-access-control-allow-all.sh + e2e: + build: + dockerfile: ./Dockerfile + environment: + - SELENIUM_REMOTE_CHROMIUM_URL=http://chromium:4444 + - SELENIUM_REMOTE_FIREFOX_URL=http://firefox:4444 + - IPFS_API_URL=http://kubo:5001 + - CUSTOM_GATEWAY_URL=http://kubo:8080 + - IPFS_COMPANION_VERSION=${IPFS_COMPANION_VERSION} + - TEST_E2E=1 + volumes: + - ./build:/home/node/app/build diff --git a/package-lock.json b/package-lock.json index 169c5660d..0b9457ad3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,7 @@ "path": "0.12.7", "raw-loader": "4.0.2", "request-progress": "3.0.0", + "selenium-webdriver": "^4.7.1", "shx": "0.3.4", "sinon": "13.0.1", "sinon-chrome": "3.0.1", @@ -18260,6 +18261,41 @@ "seek-table": "bin/seek-bzip-table" } }, + "node_modules/selenium-webdriver": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.7.1.tgz", + "integrity": "sha512-IfTM9OE8HtCKjOJwyudbAVtAHQKOJK8mu2qrXXbKyj4lqgXF+2lYW4rSZXCV6SLQRWZ+DVGkomCmFzq5orD/ZA==", + "dev": true, + "dependencies": { + "jszip": "^3.10.0", + "tmp": "^0.2.1", + "ws": ">=8.7.0" + }, + "engines": { + "node": ">= 14.20.0" + } + }, + "node_modules/selenium-webdriver/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -35430,6 +35466,26 @@ "commander": "^2.8.1" } }, + "selenium-webdriver": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.7.1.tgz", + "integrity": "sha512-IfTM9OE8HtCKjOJwyudbAVtAHQKOJK8mu2qrXXbKyj4lqgXF+2lYW4rSZXCV6SLQRWZ+DVGkomCmFzq5orD/ZA==", + "dev": true, + "requires": { + "jszip": "^3.10.0", + "tmp": "^0.2.1", + "ws": ">=8.7.0" + }, + "dependencies": { + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "requires": {} + } + } + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", diff --git a/package.json b/package.json index 1925c0413..edd4d2cac 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "watch:js": "run-p watch:js:*", "watch:js:webpack": "webpack --watch --mode development --devtool inline-source-map --config ./webpack.config.js", "test": "run-s test:*", + "test:e2e": "mocha --timeout 300000 --exit --require ignore-styles \"test/e2e/**/*.test.js\"", "test:functional": "c8 mocha --timeout 5000 --exit --require ignore-styles \"test/functional/**/*.test.js\"", "lint": "run-s lint:*", "lint:standard": "standard -v \"*.js\" \"add-on/src/**/*.js\" \"test/**/*.js\" \"scripts/**/*.js\"", @@ -59,7 +60,11 @@ "beta-build": "docker rmi -f ipfs-companion-beta-build && docker build -t ipfs-companion-beta-build --build-arg USER_ID=$(id -u ${USER}) --build-arg GROUP_ID=$(id -g ${USER}) . && mkdir -p build && docker run --rm -it --net=host -e RELEASE_CHANNEL=beta -v $(pwd)/build:/home/node/app/build ipfs-companion-beta-build npm run ci:build", "release-build": "docker rmi -f ipfs-companion-release-build && docker build -t ipfs-companion-release-build --build-arg USER_ID=$(id -u ${USER}) --build-arg GROUP_ID=$(id -g ${USER}) . && mkdir -p build && docker run --rm -it --net=host -e RELEASE_CHANNEL=stable -v $(pwd)/build:/home/node/app/build ipfs-companion-release-build npm run ci:build", "dev-build": "npm ci && npm run build", - "yarn-build": "npm run dev-build" + "yarn-build": "npm run dev-build", + "compose:e2e:build": "docker compose --file docker-compose.e2e.yml build", + "compose:e2e:up": "docker compose --file docker-compose.e2e.yml up --remove-orphans --detach kubo chromium firefox", + "compose:e2e:test": "docker compose --file docker-compose.e2e.yml run e2e npm run test:e2e", + "compose:e2e:stop": "docker compose --file docker-compose.e2e.yml stop" }, "private": true, "preferGlobal": false, @@ -101,6 +106,7 @@ "path": "0.12.7", "raw-loader": "4.0.2", "request-progress": "3.0.0", + "selenium-webdriver": "^4.7.1", "shx": "0.3.4", "sinon": "13.0.1", "sinon-chrome": "3.0.1", diff --git a/test/e2e/ipfs-companion.test.js b/test/e2e/ipfs-companion.test.js new file mode 100644 index 000000000..b0a8f7556 --- /dev/null +++ b/test/e2e/ipfs-companion.test.js @@ -0,0 +1,152 @@ +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 manifest from '../../add-on/manifest.common.json' assert { type: 'json' } + +async function delay(ms) { + return new Promise(res => setTimeout(res, ms)) +} + +function getVersion() { + return process.env.IPFS_COMPANION_VERSION || manifest.version +} + +async function openChromiumBrowser() { + console.info('Opening Chromium browser') + const extension = `build/ipfs_companion-${getVersion()}_chromium.zip` + console.info(`Checking if ${extension} exists`) + expect(fs.existsSync(extension)).to.be.true + const options = new chrome.Options() + options.addExtensions(extension) + options.addArguments('--lang=en-GB,en-US') + options.addArguments('--headless=chrome') + var 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() + await delay(5000) // waiting for the browser/extension to load + console.info('Chromium is ready') + return browser +} + +async function openFirefoxBrowser() { + console.info('Opening Firefox browser') + const extension = `build/ipfs_companion-${getVersion()}_firefox.zip` + console.info(`Checking if ${extension} exists`) + expect(fs.existsSync(extension)).to.be.true + const options = new firefox.Options() + options.setPreference('intl.accept_languages', 'en-gb,en-us') + options.setPreference('intl.locale.requested', 'en-GB,en-US') + // 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 = builder.build() + console.info('Installing the extension') + await browser.installAddon(extension, true) + await delay(5000) // waiting for the browser/extension to load + console.info('Firefox is ready') + return browser +} + +const ExtensionURLRegex = /^(moz|chrome)-extension:\/\/[^\/]+/ +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.error('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}`) + var element = browser.findElement(By.id(id)) + console.info(`Setting new value to: ${value}`) + await element.sendKeys('') + await delay(1000) // waiting for focus to be acquired + await element.clear() + await delay(1000) // waiting for input to be cleared + await element.sendKeys(value) + await element.sendKeys(Key.TAB) + await delay(5000) // waiting for the setting to be applied + console.info('Checking if the update worked') + element = browser.findElement(By.id(id)) + const v = await element.getAttribute('value') + expect(v).to.equal(value) + 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`) + await delay(5000) // waiting for the connection number to appear + const html = await browser.getPageSource() + console.debug(html) + console.info(`Looking for an element with text: 'Your node is connected to ...'`) + const p = browser.findElement(By.xpath("//p[text()='Your node is connected to ']")) + const span = p.findElement(By.css('span')) + const peers = await span.getText() + console.info(`There are ${peers} connected peers`) + return parseInt(peers) +} + +async function runTest(browser) { + const url = await findExtensionUrl(browser) + expect(url).not.to.be.undefined + await updateExtensionSettings(browser, url, 'ipfsApiUrl', process.env.IPFS_API_URL || 'http://127.0.0.1:5001') + await updateExtensionSettings(browser, url, 'customGatewayUrl', process.env.CUSTOM_GATEWAY_URL || 'http://localhost:8080') + const peers = await getNumberOfConnectedPeers(browser, url) + expect(peers).not.to.equal(0) +} + +describe('ipfs-companion', function () { + before(() => { + if (process.env.TEST_E2E !== '1') { + this.skip() + } + }) + it('should be able to discover peers in Chromium', async function () { + const browser = await openChromiumBrowser() + try { + await runTest(browser) + } finally { + await browser.quit() + } + }) + it('should be able to discover peers in Firefox', async function () { + const browser = await openFirefoxBrowser() + try { + await runTest(browser) + } finally { + await browser.quit() + } + }) +})