From c32b3037446d90dba27aa1868dddf397de4e62df Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Wed, 10 May 2023 13:52:51 +0200 Subject: [PATCH] Add CLI tooling to create seed fixtures on the fly Signed-off-by: Sebastian Martinez --- .github/workflows/check-e2e.yml | 5 +- .../workflows/check-httpd-api-unit-test.yml | 8 +- .github/workflows/check-visual.yml | 5 +- httpd-client/tests/project.test.ts | 50 +- httpd-client/vite.config.ts | 1 + package-lock.json | 558 ++++++++++++++---- package.json | 7 +- scripts/create-seed-fixture | 120 ---- scripts/install-binaries | 90 +++ scripts/run-httpd-with-fixtures | 120 ---- tests/e2e/clipboard.spec.ts | 9 +- tests/e2e/project.spec.ts | 5 +- tests/e2e/project/commit.spec.ts | 5 +- tests/e2e/project/commits.spec.ts | 4 +- tests/fixtures/seeds/palm.tar.bz2 | Bin 28957 -> 0 bytes tests/support/fixtures.ts | 144 +++-- tests/support/globalSetup.ts | 99 +++- tests/support/heartwood-version | 1 + tests/support/peerManager.ts | 217 +++++++ tests/support/process.ts | 83 +++ 20 files changed, 1073 insertions(+), 458 deletions(-) delete mode 100755 scripts/create-seed-fixture create mode 100755 scripts/install-binaries delete mode 100755 scripts/run-httpd-with-fixtures delete mode 100644 tests/fixtures/seeds/palm.tar.bz2 create mode 100644 tests/support/heartwood-version create mode 100644 tests/support/peerManager.ts create mode 100644 tests/support/process.ts diff --git a/.github/workflows/check-e2e.yml b/.github/workflows/check-e2e.yml index 61eefc587..3518dbc29 100644 --- a/.github/workflows/check-e2e.yml +++ b/.github/workflows/check-e2e.yml @@ -36,10 +36,11 @@ jobs: if: steps.playwright-dep-cache.outputs.cache-hit != 'true' run: npx playwright install chromium firefox - - name: Start http-api test server + - name: Install Radicle binaries run: | mkdir -p tests/artifacts; - ./scripts/run-httpd-with-fixtures --non-interactive --download 2>&1 | tee tests/artifacts/httpd-${{ matrix.browser }}.log & + ./scripts/install-binaries; + ./scripts/install-binaries --show-path >> $GITHUB_PATH; - name: Run Playwright tests run: | diff --git a/.github/workflows/check-httpd-api-unit-test.yml b/.github/workflows/check-httpd-api-unit-test.yml index 2dbabc328..a270df7f1 100644 --- a/.github/workflows/check-httpd-api-unit-test.yml +++ b/.github/workflows/check-httpd-api-unit-test.yml @@ -18,8 +18,10 @@ jobs: - name: Checkout uses: actions/checkout@v3 - run: npm ci - - name: Start http-api test server + - name: Install Radicle binaries run: | mkdir -p tests/artifacts; - ./scripts/run-httpd-with-fixtures --non-interactive --download 2>&1 | tee tests/artifacts/httpd-api.log & - - run: npm run test:httpd-api:unit + ./scripts/install-binaries; + ./scripts/install-binaries --show-path >> $GITHUB_PATH; + - run: | + npm run test:httpd-api:unit diff --git a/.github/workflows/check-visual.yml b/.github/workflows/check-visual.yml index 8743b42a1..5ec0ba86c 100644 --- a/.github/workflows/check-visual.yml +++ b/.github/workflows/check-visual.yml @@ -54,10 +54,11 @@ jobs: if: steps.playwright-dep-cache.outputs.cache-hit != 'true' run: npx playwright install chromium firefox - - name: Start http-api test server + - name: Install Radicle binaries run: | mkdir -p tests/artifacts; - ./scripts/run-httpd-with-fixtures --non-interactive --download 2>&1 | tee tests/artifacts/httpd-visual.log & + ./scripts/install-binaries; + ./scripts/install-binaries --show-path >> $GITHUB_PATH; - name: Run Playwright tests run: | diff --git a/httpd-client/tests/project.test.ts b/httpd-client/tests/project.test.ts index d36e7920e..5b1445740 100644 --- a/httpd-client/tests/project.test.ts +++ b/httpd-client/tests/project.test.ts @@ -1,5 +1,7 @@ import { describe, test } from "vitest"; + import { HttpdClient } from "../index"; +import { aliceMainHead, aliceRemote, rid } from "@tests/support/fixtures"; const api = new HttpdClient({ hostname: "127.0.0.1", @@ -9,9 +11,7 @@ const api = new HttpdClient({ describe("project", () => { test("#getByDelegate(delegateId)", async () => { - await api.project.getByDelegate( - "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi", - ); + await api.project.getByDelegate(aliceRemote); }); test("#getAll()", async () => { @@ -19,62 +19,45 @@ describe("project", () => { }); test("#getById(id)", async () => { - await api.project.getById("rad:zKtT7DmF9H34KkvcKj9PHW19WzjT"); + await api.project.getById(rid); }); test("#getActivity(id)", async () => { - await api.project.getActivity("rad:zKtT7DmF9H34KkvcKj9PHW19WzjT"); + await api.project.getActivity(rid); }); test("#getReadme(id, sha)", async () => { - await api.project.getReadme( - "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", - "fcc929424b82984b7cbff9c01d2e20d9b1249842", - ); + await api.project.getReadme(rid, aliceMainHead); }); test("#getBlob(id, sha, path)", async () => { - await api.project.getBlob( - "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", - "dd068e9aff9a569e597f6abaf84f120dd0cbbd70", - "src/true.c", - ); + await api.project.getBlob(rid, aliceMainHead, "src/true.c"); }); test("#getTree(id, sha)", async () => { - await api.project.getTree( - "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", - "dd068e9aff9a569e597f6abaf84f120dd0cbbd70", - ); + await api.project.getTree(rid, aliceMainHead); }); test("#getTree(id, sha, path)", async () => { - await api.project.getTree( - "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", - "dd068e9aff9a569e597f6abaf84f120dd0cbbd70", - "src", - ); + await api.project.getTree(rid, aliceMainHead, "src"); }); test("#getAllRemotes(id)", async () => { - await api.project.getAllRemotes("rad:zKtT7DmF9H34KkvcKj9PHW19WzjT"); + await api.project.getAllRemotes(rid); }); test("#getRemoteByPeer(id, peer)", async () => { - await api.project.getRemoteByPeer( - "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", - "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi", - ); + await api.project.getRemoteByPeer(rid, aliceRemote.substring(8)); }); test("#getAllCommits(id)", async () => { - await api.project.getAllCommits("rad:zKtT7DmF9H34KkvcKj9PHW19WzjT"); + await api.project.getAllCommits(rid); }); // TODO: test since/until properly. test("#getAllCommits(id, {parent, since, until, page, perPage})", async () => { - await api.project.getAllCommits("rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", { - parent: "f0b8db68847b01f0964380507a9db6800e5b5342", + await api.project.getAllCommits(rid, { + parent: aliceMainHead, since: 1679065819581, until: 1679065819590, page: 1, @@ -83,10 +66,7 @@ describe("project", () => { }); test("#getCommitBySha(id, sha)", async () => { - await api.project.getCommitBySha( - "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", - "fcc929424b82984b7cbff9c01d2e20d9b1249842", - ); + await api.project.getCommitBySha(rid, aliceMainHead); }); test.todo("#getDiff(id, revisionBase, revisionOid)"); diff --git a/httpd-client/vite.config.ts b/httpd-client/vite.config.ts index 3e2cdd370..3b48411a3 100644 --- a/httpd-client/vite.config.ts +++ b/httpd-client/vite.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ resolve: { alias: { "@tests": path.resolve("./tests"), + "@app": path.resolve("./src"), }, }, }); diff --git a/package-lock.json b/package-lock.json index 61ca1ee4c..98562d6b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,17 +39,22 @@ "@types/node": "^18.16.2", "@types/sinon": "^10.0.14", "@types/sinonjs__fake-timers": "^8.1.2", + "@types/wait-on": "^5.3.1", "@typescript-eslint/eslint-plugin": "^5.59.2", "chalk": "^5.2.0", "eslint": "^8.39.0", "eslint-plugin-svelte3": "^4.0.0", + "execa": "^7.1.1", + "exit-hook": "^3.2.0", + "get-port": "^6.1.2", "happy-dom": "^9.10.9", "prettier": "^2.8.8", "prettier-plugin-svelte": "^2.10.0", "svelte-check": "^3.3.0", "typescript": "^5.0.4", "vite": "^4.3.4", - "vitest": "^0.31.0" + "vitest": "^0.31.0", + "wait-on": "^7.0.1" }, "engines": { "node": ">=18.15.0" @@ -432,14 +437,14 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.1", + "espree": "^9.5.2", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -455,14 +460,29 @@ } }, "node_modules/@eslint/js": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", - "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", + "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -595,6 +615,27 @@ "node": ">=6.0" } }, + "node_modules/@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true + }, "node_modules/@sinonjs/commons": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", @@ -716,9 +757,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.16.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.4.tgz", - "integrity": "sha512-LUhvPmAKAbgm+p/K11IWszLZVoZDlMF4NRmqbhEzDz/CnCuehPkZXwZbBCKGJsgjnuVejotBwM7B3Scrq4EqDw==", + "version": "18.16.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.8.tgz", + "integrity": "sha512-p0iAXcfWCOTCBbsExHIDFCfwsqFwBTgETJveKMT+Ci3LY9YqQCI91F5S+TB20+aRCXpcWfvx5Qr5EccnwCm2NA==", "dev": true }, "node_modules/@types/parse5": { @@ -733,9 +774,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", "dev": true }, "node_modules/@types/sinon": { @@ -764,16 +805,25 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" }, + "node_modules/@types/wait-on": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@types/wait-on/-/wait-on-5.3.1.tgz", + "integrity": "sha512-2FFOKCF/YydrMUaqg+fkk49qf0e5rDgwt6aQsMzFQzbS419h2gNOXyiwp/o2yYy27bi/C1z+HgfncryjGzlvgQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz", - "integrity": "sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.5.tgz", + "integrity": "sha512-feA9xbVRWJZor+AnLNAr7A8JRWeZqHUf4T9tlP+TN04b05pFVhO5eN7/O93Y/1OUlLMHKbnJisgDURs/qvtqdg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/type-utils": "5.59.2", - "@typescript-eslint/utils": "5.59.2", + "@typescript-eslint/scope-manager": "5.59.5", + "@typescript-eslint/type-utils": "5.59.5", + "@typescript-eslint/utils": "5.59.5", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -799,15 +849,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.2.tgz", - "integrity": "sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.5.tgz", + "integrity": "sha512-NJXQC4MRnF9N9yWqQE2/KLRSOLvrrlZb48NGVfBa+RuPMN6B7ZcK5jZOvhuygv4D64fRKnZI4L4p8+M+rfeQuw==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/typescript-estree": "5.59.2", + "@typescript-eslint/scope-manager": "5.59.5", + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/typescript-estree": "5.59.5", "debug": "^4.3.4" }, "engines": { @@ -827,13 +877,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", - "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.5.tgz", + "integrity": "sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/visitor-keys": "5.59.2" + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/visitor-keys": "5.59.5" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -844,13 +894,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz", - "integrity": "sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.5.tgz", + "integrity": "sha512-4eyhS7oGym67/pSxA2mmNq7X164oqDYNnZCUayBwJZIRVvKpBCMBzFnFxjeoDeShjtO6RQBHBuwybuX3POnDqg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.2", - "@typescript-eslint/utils": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.5", + "@typescript-eslint/utils": "5.59.5", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -871,9 +921,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", - "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.5.tgz", + "integrity": "sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -884,13 +934,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", - "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz", + "integrity": "sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/visitor-keys": "5.59.2", + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/visitor-keys": "5.59.5", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -911,17 +961,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", - "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.5.tgz", + "integrity": "sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/typescript-estree": "5.59.2", + "@typescript-eslint/scope-manager": "5.59.5", + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/typescript-estree": "5.59.5", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -937,12 +987,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", - "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.5.tgz", + "integrity": "sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/types": "5.59.5", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -1167,6 +1217,22 @@ "node": "*" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, "node_modules/baconjs": { "version": "3.0.17", "resolved": "https://registry.npmjs.org/baconjs/-/baconjs-3.0.17.tgz", @@ -1428,6 +1494,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -1554,6 +1632,15 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -1596,9 +1683,21 @@ } }, "node_modules/dompurify": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.2.tgz", - "integrity": "sha512-B8c6JdiEpxAKnd8Dm++QQxJL4lfuc757scZtcapj6qjTjrQzyq5iAyznLKVvK+77eYNiFblHBlt7MM0fOeqoKw==" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz", + "integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ==" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, "node_modules/es6-promise": { "version": "3.3.1", @@ -1656,15 +1755,15 @@ } }, "node_modules/eslint": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", - "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", + "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.39.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.40.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -1675,8 +1774,8 @@ "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.0", - "espree": "^9.5.1", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -1736,9 +1835,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1804,14 +1903,14 @@ } }, "node_modules/espree": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", - "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", "dev": true, "dependencies": { "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.0" + "eslint-visitor-keys": "^3.4.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1880,6 +1979,41 @@ "node": ">=0.10.0" } }, + "node_modules/execa": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", + "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit-hook": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-3.2.0.tgz", + "integrity": "sha512-aIQN7Q04HGAV/I5BszisuHTZHXNoC23WtLkxdCLuYZMdWviRD0TMIt2bnUBi9MrHaF/hH8b3gwG9iaAUHKnJGA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", @@ -2011,6 +2145,40 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -2061,6 +2229,30 @@ "node": "*" } }, + "node_modules/get-port": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-6.1.2.tgz", + "integrity": "sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -2140,13 +2332,13 @@ "dev": true }, "node_modules/happy-dom": { - "version": "9.10.9", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-9.10.9.tgz", - "integrity": "sha512-3RnOyu6buPMpDAyOpp8yfR5Xi/k2p5MhrDwlG/dgpVHkptFN5IqubdbGOQU5luB7ANh6a08GOuiB+Bo9JCzCBw==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-9.16.0.tgz", + "integrity": "sha512-goq7grRjIiV2Svb251LWQOo/xm04za2mJ9+assbZJx1KnaVOX1gZBBp4MHbiFNkR6JW7UL81iCtZxCVu+qU5ng==", "dev": true, "dependencies": { "css.escape": "^1.5.1", - "he": "^1.2.0", + "entities": "^4.5.0", "iconv-lite": "^0.6.3", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^2.0.0", @@ -2290,15 +2482,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "bin": { - "he": "bin/he" - } - }, "node_modules/html-void-elements": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", @@ -2308,6 +2491,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -2462,6 +2654,18 @@ "node": ">=8" } }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -2473,6 +2677,19 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/joi": { + "version": "17.9.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.9.2.tgz", + "integrity": "sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-sdsl": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", @@ -2659,9 +2876,9 @@ } }, "node_modules/marked": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/marked/-/marked-5.0.1.tgz", - "integrity": "sha512-Nn9peC4lvIZdcfp8Uze6xk4ZYowkcj/K6+e/6rLHadhtjqeip/bYRxMgt3124IGGJ3RPs2uX5YVmAGbUutY18g==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-5.0.2.tgz", + "integrity": "sha512-TXksm9GwqXCRNbFUZmMtqNLvy3K2cQHuWmyBDLOrY1e6i9UvZpOTJXoz7fBjYkJkaUFzV9hBFxMuZSyQt8R6KQ==", "bin": { "marked": "bin/marked.js" }, @@ -2691,6 +2908,12 @@ "node": ">=8" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2713,6 +2936,39 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -2756,15 +3012,15 @@ } }, "node_modules/mlly": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.2.0.tgz", - "integrity": "sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.2.1.tgz", + "integrity": "sha512-1aMEByaWgBPEbWV2BOPEMySRrzl7rIHXmQxam4DM8jVjalTQDjpN2ZKOLUrwyhfZQO7IXHml2StcHMhooDeEEQ==", "dev": true, "dependencies": { "acorn": "^8.8.2", "pathe": "^1.1.0", - "pkg-types": "^1.0.2", - "ufo": "^1.1.1" + "pkg-types": "^1.0.3", + "ufo": "^1.1.2" } }, "node_modules/mri": { @@ -2833,6 +3089,33 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2842,6 +3125,21 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -3181,9 +3479,9 @@ } }, "node_modules/rollup": { - "version": "3.21.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.5.tgz", - "integrity": "sha512-a4NTKS4u9PusbUJcfF4IMxuqjFzjm6ifj76P54a7cKnvVzJaG12BLVR+hgU2YDGHzyMMQNxLAZWuALsn8q2oQg==", + "version": "3.21.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.6.tgz", + "integrity": "sha512-SXIICxvxQxR3D4dp/3LDHZIJPC8a4anKMHd4E3Jiz2/JnY+2bEjqrOokAauc5ShGVNFHlEFjBXAXlaxkJqIqSg==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -3219,6 +3517,21 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -3315,6 +3628,12 @@ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/sinon": { "version": "15.0.4", "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.0.4.tgz", @@ -3427,6 +3746,18 @@ "node": ">=0.10.0" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -3475,17 +3806,17 @@ } }, "node_modules/svelte": { - "version": "3.58.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.58.0.tgz", - "integrity": "sha512-brIBNNB76mXFmU/Kerm4wFnkskBbluBDCjx/8TcpYRb298Yh2dztS2kQ6bhtjMcvUhd5ynClfwpz5h2gnzdQ1A==", + "version": "3.59.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.1.tgz", + "integrity": "sha512-pKj8fEBmqf6mq3/NfrB9SLtcJcUvjYSWyePlfCqN9gujLB25RitWK8PvFzlwim6hD/We35KbPlRteuA6rnPGcQ==", "engines": { "node": ">= 8" } }, "node_modules/svelte-check": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.3.0.tgz", - "integrity": "sha512-wZtOvY8V2fjzCbS4dGjDp0Ebh6VyXg6A39s7TDc8wc0154yqWKu18Rd9Ad+GOs7sXst7dbTmjvOaexjwoqPM7A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.3.2.tgz", + "integrity": "sha512-67j3rI0LDc2DvL0ON/2pvCasVVD3nHDrTkZNr4eITNfo2oFXdw7SIyMOiFj4swu+pjmFQAigytBK1IWyik8dBw==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", @@ -3873,9 +4204,9 @@ } }, "node_modules/vite": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.4.tgz", - "integrity": "sha512-f90aqGBoxSFxWph2b39ae2uHAxm5jFBBdnfueNxZAT1FTpM13ccFQExCaKbR2xFW5atowjleRniQ7onjJ22QEg==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz", + "integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==", "dev": true, "dependencies": { "esbuild": "^0.17.5", @@ -4045,6 +4376,25 @@ "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.0.0.tgz", "integrity": "sha512-Cl65diFGxz7gpwbav10HqiY/eVYTO1sjQpmRmV991Bj7wAoOAjGQ97PpQcXorDE2Uc4hnGWLY17xme+5t6MlSg==" }, + "node_modules/wait-on": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.0.1.tgz", + "integrity": "sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog==", + "dev": true, + "dependencies": { + "axios": "^0.27.2", + "joi": "^17.7.0", + "lodash": "^4.17.21", + "minimist": "^1.2.7", + "rxjs": "^7.8.0" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/package.json b/package.json index 4f571f55f..75efcaaf5 100644 --- a/package.json +++ b/package.json @@ -32,16 +32,21 @@ "@types/sinon": "^10.0.14", "@types/sinonjs__fake-timers": "^8.1.2", "@typescript-eslint/eslint-plugin": "^5.59.2", + "@types/wait-on": "^5.3.1", "chalk": "^5.2.0", "eslint": "^8.39.0", "eslint-plugin-svelte3": "^4.0.0", "happy-dom": "^9.10.9", "prettier": "^2.8.8", + "execa": "^7.1.1", + "exit-hook": "^3.2.0", + "get-port": "^6.1.2", "prettier-plugin-svelte": "^2.10.0", "svelte-check": "^3.3.0", "typescript": "^5.0.4", "vite": "^4.3.4", - "vitest": "^0.31.0" + "vitest": "^0.31.0", + "wait-on": "^7.0.1" }, "dependencies": { "@radicle/gray-matter": "4.1.0", diff --git a/scripts/create-seed-fixture b/scripts/create-seed-fixture deleted file mode 100755 index 7a28f07cb..000000000 --- a/scripts/create-seed-fixture +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env bash - -killall radicle-node -killall git-daemon - -set -euo pipefail - -export RAD_PASSPHRASE=asdf - -REPO_ROOT=$(git rev-parse --show-toplevel) -ID=$(echo $RANDOM | md5sum | head -c 4) -BASE_PATH=$REPO_ROOT/tests/tmp/seed-fixture-$ID - -TEST_REPO_ARCHIVE=$REPO_ROOT/tests/fixtures/repos/source-browsing.tar.bz2 -TEST_REPO_NAME=source-browsing -TEST_REPO_PATH=$BASE_PATH/repos/$TEST_REPO_NAME - -PALM_RAD_HOME=$BASE_PATH/seeds/palm -PALM_CHECKOUT=$BASE_PATH/checkout/palm -ALICE_RAD_HOME=$BASE_PATH/peers/alice -ALICE_CHECKOUT=$BASE_PATH/checkout/alice -BOB_RAD_HOME=$BASE_PATH/peers/bob -BOB_CHECKOUT=$BASE_PATH/checkout/bob - -mkdir -p $PALM_RAD_HOME -mkdir -p $PALM_CHECKOUT -mkdir -p $ALICE_RAD_HOME -mkdir -p $ALICE_CHECKOUT -mkdir -p $BOB_RAD_HOME -mkdir -p $BOB_CHECKOUT -mkdir -p $TEST_REPO_PATH - -tar -xf $TEST_REPO_ARCHIVE -C $TEST_REPO_PATH - -### PALM NODE ### - -eval $(ssh-agent) - -export RAD_HOME=$PALM_RAD_HOME -export RAD_SEED=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee - -rad auth -radicle-node --listen 0.0.0.0:3446 --git-daemon 0.0.0.0:4446 --tracking-policy track --tracking-scope all & - -### ALICE ### - -export RAD_HOME=$ALICE_RAD_HOME -export RAD_SEED=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - -export GIT_AUTHOR_NAME="Alice Liddell" -export GIT_AUTHOR_EMAIL="alice@radicle.xyz" -export GIT_COMMITTER_NAME="Alice Liddell" -export GIT_COMMITTER_EMAIL="alice@radicle.xyz" - -rad auth - -cd $ALICE_CHECKOUT - -git clone $TEST_REPO_PATH -cd $TEST_REPO_NAME - -git checkout main -rad init --name "source-browsing" \ - --description "Git repository for source browsing tests" \ - --default-branch "main" \ - --no-confirm - -git checkout feature/branch -git push rad - -git checkout orphaned-branch -git push rad - -radicle-node --listen 0.0.0.0:3444 --git-daemon 0.0.0.0:4444 \ - --connect z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8@0.0.0.0:3446 & - -sleep 1 - -### PALM CLONE ### - -cd $PALM_CHECKOUT -export RAD_HOME=$PALM_RAD_HOME -rad clone rad:zKtT7DmF9H34KkvcKj9PHW19WzjT - -### BOB ### - -export GIT_AUTHOR_NAME="Bob Belcher" -export GIT_AUTHOR_EMAIL="bob@radicle.xyz" -export GIT_COMMITTER_NAME="Bob Belcher" -export GIT_COMMITTER_EMAIL="bob@radicle.xyz" -export GIT_COMMITTER_DATE="Mon Dec 21 14:00 2022 +0100" - -export RAD_HOME=$BOB_RAD_HOME -export RAD_SEED=fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe - -rad auth -rad auth -radicle-node --listen 0.0.0.0:3445 --git-daemon 0.0.0.0:4445 \ - --connect z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8@0.0.0.0:3446 & - -sleep 2 - -cd $BOB_CHECKOUT -rad clone rad:zKtT7DmF9H34KkvcKj9PHW19WzjT - -cd $TEST_REPO_NAME -echo "Updated readme" > README.md -git add README.md -git commit --message "Update readme" --date "$GIT_COMMITTER_DATE" -git push rad - -### WAIT FOR SYNC WITH PALM ### - -sleep 2 - -cd $BASE_PATH -tar -cjf palm.tar.bz2 --exclude "post-receive" --exclude "pre-receive" -C $PALM_RAD_HOME . - -killall radicle-node -killall git-daemon diff --git a/scripts/install-binaries b/scripts/install-binaries new file mode 100755 index 000000000..7cdfafb2c --- /dev/null +++ b/scripts/install-binaries @@ -0,0 +1,90 @@ +#!/bin/bash +set -e + +BINARIES=(rad radicle-node radicle-httpd git-remote-rad) +REPO_ROOT=$(git rev-parse --show-toplevel) +REV=$(cat "$REPO_ROOT/tests/support/heartwood-version") +BINARY_PATH=$REPO_ROOT/tests/tmp/bin/${REV:0:7} +OS=$(uname) + +show_usage() { + echo + echo "Installs binaries required for running e2e test suite." + echo + echo "USAGE:" + echo " install-binaries [-s|h]" + echo + echo "OPTIONS:" + echo " -s --show-path Print the binary path, and skip installation." + echo " -h --help Print this Help." + echo +} + +while [ $# -ne 0 ]; do + case "$1" in + --show-path | -s) + echo "$BINARY_PATH" + exit + ;; + --help | -h) + show_usage + exit + ;; + esac + +done + + +echo +echo "Using revision $REV" +echo + +mkdir -p "$BINARY_PATH" + +for BINARY_NAME in "${BINARIES[@]}" +do + if [ -x "$(command -v "$BINARY_PATH/$BINARY_NAME")" ]; then + echo ✅ "$BINARY_NAME" + else + # To provide deterministic Node and Repo IDs, we need a rad CLI compiled with the --debug flag. + if [ "$BINARY_NAME" = "rad" ]; then + DOWNLOAD_NAME=rad-debug + else + DOWNLOAD_NAME=$BINARY_NAME + fi + + case "$OS" in + Darwin) + echo Downloading "$BINARY_NAME" from "https://files.radicle.xyz/$REV/aarch64-apple-darwin/$DOWNLOAD_NAME" + curl --fail -s "https://files.radicle.xyz/$REV/aarch64-apple-darwin/$DOWNLOAD_NAME" --output "$BINARY_PATH/$BINARY_NAME" || (echo "Download failed" && exit 1);; + Linux) + echo Downloading "$BINARY_NAME" from "https://files.radicle.xyz/$REV/x86_64-unknown-linux-musl/$DOWNLOAD_NAME" + curl --fail -s "https://files.radicle.xyz/$REV/x86_64-unknown-linux-musl/$DOWNLOAD_NAME" --output "$BINARY_PATH/$BINARY_NAME" || (echo "Download failed" && exit 1);; + *) echo "There are no precompiled binaries for your OS: $OS, compile $BINARY_NAME manually and make sure it's in PATH." && exit 1 ;; + esac + + chmod a+x "$BINARY_PATH/$BINARY_NAME" + fi +done + +RADICLE_HOME=${RAD_HOME:-"${HOME}/.radicle/bin"} + +# Add a separator between binaries download and PATH instructions. +echo + +if [[ ":$PATH:" != *":$BINARY_PATH:"* ]]; then + if [[ ":$PATH:" == *":$RADICLE_HOME:"* ]]; then + echo "⚠️ You already have a radicle bin folder in your PATH variable." + echo + echo "To ensure you are using the correct test binaries, run the following command:" + else + echo "Before running the tests, make sure your PATH variable is set up correctly by running:" + fi + + echo " 👉 export PATH=$BINARY_PATH:\$PATH" + +else + echo "✅ $BINARY_PATH is already in your PATH variable." +fi + + diff --git a/scripts/run-httpd-with-fixtures b/scripts/run-httpd-with-fixtures deleted file mode 100755 index 25022c16d..000000000 --- a/scripts/run-httpd-with-fixtures +++ /dev/null @@ -1,120 +0,0 @@ -#!/bin/bash -set -e - -REV=b29321dbf7999ec7ec9b7ac9192071e512ada407 - -REPO_ROOT=$(git rev-parse --show-toplevel) -FIXTURE=$REPO_ROOT/tests/fixtures/seeds/palm.tar.bz2 -WORKSPACE=$REPO_ROOT/tests/tmp/palm -PASSPHRASE=asdf -BINARY_PATH="$REPO_ROOT/tests/tmp" -BINARY_NAME=radicle-httpd -OS=$(uname) - -show_usage() { - echo - echo "Starts a ${BINARY_NAME} backend with test fixtures." - echo - echo "USAGE:" - echo " run-httpd-with-fixtures [-d|h|n]" - echo - echo "OPTIONS:" - echo " -d --download Download and use a precompiled binary." - echo " -h --help Print this Help." - echo " -n --non-interactive Run in non-interactive mode, no user prompts." - echo -} - -prompt_workspace_removal() { - echo "This will irrevocably destroy the following directories:" - echo - echo $WORKSPACE - echo - - read -r -p "Are you sure you want to continue? [yes/no]: " confirm - case "$confirm" in - [yY][eE][sS]) - rm -rf $WORKSPACE - echo "Done" - ;; - *) - echo "Ok, I won't touch your data." - exit - ;; - esac -} - -prepare_workspace() { - echo - echo "Unpacking fixture $FIXTURE" - mkdir -p $WORKSPACE - tar -xf $FIXTURE -C $WORKSPACE -} - -NON_INTERACTIVE=false -DOWNLOAD=false - -while [ $# -ne 0 ]; do - case "$1" in - --download | -d) - DOWNLOAD=true - ;; - --non-interactive | -n) - NON_INTERACTIVE=true - ;; - *) - show_usage - exit - ;; - esac - - shift -done - -if [ "$DOWNLOAD" = true ]; then - CACHED_BINARY_NAME="$BINARY_NAME-${REV:0:7}" - - if ! [ -x "$(command -v $BINARY_PATH/$CACHED_BINARY_NAME)" ]; then - case "$OS" in - Darwin) - echo Downloading $BINARY_NAME from https://files.radicle.xyz/$REV/aarch64-apple-darwin/$BINARY_NAME - curl --fail -s "https://files.radicle.xyz/$REV/aarch64-apple-darwin/$BINARY_NAME" --output "$BINARY_PATH/$CACHED_BINARY_NAME" || (echo "Download failed" && exit 1);; - Linux) - echo Downloading $BINARY_NAME from https://files.radicle.xyz/$REV/x86_64-unknown-linux-musl/$BINARY_NAME - curl --fail -s "https://files.radicle.xyz/$REV/x86_64-unknown-linux-musl/$BINARY_NAME" --output "$BINARY_PATH/$CACHED_BINARY_NAME" || (echo "Download failed" && exit 1);; - *) echo "There are no precompiled binaries for your OS: $OS, compile $BINARY_NAME manually and make sure it's in PATH." && exit 1 ;; - esac - - chmod a+x "$BINARY_PATH/$CACHED_BINARY_NAME" - fi - - export PATH="$BINARY_PATH:$PATH" - BINARY_NAME=$CACHED_BINARY_NAME -fi - -if ! [ -x "$(command -v $BINARY_NAME)" ]; then - echo - echo "Couldn't find $BINARY_NAME binary in your PATH." - echo - echo "You can download it with the --download option, or" - echo "compile it from source:" - echo - echo " 👉 https://github.com/radicle-dev/heartwood" - echo - exit 1 -fi - -if [ "$NON_INTERACTIVE" = true ]; then - rm -rf $WORKSPACE - prepare_workspace -else - prompt_workspace_removal - prepare_workspace -fi - -echo -echo "Starting $BINARY_NAME" -echo " RAD_HOME=$WORKSPACE RAD_PASSPHRASE=$PASSPHRASE $BINARY_NAME" -echo - -RAD_HOME=$WORKSPACE RAD_PASSPHRASE=$PASSPHRASE $BINARY_NAME diff --git a/tests/e2e/clipboard.spec.ts b/tests/e2e/clipboard.spec.ts index e69710c94..06455c32b 100644 --- a/tests/e2e/clipboard.spec.ts +++ b/tests/e2e/clipboard.spec.ts @@ -1,8 +1,10 @@ import type { Page } from "@playwright/test"; + import { expect, projectFixtureUrl, rid, + seedRemote, test, } from "@tests/support/fixtures.js"; @@ -35,7 +37,7 @@ test("copy to clipboard", async ({ page, browserName, context }) => { const clipboardContent = await page.evaluate( "navigator.clipboard.readText()", ); - expect(clipboardContent).toBe(`${rid}`); + expect(clipboardContent).toBe(rid); } // `rad clone` URL. @@ -78,10 +80,7 @@ test("copy to clipboard", async ({ page, browserName, context }) => { // Seed address. { await page.locator(".clipboard").first().click(); - await expectClipboard( - "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8@127.0.0.1:8776", - page, - ); + await expectClipboard(`${seedRemote}@127.0.0.1:8776`, page); } // Clear the system clipboard contents so developers don't wonder why there's diff --git a/tests/e2e/project.spec.ts b/tests/e2e/project.spec.ts index 3a500e732..98b97b025 100644 --- a/tests/e2e/project.spec.ts +++ b/tests/e2e/project.spec.ts @@ -1,4 +1,5 @@ import type { Page } from "@playwright/test"; + import { aliceMainHead, aliceRemote, @@ -341,10 +342,10 @@ test("peer and branch switching", async ({ page }) => { // Default `main` branch. { await expect(page.getByTitle("Current branch")).toContainText( - "main 1e0bb83", + "main ec5eb0b", ); await expectCounts({ commits: 9, contributors: 2 }, page); - await expect(page.locator("text=1e0bb83 Update readme")).toBeVisible(); + await expect(page.locator("text=ec5eb0b Update readme")).toBeVisible(); } } }); diff --git a/tests/e2e/project/commit.spec.ts b/tests/e2e/project/commit.spec.ts index a0063a41b..2639bbc9f 100644 --- a/tests/e2e/project/commit.spec.ts +++ b/tests/e2e/project/commit.spec.ts @@ -3,11 +3,12 @@ import { expect, projectFixtureUrl, bobRemote, + bobHead, } from "@tests/support/fixtures.js"; const modifiedFileFixture = `${projectFixtureUrl}/remotes/${bobRemote.substring( 8, -)}/commits/1e0bb83a89b63da815f2fc24e7ae3c5ceb30e0eb`; +)}/commits/${bobHead}`; test("navigation from commit list", async ({ page }) => { await page.goto(projectFixtureUrl); @@ -43,7 +44,7 @@ test("modified file", async ({ page }) => { const header = page.locator(".commit .header"); await expect(header.locator("text=Update readme")).toBeVisible(); await expect( - header.locator("text=1e0bb83a89b63da815f2fc24e7ae3c5ceb30e0eb"), + header.locator("text=ec5eb0b5efb73da17a2d25454cc47eea3967f328"), ).toBeVisible(); } diff --git a/tests/e2e/project/commits.spec.ts b/tests/e2e/project/commits.spec.ts index e077eee4b..8bf15cd56 100644 --- a/tests/e2e/project/commits.spec.ts +++ b/tests/e2e/project/commits.spec.ts @@ -76,7 +76,7 @@ test("peer and branch switching", async ({ page }) => { const latestCommit = page.locator(".teaser").first(); await expect(latestCommit).toContainText("Update readme"); - await expect(latestCommit).toContainText("1e0bb83"); + await expect(latestCommit).toContainText("ec5eb0b"); const earliestCommit = page.locator(".teaser").last(); await expect(earliestCommit).toContainText( @@ -109,7 +109,7 @@ test("relative timestamps", async ({ page }) => { ); const latestCommit = page.locator(".teaser").first(); await expect(latestCommit).toContainText("Bob Belcher committed now"); - await expect(latestCommit).toContainText("1e0bb83"); + await expect(latestCommit).toContainText("ec5eb0b"); const earliestCommit = page.locator(".teaser").last(); await expect(earliestCommit).toContainText( "Alice Liddell committed last month", diff --git a/tests/fixtures/seeds/palm.tar.bz2 b/tests/fixtures/seeds/palm.tar.bz2 deleted file mode 100644 index 78c25d57f8ecdf4daeba9fd412eb1db2be46f36c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28957 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 I5Q3+s0HnSqx&QzG diff --git a/tests/support/fixtures.ts b/tests/support/fixtures.ts index f3d998e7f..f05876c00 100644 --- a/tests/support/fixtures.ts +++ b/tests/support/fixtures.ts @@ -1,13 +1,25 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import type * as Stream from "node:stream"; import * as Fs from "node:fs/promises"; +import * as FsSync from "node:fs"; import * as Path from "node:path"; +import { dirname } from "path"; +import { fileURLToPath } from "url"; import { test as base, expect } from "@playwright/test"; +import * as Process from "./process.js"; import * as logLabel from "@tests/support/logLabel.js"; +import { createPeerManager } from "./peerManager.js"; +import { sleep } from "@app/lib/sleep"; export { expect }; +const filename = fileURLToPath(import.meta.url); +export const supportDir = dirname(filename); +export const tmpDir = Path.resolve(supportDir, "..", "./tmp"); +const fixturesDir = Path.resolve(supportDir, "..", "./fixtures"); + export const test = base.extend<{ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type forAllTests: void; @@ -135,39 +147,6 @@ function log(text: string, label: string, outputLog: Stream.Writable) { } } -export function configFixture() { - window.APP_CONFIG = { - reactions: [], - seeds: { - defaultHttpdPort: 8080, - defaultHttpdScheme: "http", - defaultNodePort: 8776, - pinned: [ - { - baseUrl: { - hostname: "127.0.0.1", - port: 8080, - scheme: "http", - }, - }, - ], - }, - projects: { - pinned: [ - { - name: "source-browsing", - id: "rad:git:hnrkdi8be7n4hhqoz9rpzrgd68u9dr3zsxgmy", - baseUrl: { - hostname: "127.0.0.1", - port: 8080, - scheme: "http", - }, - }, - ], - }, - }; -} - export function appConfigWithFixture() { window.APP_CONFIG = { reactions: [], @@ -189,7 +168,7 @@ export function appConfigWithFixture() { pinned: [ { name: "source-browsing", - id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", + id: "rad:z4BwwjPCFNVP27FwVbDFgwVwkjcir", baseUrl: { hostname: "127.0.0.1", port: 8080, @@ -201,12 +180,101 @@ export function appConfigWithFixture() { }; } +export async function startPalmHttpd() { + const peerManager = await createPeerManager({ + dataDir: Path.resolve(Path.join(tmpDir, "peers")), + outputLog: FsSync.createWriteStream(Path.resolve(Path.join(tmpDir, "log"))), + }); + const palm = await peerManager.startPeer({ name: "palm" }); + await palm.startHttpd(); +} + +export async function createSeedFixture() { + const projectName = "source-browsing"; + const sourceBrowsingDir = Path.join(tmpDir, "repos", projectName); + await Fs.mkdir(sourceBrowsingDir, { recursive: true }); + await Process.spawn("tar", [ + "-xf", + Path.join(fixturesDir, `repos/${projectName}.tar.bz2`), + "-C", + sourceBrowsingDir, + ]); + const peerManager = await createPeerManager({ + dataDir: Path.resolve(Path.join(tmpDir, "peers")), + outputLog: FsSync.createWriteStream(Path.join(tmpDir, "log")), + }); + const palm = await peerManager.startPeer({ name: "palm" }); + await palm.startHttpd(); + const alice = await peerManager.startPeer({ + name: "alice", + gitOptions: gitOptions["alice"], + }); + const bob = await peerManager.startPeer({ + name: "bob", + gitOptions: gitOptions["bob"], + }); + await palm.startNode({ trackingPolicy: "track", trackingScope: "all" }); + await alice.startNode({ connect: palm }); + await bob.startNode({ connect: palm }); + await sleep(1000); + await alice.git(["clone", sourceBrowsingDir], { cwd: alice.checkoutPath }); + await alice.git(["checkout", "main"]); + await alice.rad([ + "init", + "--name", + projectName, + "--default-branch", + "main", + "--description", + "Git repository for source browsing tests", + "--announce", + ]); + await alice.git(["checkout", "feature/branch"]); + await alice.git(["push", "rad"]); + await alice.git(["checkout", "orphaned-branch"]); + await alice.git(["push", "rad"]); + const { stdout: rid } = await alice.rad(["inspect"]); + await alice.rad(["track", bob.nodeId]); + await sleep(2000); + await bob.rad(["clone", rid], { cwd: bob.checkoutPath }); + await sleep(2000); + await Fs.writeFile( + Path.join(bob.checkoutPath, "source-browsing", "README.md"), + "Updated readme", + ); + await bob.git(["add", "README.md"]); + await bob.git([ + "commit", + "--message", + "Update readme", + "--date", + "Mon Dec 21 14:00 2022 +0100", + ]); + await bob.git(["push", "rad"]); +} + export const aliceMainHead = "fcc929424b82984b7cbff9c01d2e20d9b1249842"; export const aliceRemote = - "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"; + "did:key:z6MkqGC3nWZhYieEVTVDKW5v588CiGfsDSmRVG9ZwwWTvLSK"; export const bobRemote = - "did:key:z6MksMTThc1aDU2Ztc43jJUivuyBLNWiLsDf4X65rABe7HbA"; -export const rid = "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT"; + "did:key:z6Mkg49NtQR2LyYRDCQFK4w1VVHqhypZSSRo7HsyuN7SV7v5"; +export const bobHead = "ec5eb0b5efb73da17a2d25454cc47eea3967f328"; +export const rid = "rad:z4BwwjPCFNVP27FwVbDFgwVwkjcir"; export const projectFixtureUrl = `/seeds/127.0.0.1/${rid}`; export const seedPort = 8080; -export const seedRemote = "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"; +export const seedRemote = "z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S"; +export const gitOptions = { + alice: { + GIT_AUTHOR_NAME: "Alice Liddell", + GIT_AUTHOR_EMAIL: "alice@radicle.xyz", + GIT_COMMITTER_NAME: "Alice Liddell", + GIT_COMMITTER_EMAIL: "alice@radicle.xyz", + }, + bob: { + GIT_AUTHOR_NAME: "Bob Belcher", + GIT_AUTHOR_EMAIL: "bob@radicle.xyz", + GIT_COMMITTER_NAME: "Bob Belcher", + GIT_COMMITTER_EMAIL: "bob@radicle.xyz", + GIT_COMMITTER_DATE: "Mon Dec 21 14:00 2022 +0100", + }, +}; diff --git a/tests/support/globalSetup.ts b/tests/support/globalSetup.ts index de9dd8a91..f37fade5e 100644 --- a/tests/support/globalSetup.ts +++ b/tests/support/globalSetup.ts @@ -1,31 +1,86 @@ import type { FullConfig } from "@playwright/test"; -import { seedPort, seedRemote } from "@tests/support/fixtures.js"; +import * as Fs from "node:fs/promises"; +import * as Path from "node:path"; +import * as readline from "node:readline/promises"; +import { execa } from "execa"; + +import { + createSeedFixture, + supportDir, + startPalmHttpd, + tmpDir, +} from "./fixtures.js"; + +const workspacePaths = [Path.join(tmpDir, "peers"), Path.join(tmpDir, "repos")]; export default async function globalSetup(_config: FullConfig): Promise { - await assertHttpApiRunning(); -} - -// Assert that the test http-api is running. If it is not running, throw an -// error that explains how to run it. -async function assertHttpApiRunning(): Promise { - const notRunningMessage = - "The http-api server with test fixtures needs to be running.\n" + - "👉 You can start it with `./scripts/run-httpd-with-fixtures`\n"; - - let nodeId: string | undefined = undefined; - try { - const response = await fetch(`http://0.0.0.0:${seedPort}/api/v1`); - const data = await response.json(); - nodeId = data.node.id; - } catch (err) { - console.error(err); - throw new Error(notRunningMessage); + await assertRadInstalled(); + } catch (error) { + console.error(error); + process.exit(1); } - if (nodeId !== seedRemote) { - const wrongSeedMessage = `The server on port ${seedPort} doesn't have the right fixtures.\n`; - throw new Error(wrongSeedMessage + notRunningMessage); + if (!process.env.SKIP_FIXTURE_CREATION) { + console.log("Setting up global test environment"); + if (!process.env.CI) { + await promptWorkspaceRemoval(); + } + await removeWorkspace(); + + console.log("Creating seed fixture"); + await createSeedFixture(); + console.log("Running tests"); + } else { + await startPalmHttpd(); + } +} + +// Assert that the `rad` CLI is installed and has the correct version. +async function assertRadInstalled(): Promise { + const versionConstraint = ( + await Fs.readFile(`${supportDir}/heartwood-version`, "utf8") + ).substring(0, 7); + const { stdout: version } = await execa("rad", ["--version"]); + if (!version.includes(versionConstraint)) { + throw new Error( + `rad version ${version} does not satisfy ${versionConstraint}`, + ); + } +} + +async function promptWorkspaceRemoval(): Promise { + console.log(""); + console.log("This will irrevocably destroy the following directories:"); + console.log(""); + workspacePaths.forEach(path => console.log(path)); + console.log(""); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const result = await rl.question( + "Are you sure you want to continue? [yes/no]: ", + ); + rl.close(); + + if (result.toLowerCase() === "yes") { + console.log("Done"); + return; + } + + console.log("Ok, I won't touch your data."); + process.exit(1); +} + +async function removeWorkspace(): Promise { + for (const path of workspacePaths) { + await Fs.rm(path, { + recursive: true, + force: true, + }); } } diff --git a/tests/support/heartwood-version b/tests/support/heartwood-version new file mode 100644 index 000000000..34eec893d --- /dev/null +++ b/tests/support/heartwood-version @@ -0,0 +1 @@ +a6a3290833f6da4a7785a426bed342df069ce47f diff --git a/tests/support/peerManager.ts b/tests/support/peerManager.ts new file mode 100644 index 000000000..e03539e0c --- /dev/null +++ b/tests/support/peerManager.ts @@ -0,0 +1,217 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { ExecaChildProcess, Options } from "execa"; + +import * as Fs from "node:fs/promises"; +import * as Path from "node:path"; +import * as Stream from "node:stream"; +import getPort from "get-port"; +import waitOn from "wait-on"; +import { execa } from "execa"; + +import * as Process from "./process.js"; + +interface PeerManagerParams { + dataPath: string; + seed: string; + name: string; + gitOptions?: Record; + outputLog: Stream.Writable; +} + +export interface PeerManager { + startPeer(params: { + name: string; + gitOptions?: Record; + }): Promise; +} + +export function generateSeed(index: number) { + return Array(64).fill(index.toString()).join(""); +} + +export async function createPeerManager(createParams: { + dataDir: string; + outputLog?: Stream.Writable; +}): Promise { + let outputLog: Stream.Writable; + let outputLogFile: Fs.FileHandle; + if (createParams.outputLog) { + outputLog = createParams.outputLog; + } else { + outputLogFile = await Fs.open( + Path.join(createParams.dataDir, "peer-manager.log"), + "a", + ); + outputLog = outputLogFile.createWriteStream(); + } + + const nodes: RadiclePeer[] = []; + return { + // Starts a new node and registers it. + async startPeer(params) { + const peer = await RadiclePeer.create({ + dataPath: createParams.dataDir, + name: params.name, + gitOptions: params.gitOptions, + seed: generateSeed(nodes.length + 1), + outputLog, + }); + nodes.push(peer); + + return peer; + }, + }; +} + +export class RadiclePeer { + public checkoutPath: string; + public nodeId: string; + + #seed: string; + #radHome: string; + #outputLog: Stream.Writable; + #gitOptions?: Record; + #listenSocketAddr?: string; + + private constructor(props: { + checkoutPath: string; + nodeId: string; + seed: string; + gitOptions?: Record; + radHome: string; + logFile: Stream.Writable; + }) { + this.checkoutPath = props.checkoutPath; + this.nodeId = props.nodeId; + this.#gitOptions = props.gitOptions; + this.#seed = props.seed; + this.#radHome = props.radHome; + this.#outputLog = props.logFile; + } + + public static async create({ + dataPath, + name, + gitOptions, + seed, + outputLog: logFile, + }: PeerManagerParams): Promise { + const checkoutPath = Path.join(dataPath, name, "copy"); + await Fs.mkdir(checkoutPath, { recursive: true }); + const radHome = Path.join(dataPath, name, "home"); + await Fs.mkdir(radHome, { recursive: true }); + + const env = { + ...gitOptions, + RAD_HOME: radHome, + RAD_PASSPHRASE: "asdf", + RAD_SEED: seed, + }; + + await execa("rad", ["auth"], { env }); + const { stdout: nodeId } = await execa("rad", ["self", "--nid"], { env }); + + return new RadiclePeer({ + checkoutPath, + gitOptions, + seed, + nodeId, + radHome, + logFile, + }); + } + + public async startHttpd() { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.spawn("radicle-httpd"); + + await waitOn({ + resources: ["tcp:127.0.0.1:8080"], + timeout: 7000, + }); + } + + public async startNode(params?: { + connect?: RadiclePeer; + trackingScope?: "trusted" | "all"; + trackingPolicy?: "track" | "block"; + }) { + const gitPort = await getPort(); + const gitSocketAddr = `0.0.0.0:${gitPort}`; + const listenPort = await getPort(); + this.#listenSocketAddr = `0.0.0.0:${listenPort}`; + + const args = [ + "--git-daemon", + gitSocketAddr, + "--listen", + this.#listenSocketAddr, + ]; + if (params?.connect) { + args.push( + "--connect", + `${params.connect.nodeId}@${params.connect.#listenSocketAddr}`, + ); + } + if (params?.trackingScope) { + args.push("--tracking-scope", params.trackingScope); + } + if (params?.trackingPolicy) { + args.push("--tracking-policy", params.trackingPolicy); + } + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.spawn("radicle-node", args); + + await waitOn({ + resources: [`tcp:${this.#listenSocketAddr}`], + timeout: 7000, + }); + } + + public uiUrl(): string { + return `/seeds/127.0.0.1:8080`; + } + public ridUrl(rid: string): string { + return `/seeds/127.0.0.1:8080/${rid}`; + } + + public git(args: string[] = [], opts?: Options): ExecaChildProcess { + return this.spawn("git", args, { + cwd: Path.join(this.checkoutPath, "source-browsing"), + ...opts, + }); + } + + public rad(args: string[] = [], opts?: Options): ExecaChildProcess { + return this.spawn("rad", args, { + cwd: Path.join(this.checkoutPath, "source-browsing"), + ...opts, + }); + } + + public spawn( + cmd: string, + args: string[] = [], + opts?: Options, + ): ExecaChildProcess { + opts = { + ...opts, + env: { + ...opts?.env, + ...this.#gitOptions, + GIT_CONFIG_GLOBAL: "/dev/null", + GIT_CONFIG_NOSYSTEM: "1", + RAD_HOME: this.#radHome, + RAD_PASSPHRASE: "asdf", + RAD_SEED: this.#seed, + }, + }; + const childProcess = Process.spawn(cmd, args, opts); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Process.prefixOutput(childProcess, this.nodeId, this.#outputLog); + + return childProcess; + } +} diff --git a/tests/support/process.ts b/tests/support/process.ts new file mode 100644 index 000000000..197c96bf8 --- /dev/null +++ b/tests/support/process.ts @@ -0,0 +1,83 @@ +import type { ExecaChildProcess, Options } from "execa"; + +import * as Stream from "node:stream"; +import onExit from "exit-hook"; +import { StringDecoder } from "string_decoder"; +import { execa } from "execa"; + +import { make } from "./logLabel.js"; + +// Processes that should be SIGKILLed when the Node process shutsdown. +// We add all proxy and seed instances that we spawn to this list. +const processes: ExecaChildProcess[] = []; + +onExit(killAllProcesses); + +// Kill all processes with SIGKILL +export function killAllProcesses(): void { + for (const process of processes) { + if (process.exitCode === null) { + process.kill("SIGKILL"); + } + } +} + +// Spawn a process with `execa` and register it. +// +// The process will be killed by `killAllProcesses`. +export function spawn( + bin: string, + args: string[], + options?: Options, +): ExecaChildProcess { + const child = execa(bin, args, options); + processes.push(child); + return child; +} + +// Forwards piped `stdout` and `stderr` of a child process to `output` +// and prefixes it with the given label. The prefix is colored. +export function prefixOutput( + childProcess: ExecaChildProcess, + label: string, + output: Stream.Writable, +): ExecaChildProcess { + const pref = make(label); + if (childProcess.stdout) { + const stdoutPrefix = new LinePrefix(pref); + childProcess.stdout.pipe(stdoutPrefix).pipe(output, { end: false }); + } + if (childProcess.stderr) { + const stderrPrefix = new LinePrefix(pref); + childProcess.stderr.pipe(stderrPrefix).pipe(output, { end: false }); + } + + return childProcess; +} + +// A transform that prefixes each line from the source with the given +// string and pushes it to the sink. +class LinePrefix extends Stream.Transform { + private buffer: string = ""; + private stringDecoder = new StringDecoder(); + + public constructor(private prefix: string) { + super(); + } + + public _transform(data: Buffer, _encoding: string, next: () => void): void { + const str = this.buffer + this.stringDecoder.write(data); + const lines = str.split(/\r?\n/); + this.buffer = lines.pop() || ""; + lines.forEach(line => this.push(`${this.prefix}${line}\n`)); + next(); + } + + public _flush(done: () => void): void { + const rest = `${this.buffer}${this.stringDecoder.end()}`; + if (rest) { + this.push(`${this.prefix}${rest}\n`); + } + done(); + } +}