There was no packaging target anywhere, no artifact, and no `key` in `manifest/chrome.json` — so an unpacked Chrome load derived its extension id, and therefore its `chrome.storage.local` partition, from the absolute checkout path. Moving or re-cloning the checkout presented an empty wallet, with no error and nothing in the UI to say so. `manifest/chrome.json` now carries a fixed `key`: the public half of an RSA keypair, which pins the extension id to `gipbhkogfopeahplcjhipkgpcimdpkip`. The private half is a credential and is not in this repo; no target generates one into the working tree, and `tests/extensionId.test.js` fails if a `.pem` is ever committed. Changing `key` changes the id and orphans every wallet stored under the old one. `make package` (script/package) runs `make build` — the only audited path to a release build — and writes one self-contained, versioned archive per browser into `release/`, plus `SHA256SUMS`. The archives are deterministic: entries sorted, timestamps fixed, compression level fixed, so two builds of one commit are byte-identical. Self-containment is checked rather than assumed: every path the manifests and the popup HTML reference is resolved and required to be inside the archive, a reference that climbs out of the extension root is a hard failure, and files left at the `dist/` root — `dist/styles.css`, which build.js copies into each browser directory — are reported as deliberately not shipped rather than dropped by a glob. The archive is then read back off disk and compared member by member against the directory it was built from. The zip writer and reader are stdlib zlib in `script/lib/zip.js`; no new dependency, and nothing unpinned. One version, enforced rather than generated. `script/lib/version.js` requires `package.json`, `manifest/chrome.json` and `manifest/firefox.json` to agree and fails the build naming each file and what it said, instead of reading from one of the three. `BUILD_COMMIT` now carries `-dirty` when the working tree does not match `HEAD`, and `-unknown` when git cannot say; the full hash behind the About screen's commit link stays clean so the link still resolves. Two real-browser observations, both run through the pinned harnesses: - `tests/e2e/storagePartition.js` loads the build from two different paths in one Chrome profile. With `key`: same id, and the second load reads the first load's storage. Without `key`: different ids, and the second load sees an empty partition. Loading both keyed copies at once yields one id, not two. - `tests/e2e/firefox/reinstall.js` installs the packaged XPI in a real Firefox, creates a wallet, quits the browser, restarts on the same profile, adds the add-on again, and decrypts the vault back to the original recovery phrase. It then observes that an explicit uninstall DESTROYS that storage — correct browser behaviour, but for a wallet it means Remove is irreversible except from the recovery phrase, so README.md says so. Firefox ships an UNSIGNED XPI. README.md states plainly that release Firefox and ESR will refuse it, that Developer Edition, Nightly or an Unbranded build is required, and that a temporary add-on does not survive a browser restart. AMO signing, CRX packing, tagging and any upload are deliberately out of scope.
100 lines
4.1 KiB
Bash
Executable File
100 lines
4.1 KiB
Bash
Executable File
#!/bin/sh
|
|
# script/test-e2e: build the extension and drive the real popup in a real
|
|
# Chromium inside a pinned container. Our own extension to
|
|
# scripts-to-rule-them-all.
|
|
#
|
|
# Deliberately NOT called by script/check or script/test: REPO_POLICIES.md
|
|
# caps make test at 20 seconds and a browser suite does not fit. Run it
|
|
# yourself before touching popup views. ESLint's no-undef now catches a
|
|
# used-but-not-imported identifier in make check, but only this suite sees
|
|
# what a view actually does when it runs.
|
|
# .gitea/workflows/e2e.yml also runs it on every push, in a job separate
|
|
# from check so that cap and the local fast path both stay intact.
|
|
#
|
|
# Docker is the only prerequisite. The repo reaches the container as a
|
|
# build context and the extension is built inside it (see
|
|
# tests/e2e/Dockerfile), so nothing here depends on the node, yarn or make
|
|
# on the machine that starts the run. That is not a convenience: a bind
|
|
# mount cannot work under Gitea Actions, and the runner image's node is too
|
|
# old to install this repo's dependencies.
|
|
set -eu
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
|
|
|
IMAGE="$("$SCRIPT_DIR/projectname")-e2e-chrome"
|
|
|
|
IIDFILE=""
|
|
|
|
cleanup() {
|
|
if [ -n "$IIDFILE" ]; then
|
|
rm -f "$IIDFILE"
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
cd "$ROOT"
|
|
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
echo "test-e2e: docker is required to run the e2e suite" >&2
|
|
exit 1
|
|
fi
|
|
|
|
IIDFILE="$(mktemp)"
|
|
trap cleanup EXIT
|
|
trap 'cleanup; exit 130' INT TERM
|
|
|
|
echo "Building the Chrome e2e image (extension included)..."
|
|
docker build --iidfile "$IIDFILE" -t "$IMAGE" -f tests/e2e/Dockerfile .
|
|
|
|
echo "Running e2e suite in the pinned Playwright container..."
|
|
# The image is run by ID, not by tag: where two clones of this repo run
|
|
# the suite at once, the other build can move the tag between this
|
|
# build and this run, and the suite would then silently test the other
|
|
# checkout.
|
|
#
|
|
# --ipc=host: Chromium's shared-memory needs more than the default
|
|
# 64MB /dev/shm or renderers crash.
|
|
# HOME=/tmp: the image's root home is not a reliable place for the
|
|
# browser profile.
|
|
# PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1: without it,
|
|
# ctx.route() intercepts page requests only, and every fetch made by
|
|
# the MV3 background service worker — the JSON-RPC calls behind
|
|
# every approval the suite drives among them — goes to the real
|
|
# internet. The flag is experimental and Playwright may drop or
|
|
# rename it. It cannot break silently: the harness asks the worker
|
|
# for one request of its own at launch and aborts the whole suite
|
|
# if it does not reach the route handler (see the interception
|
|
# canary in tests/e2e/harness.js). If a future Playwright removes
|
|
# the flag, that probe is what will fail, and the fix is either a
|
|
# replacement mechanism or an honest downgrade of the isolation
|
|
# claim in tests/e2e/network.js and README.md — not deleting the
|
|
# probe. The image is pinned by digest, so this can only ever bite
|
|
# on a deliberate bump.
|
|
docker run --rm \
|
|
--ipc=host \
|
|
-e HOME=/tmp \
|
|
-e PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1 \
|
|
-e "E2E_TRACE_NETWORK=${E2E_TRACE_NETWORK:-0}" \
|
|
"$(cat "$IIDFILE")" \
|
|
node tests/e2e/run.js
|
|
|
|
# Where chrome.storage.local lives, and what moves it: two unpacked loads
|
|
# from two different paths in one profile, with the shipped manifest and
|
|
# again with `key` stripped out. Its own browser sessions — four of them —
|
|
# because the whole subject is what happens ACROSS loads, which the suite
|
|
# above cannot express with one.
|
|
#
|
|
# No PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS here: this drives no
|
|
# RPC and installs no route handlers, and the browser is started with
|
|
# --host-resolver-rules=MAP * ~NOTFOUND so nothing it does can leave.
|
|
echo "Running the extension-id and storage-partition observations..."
|
|
docker run --rm \
|
|
--ipc=host \
|
|
-e HOME=/tmp \
|
|
"$(cat "$IIDFILE")" \
|
|
node tests/e2e/storagePartition.js
|
|
}
|
|
|
|
main "$@"
|