test(integration): Tier 1 expansion — 23 new cross-layer BitBox specs#569
Merged
Merged
Conversation
Tier-1 integration tests stitching SellBitboxCubit → FakeBitboxCredentials boundary → real RealUnitSellPaymentInfoService → MockClient. The cubit and the service are both real production code; only the BitBox transport and the HTTP wire are stubbed. This pins: * happy path — full swap+deposit ceremony emits two BitBox signs and the correct broadcast order/wire-shape (unsignedTx + r/s/v padding). Regression class: silently double-signing on the device or swapping leg order. * cancel mid-swap — FakeBitboxBehavior.cancel propagates as SigningCancelledException all the way to SellBitboxError instead of being silently accepted as a successful sign (PR #322 bug class). * disconnect — BitboxNotConnectedException is caught EXPLICITLY by the cubit's typed catch and emits SellBitboxBitboxRequired, not a generic Error state. Regression class: re-pair screen replaced by raw error string (PR #341). * malformed signature — FormatException from a frame-desync hits the generic catch and surfaces as SellBitboxError, NOT mis-classified as a BitBox disconnect (would mask sig bugs as UX disconnect prompts). * deposit-retry — transient 5xx on the deposit broadcast lands the cubit in SellBitboxDepositRetry with both signed envelopes preserved; the user does not have to re-sign on the device. Regression class: funds-at-risk loss of the already-signed swap (PR #338).
Tier-1 integration tests for the EIP-7702 delegation sign path. Stitches Eip712Signer.signDelegation directly against the FakeBitboxCredentials boundary (the production caller, RealUnitSellPaymentInfoService.confirmPayment, delegates the EIP-712 sign here without further mutation). Pins: * happy path on BOTH credential arms — FakeBitbox.success and a real EthPrivateKey both produce a 65-byte sig over the same payload, and the two sigs differ (no leaked default key path). Regression class: the polymorphic switch in _signTypedData wired both arms to the same key (PR #318). * cancel — FakeBitbox.cancel returns "0x" and the post-sign guard in _signTypedData converts it into a typed SigningCancelledException, instead of letting the empty signature reach the backend. * chainId-wiring — verified across [1, 11155111, 42161] that signDelegation forwards eip7702Data.domain.chainId verbatim to the BitBox sign call (both the int argument AND the chainId embedded in the json payload's domain). A wrong chainId here makes the BitBox display the wrong network — silent until the user spots it. * disconnect — BitboxNotConnectedException at the BitBox boundary surfaces verbatim through Eip712Signer; it is NOT re-wrapped as SigningCancelledException (would mask a re-pair situation as a cancel prompt). Test uses a RecordingFakeBitbox subclass to capture (chainId, jsonData) without mocking the production signer.
DFXAuthService.getAuthToken on a cold cache stitches three layers together
that have each been a source of production incidents: the HTTP signMessage
fetch, the BitBox personal-message sign, and the auth POST. The per-layer
suites pin each branch in isolation, but only a cross-layer test catches
seam regressions (e.g. a refactor that bypasses the empty-signature guard
on the BitBox path, or one that POSTs /v1/auth before the sign returns).
Covers four arcs end-to-end via MockClient on the HTTP boundary and the
existing FakeBitboxCredentials on the hardware boundary:
- happy path: full sign-then-auth round-trip caches the JWT and the
signature; a second ensureSignatureFor is a no-op (no device re-trigger)
- cancel: FakeBitbox returns empty bytes → SigningCancelledException,
/v1/auth is never POSTed, no JWT cached, lock fires in the finally
- sign-message timeout: FakeBitboxBehavior.timeout + fake_async crossing
the 3-min _signMessageTimeout surfaces a TimeoutException and still
runs lockCurrentWallet (no leaked unlocked-mnemonic state)
- 403 country-blocked: sign succeeds, /v1/auth returns 403 with body,
the message propagates and the JWT cache stays empty (signature does
persist — that's a wallet property, not an auth-response artefact)
The BitBox reconnect arc spans three layers that interact through
side effects: FakeBitboxCredentials flipping behaviour mid-flight,
BitboxService's observer clearing every active credentials reference
on device-loss (without nulling the slot), and SellBitboxCubit
.retryAfterConnection re-checking credentials.isConnected to drop
the BitBox-required gate. Each layer has its own suite, but the
seams between them are where reconnect bugs hide — observers that
orphan the credentials reference, init() that doesn't re-attach
pre-existing credentials, or a cubit that fails to re-evaluate the
gate after the device returns.
Three cross-layer arcs, all inside fakeAsync so the observer's
periodic timer and the cubit's scheduleMicrotask are bound to the
same virtual clock:
- disconnect-mid-sign-retry: a single FakeBitboxCredentials flips
success → disconnect → success while signCallCount keeps climbing,
matching the mid-sign BLE drop arc the real plugin exposes
- observer-detects-device-loss: simulated device vanishes via
SimulatedBitboxPlatform; observer clears bitboxManager and flips
isConnected, but service.getCredentials(addr) still returns the
same instance — the cubit's reference stays valid for recovery
- reattach-after-init: persisted credentials built before pairing,
init() promotes them via setBitbox, retryAfterConnection on
SellBitboxCubit then settles on SellBitboxEthReady without
a faucet hop or a cubit rebuild
Stitches ConnectBitboxCubit together with a real BitboxService driven by the official SimulatedBitboxPlatform so the four BitBox boundary touchpoints — startScan / pair (init) / getChannelHash / confirmPairing — run through production code instead of mocks. The existing unit suite in test/screens/hardware_connect_bitbox/bloc/ exercises the cubit with a _MockBitboxService, so a typo on the SDK method name or a missed init/observer wiring would only surface at pairing time on real hardware. This suite pins the same contract through the simulator and adds an observer-driven device-loss case plus a re-pair-after-disconnect case that the unit suite can't reach without mocking the very behaviour it claims to verify. Downstream WalletService.createBitboxWallet and DFXAuthService.ensureSignatureFor stay mocked — they have their own dedicated suites and a regression there must not surface as a noisy failure at the BitBox boundary.
Stitches WalletService.createBitboxWallet together with a real BitboxService driven by the simulator, a real WalletRepository backed by AppDatabase.forTesting(NativeDatabase.memory()), and a real SettingsRepository on SharedPreferences.setMockInitialValues. The commit path runs end-to-end: simulator getETHAddress → WalletRepository.createViewWallet → WalletStorage.insertWallet (walletInfos row, empty seed column) → SettingsRepository.saveCurrentWalletId The hardware-failure case pins the partial-commit contract: a device-side throw must leave the DB and SharedPreferences untouched, otherwise a half-paired hardware wallet ends up as the next launch's current wallet pointing at an address the device never confirmed. The persistence round-trip cold-load case via a fresh WalletService instance pins that BitBox rows reload as BitboxWallet WITHOUT touching the mnemonic-encryption key — getWalletById must dispatch on WalletType.bitbox and skip the decrypt path entirely. Style anchor: test/packages/repository/wallet_repository_test.dart for the in-memory DB + mock SecureStorage setup, and test/packages/service/wallet_service_test.dart for the createBitboxWallet contract (which the unit suite covers with mocks across every layer).
d4aacf7 to
a6ec281
Compare
60247dd
into
feat/visual-regression-pilot
14 of 16 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Before this PR:
test/integration/had exactly 1 file (kyc_sign_flow_test.dart, 4 tests). Tier 1 — cubit + service + signer cross-layer driven byFakeBitboxCredentials/SimulatedBitboxPlatform— was the documented thin spot of the test pyramid.This PR triples the breadth: 6 new specs, 23 new tests, 100 % green, no production-code touched.
What's in this PR
Three review-gated bundles, all merged into
test/tier1-expansion:Bundle A — Sell + EIP-7702
sell_bitbox_flow_test.dart(5 tests): realSellBitboxCubit+ realRealUnitSellPaymentInfoService+MockClientHTTP boundary +FakeBitboxCredentials. Pins happy / cancel / disconnect / malformed-sig / deposit-retry (SellBitboxDepositRetry state with signedSwap + signedDeposit preserved across post-swap broadcast failure).eip7702_delegation_bitbox_test.dart(4 tests):Eip712Signer.signDelegationthroughFakeBitboxCredentials. Happy + cancel + disconnect + chainId-wiring across mainnet / sepolia / arbitrum.Bundle B — Connect + Wallet-Creation
connect_bitbox_flow_test.dart(4 tests): realConnectBitboxCubit+ realBitboxService+ realBitboxManagerthroughinstallSimulatedBitboxPlatform()at theBitboxUsbPlatform.instanceseam. Happy / pair-rejected / observer-disconnect / re-pair-after-disconnect (P461 chore: typo in *_repository.dart #1 contract —init()re-attaches the SAME credentials instance, soisConnectedflipstrue → false → trueover a single reference).wallet_creation_bitbox_test.dart(3 tests): realWalletService.createBitboxWallet→ realWalletRepository→ Drift in-memory → realBitboxService+ simulator. Happy (schema +verifyNever(getOrCreateMnemonicKey)) / hardware-failure (rollback) / persistence-round-trip (freshWalletServiceon the same DB+Prefs cold-loads without touching the AES-key store).Bundle C — Auth + Reconnect
dfx_auth_sign_ceremony_bitbox_test.dart(4 tests): realDFXAuthService+ realSessionCachethrough the BitBox account boundary. Happy (cold-cache full ceremony) / cancel (no POST,lockCurrentWalletfires) / timeout via fake_async (3-min_signMessageTimeoutcap) / 403 country-blocked propagation.bitbox_reconnect_recovery_test.dart(3 tests): realBitboxServiceobserver +SimulatedBitboxPlatform+ realSellBitboxCubit.retryAfterConnection. Disconnect-mid-sign-retry / observer-device-loss (credentials slot preserved) / reattach-after-init (P461 surface).Coverage impact
Tier 1 tests are cross-layer behaviour pins, not line-coverage drivers — every file the new tests touch is already at 100 % via the wave 1-3 sweep. The value is catching regressions that pass per-layer unit tests but break the seam between them (e.g. cubit catches the wrong exception type, sign-ceremony skips lock on cancel, re-pair returns a fresh credentials instance breaking the P461 contract).
Test plan
flutter analyze— no issuesflutter test test/integration/— 27 tests green (4 existing + 23 new)pull-request.yaml)