test: drive the EIP-1193 dApp approval round trips in the browser (closes #183)
All checks were successful
check / check (push) Successful in 33s

The seam between the content script, the inpage provider, the background worker
and the approval popup had no coverage at all. The unit suite covers each side
in isolation, so a wallet that signed the wrong payload, handed back a signature
from the wrong key, hung on a rejected prompt, or put the user's password back
on the extension messaging boundary would have passed everything.

Ten end-to-end tests now drive it. The route handler serves a local test page on
a reserved-TLD origin; window.ethereum arrives there from the shipped MAIN-world
content script, not from anything the fixture installs, and the page's own
EIP-6963 announcement is required to be that provider by object identity. The
page then speaks eth_requestAccounts, personal_sign, eth_signTypedData_v4 and
eth_sendTransaction through the real prompts, approved and rejected.

The assertions are the point:

  - every signature is recovered in the runner with verifyMessage,
    verifyTypedData and Transaction.from(), and compared against the active
    address read out of extension storage. The background verifies too; nothing
    here leans on its verdict.
  - the transaction is checked against the raw signed bytes captured at
    eth_sendRawTransaction, which the RPC stub now records and answers with the
    transaction's real hash. Signer, recipient, value, call data and chain id
    are all compared there, and the hash the page received is required to be the
    hash of those bytes.
  - a rejected prompt must reach the page as a rejection rather than hang or
    resolve, and must carry EIP-1193 code 4001 across the boundary. The code is
    asserted on the wire because that is where it survives: the inpage provider
    rebuilds the rejection as a bare Error, so the calling page catches a message
    and no code. Reported, not asserted either way.
  - the password must appear in no message the approval window sends to the
    background, observed directly by wrapping chrome.runtime.sendMessage before
    Approve is clicked, with the response message that would carry it required
    to be present so the check cannot pass on an empty record.

Every one of those was run against a deliberately broken variant and seen to
fail: a corrupted recovered signer, a payload carrying the password again, a
provider that resolves instead of rejecting, and approval screens displaying the
wrong message, value and call data.

Two honest limits. The RPC is stubbed throughout, so this does not discharge a
real dApp with real funds against a real network. And the site-connection prompt
goes through chrome.action.openPopup(), whose browser-action popup headless
Chromium will not expose as a page, so that one prompt is driven at the URL the
extension puts on the action instead — same page, same approval id, but a real
toolbar click is not observable from a headless harness. Both are stated in
README.md rather than presented as covered.

Test-only: nothing under src/ changes.
This commit is contained in:
2026-08-12 11:01:03 +00:00
parent 52c7c1b060
commit 13d444c3aa
4 changed files with 1150 additions and 1 deletions

View File

@@ -169,6 +169,34 @@ reserve while sitting on the same side of the estimate, so swapping the two in
what [#154](https://git.eeqj.de/sneak/AutistMask/issues/154) was, and it was
previously correct by reading only.
It also covers the **dApp approval round trips** — the one place where the
content script, the inpage provider, the background worker and the approval
popup all have to work together. A local test page is served by the route
handler on a reserved-TLD origin, gets `window.ethereum` from the shipped
`MAIN`-world content script like any other page, and drives
`eth_requestAccounts`, `personal_sign`, `eth_signTypedData_v4` and
`eth_sendTransaction` through the real prompts. Every signature is recovered in
the runner and compared against the active address, the transaction assertions
run against the raw signed transaction captured at `eth_sendRawTransaction`
rather than against anything the extension reported, rejecting each prompt is
required to return a rejection to the page rather than hang or resolve, and the
password is required to be absent from every message the approval window sends
to the background — with the message that would carry it required to be present,
so that check cannot pass by observing nothing. That last one is the standing
floor under [#157](https://git.eeqj.de/sneak/AutistMask/issues/157).
Three limits of that coverage, none of them papered over. The RPC is stubbed
throughout, so this is **not** a real dApp against a real network with real
funds; that remains a human pass before 1.0.0. The site-connection prompt is
raised through `chrome.action.openPopup()`, and headless Chromium's
browser-action popup is not a page Playwright can see or click, so that one
prompt is driven at the URL the extension itself puts on the action — the same
page and the same approval id, but whether a real toolbar click shows it is not
observable here. And the EIP-1193 error code does not survive the last hop: the
rejection that crosses the boundary carries code 4001 and is asserted to, but
`src/content/inpage.js` rebuilds it as `new Error(message)`, so the calling page
catches an error with no `code` property.
Any test that drives a failure path on purpose declares the `console.error` it
is about to provoke, via `errors.expect()`. That is not a mute: the declaration
consumes exactly one matching record, and a declaration nothing matched fails