feat: multi-wallet architecture with localStorage state, EIP-712 verification, cross-tab WS leadership
- wallets.js: WalletManager state management with chain validation - verifier.js: WalletVerifier EIP-712 signing via window.ethereum - stream.js: WalletStreamManager with BroadcastChannel leader election, backoff + jitter - AGENTS.md: updated with coding guidelines
This commit is contained in:
116
test/README.md
Normal file
116
test/README.md
Normal file
@ -0,0 +1,116 @@
|
||||
# cbBTC Dashboard Test Suite
|
||||
|
||||
The test suite is organized under the `/test` folder. All tests run in Node.js with ES modules and mock browser APIs (`localStorage`, `WebSocket`, `BroadcastChannel`, `window.ethereum`). The setup file `test/test_setup.mjs` provides the global `location` object that `stream.js` requires at import time.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Targets | Requires | How to run |
|
||||
|---|---|---|---|
|
||||
| `test/test_wallets.js` | **WalletManager** — CRUD, persistence, security | None | `node --experimental-modules test/test_wallets.js` |
|
||||
| `test/test_stream.js` | **WalletStreamManager** — WebSocket, backoff, routing | `test/test_setup.mjs` | `node --experimental-modules --import ./test/test_setup.mjs ./test/test_stream.js` |
|
||||
| `test/test_tabs.js` | **WalletStreamManager** — multi-tab leader election, failover | `test/test_setup.mjs` | `node --experimental-modules --import ./test/test_setup.mjs ./test/test_tabs.js` |
|
||||
| `test/test_verifier.js` | **WalletVerifier** — Web3 provider error handling | None | `node --experimental-modules test/test_verifier.js` |
|
||||
| `test/test_setup.mjs` | Provides `globalThis.location` for `stream.js` | — | Imported via `--import` |
|
||||
|
||||
## What Can Be Tested — by Module
|
||||
|
||||
### 1. WalletManager (`test_wallets.js`)
|
||||
|
||||
The `WalletManager` class manages tracked wallets in `localStorage`. The test suite covers:
|
||||
|
||||
**Address Validation**
|
||||
- Rejection of invalid address formats per chain (EVM, Bitcoin, Solana).
|
||||
- Acceptance of valid addresses for EVM (`0x...`), Bitcoin legacy (`1...`), and Solana base58 patterns.
|
||||
|
||||
**CRUD (Create/Read/Update/Delete)**
|
||||
- `addWallet()` — adds wallets, persists to storage, returns success/error.
|
||||
- `getWallets()` — returns deep copies of tracked wallets.
|
||||
- `findWallet()` — returns wallet object or `undefined`.
|
||||
- `removeWallet()` — splices wallet from state and persists.
|
||||
|
||||
**Duplicate Guard**
|
||||
- Same `address` + `chain` pair is rejected on second `addWallet()` call.
|
||||
|
||||
**Verification Lifecycle**
|
||||
- Wallets start with `isVerified: false`.
|
||||
- `verifyWallet()` sets the flag to `true` and stores the signature.
|
||||
- `revokeVerification()` resets the flag and clears the signature.
|
||||
|
||||
**Security: localStorage Tampering**
|
||||
- `loadWallets()` always resets `isVerified` to `false` on restart, regardless of what was written to storage. This prevents a user from forging verification by editing `localStorage` directly.
|
||||
|
||||
### 2. WalletVerifier (`test_verifier.js`)
|
||||
|
||||
The `WalletVerifier` class handles Web3 provider interactions. The test suite covers error-path cases:
|
||||
|
||||
**Missing Extension**
|
||||
- When `window.ethereum` is undefined, verification fails with `"No Web3 wallet extension found"`.
|
||||
|
||||
**User Rejection (Account Request)**
|
||||
- When the user denies `eth_requestAccounts` (EIP-1193 error 4001), verification cancels with `"Verification cancelled"`.
|
||||
|
||||
**Signature Rejection**
|
||||
- When the user denies the typed-data signature request, the error is mapped to `"rejected"`.
|
||||
|
||||
### 3. WalletStreamManager — Core (`test_stream.js`)
|
||||
|
||||
The `WalletStreamManager` class manages the WebSocket connection, event routing, and reconnect logic with exponential backoff. The test suite covers:
|
||||
|
||||
**Multiplexed Subscription**
|
||||
- On connect, the manager sends a single `subscribe` frame containing all tracked wallets (from `WalletManager`).
|
||||
|
||||
**Event Routing**
|
||||
- Incoming server messages are parsed and dispatched to registered `on('balance_update', ...)` callbacks.
|
||||
- Events route by `data.type` (or `data.event`, or fallback `'message'`).
|
||||
|
||||
**Callback Error Isolation**
|
||||
- If one registered callback throws, remaining callbacks for the same event still execute. Exceptions are caught and logged but never propagate.
|
||||
|
||||
**Dynamic Subscription**
|
||||
- `subscribeToNewWallet()` sends an immediate `subscribe_wallet` frame without cycling the connection.
|
||||
|
||||
**Exponential Backoff + Jitter**
|
||||
- On network drop (`simulateNetworkDrop()`), the manager retries connecting. The delay follows `BASE_DELAY * 2^attempt` with ±25% jitter, capped at `MAX_DELAY` (30s).
|
||||
- Tests speed up time by overriding `setTimeout` to fire instantly.
|
||||
|
||||
### 4. WalletStreamManager — Multi-Tab (`test_tabs.js`)
|
||||
|
||||
The leader/follower election and cross-tab synchronization system relies on `BroadcastChannel` and `localStorage` heartbeats. The test suite covers:
|
||||
|
||||
**Leader Election**
|
||||
- First tab to connect finds no heartbeat in `localStorage`, claims leadership, opens the WebSocket, and starts heartbeats.
|
||||
- Second tab finds the heartbeat, assigns itself as a follower, and avoids opening a second WebSocket.
|
||||
|
||||
**Cross-Tab Event Broadcasting**
|
||||
- Leader receives server data on the WebSocket and posts it to `BroadcastChannel`.
|
||||
- Followers receive the broadcast and dispatch it to their local listeners.
|
||||
|
||||
**Proxy Subscription Forwarding**
|
||||
- When a follower calls `subscribeToNewWallet()`, the request is sent through `BroadcastChannel` to the leader, which opens the actual network subscription.
|
||||
|
||||
**Automatic Failover**
|
||||
- When the leader tab closes (`disconnect()`), it broadcasts `LEADER_DEATH` before closing the channel.
|
||||
- Followers detect the signal, claim leadership, remove the old heartbeat, write a new one, and open their own WebSocket connection.
|
||||
|
||||
## Mock Infrastructure
|
||||
|
||||
All tests replace browser APIs with in-memory equivalents:
|
||||
|
||||
- **localStorage** — plain object (`mockStorage`) with `getItem`, `setItem`, `removeItem`, `clear`.
|
||||
- **setTimeout** — overridden to fire callbacks after 1ms instead of the specified delay, so backoff/retry logic runs instantly.
|
||||
- **WebSocket** — `MockWebSocket` auto-opens after 2ms, exposes `simulateServerPush()` and `simulateNetworkDrop()` helpers.
|
||||
- **BroadcastChannel** — `MockBroadcastChannel` delivers messages synchronously to all other registered channels, simulating cross-tab delivery.
|
||||
|
||||
## Extending the Test Suite
|
||||
|
||||
To add a new test:
|
||||
|
||||
1. Add a `describe`/`it` block or inline `assert` inside the relevant `test_<module>.js` file.
|
||||
2. For stream-related tests, ensure the file is imported via `--import ./test/test_setup.mjs`.
|
||||
3. Run the test from the project root with the appropriate command.
|
||||
|
||||
To add a new module test:
|
||||
|
||||
1. Create `test/test_<module>.js` with imports using `../<module>.js` (relative to the `/test` folder).
|
||||
2. Add the required mock infrastructure at the top.
|
||||
3. Register it in this README with its run command.
|
||||
Reference in New Issue
Block a user