Skip to content

feat(test): FakeBitboxCredentials + Phase 1 sign-flow integration tests#320

Merged
TaprootFreak merged 1 commit into
feat/kyc-cubit-unit-testsfrom
feat/fake-bitbox-credentials
May 15, 2026
Merged

feat(test): FakeBitboxCredentials + Phase 1 sign-flow integration tests#320
TaprootFreak merged 1 commit into
feat/kyc-cubit-unit-testsfrom
feat/fake-bitbox-credentials

Conversation

@TaprootFreak
Copy link
Copy Markdown
Contributor

Phase 1 foundation of #314 — stacks on top of #319 (Phase 0).

Base branch is `feat/kyc-cubit-unit-tests`, not `develop`. Will retarget to `develop` once #319 is merged.

Summary

A controllable in-test stand-in for `BitboxCredentials` plus a first set of cross-layer integration tests that exercise the BitBox-gated KYC sign flow end-to-end (no device, no simulator).

`FakeBitboxCredentials extends BitboxCredentials` so every existing `credentials is BitboxCredentials` type guard — most notably the `BitboxNotConnectedException` check in `RealUnitRegistrationService` — treats the fake identically to a real hardware wallet. Behaviour is selected via a `FakeBitboxBehavior` enum that mirrors every observed real-world outcome of the ceremony:

Mode Behaviour Mirrors
`success` Deterministic EIP-712 / personal-message signature from an embedded test private key User confirms on device
`cancel` Returns `'0x'` iOS bridge cancel signal (the bug PR #312 fixed)
`disconnect` Throws `SigningCancelledException`; `isConnected == false` BLE link drop
`timeout` Never resolves; caller imposes its own outer timeout Device hangs
`malformed` Returns non-hex data Frame-desync regression like `bitbox_flutter` PR #11

What's in the PR

File Purpose
`lib/packages/hardware_wallet/fake_bitbox_credentials.dart` The fake + enum
`test/packages/hardware_wallet/fake_bitbox_credentials_test.dart` Unit tests for every mode across `signTypedDataV4` + `signPersonalMessage`, plus the disconnect→success reconnect-flip pattern
`test/integration/kyc_sign_flow_test.dart` Cross-layer tests: fake → `Eip712Signer.signRegistration` → `SigningCancelledException` for happy path, cancel, disconnect, and reconnect-and-retry

What's NOT in the PR (deferred to follow-up Phase 1 PRs)

  • Top-level `integration_test/` directory with the integration_test binding — needs full app boot (DB, secure storage, encryption-key migration); deserves its own PR
  • iOS Simulator CI job (`futureware-tech/simulator-action@v3`) in `pull-request.yaml` — needs separate validation cycle
  • The full 7 PR fix: gate sensitive KYC steps behind BitBox EIP-712 sign #312 scenarios as widget-level integration tests — each is non-trivial; will land one at a time on top of the integration_test scaffold

Test plan

  • `flutter analyze` — clean (only the pre-existing `i18n.dart` generated-code warning)
  • `flutter test` — 242 / 242 passing locally (Phase 0 baseline 223 + 19 new)
  • `dart format` — applied

Phase 1 foundation of #314: a controllable in-test stand-in for
`BitboxCredentials` and a first cross-layer integration test exercising
the BitBox-gated KYC sign flow end-to-end.

`FakeBitboxCredentials extends BitboxCredentials`, so existing
`credentials is BitboxCredentials` type guards (e.g. the
`BitboxNotConnectedException` check in `RealUnitRegistrationService`)
treat the fake identically to a real hardware wallet. The behaviour is
selected via a `FakeBitboxBehavior` enum that mirrors every observed
real-world outcome of the ceremony:

- success     — produces a deterministic EIP-712 / personal-message
                signature from an embedded test private key
- cancel      — returns '0x' (matches the iOS bridge cancel signal)
- disconnect  — throws SigningCancelledException; isConnected == false
- timeout     — never resolves; caller imposes its own outer timeout
- malformed   — returns non-hex data (simulates frame-desync regressions
                like bitbox_flutter PR #11)

Unit tests cover every behaviour mode across `signTypedDataV4` and
`signPersonalMessage`, including the disconnect-flip-to-success retry
pattern.

Cross-layer integration tests live under `test/integration/` (headless,
runs as part of `flutter test`):
- happy path: fake → Eip712Signer.signRegistration → 65-byte sig
- cancel mid-sign: fake "0x" → Eip712Signer guard → SigningCancelledException
- BLE disconnect: SigningCancelledException at the credentials boundary
- reconnect-and-retry: flipped fake completes on the second attempt

The top-level `integration_test/` directory is intentionally deferred:
scenarios that need full app boot, BLE / USB channels, or the
integration_test binding will land there as a follow-up.

Stacks on top of feat/kyc-cubit-unit-tests (Phase 0). 242 / 242 tests
green; flutter analyze clean.
@TaprootFreak TaprootFreak marked this pull request as ready for review May 13, 2026 12:52
TaprootFreak added a commit that referenced this pull request May 15, 2026
)

## Summary
Adds 38 unit tests across six previously-untested files in
`lib/packages/utils/` and `lib/packages/wallet/`. Each spec lives in the
mirror path under `test/` per the project convention.

| File under test | Test file | Cases |
| --- | --- | --- |
| `lib/packages/utils/fast_hash.dart` |
`test/packages/utils/fast_hash_test.dart` | 5 |
| `lib/packages/utils/jwt_decoder.dart` |
`test/packages/utils/jwt_decoder_test.dart` | 7 |
| `lib/packages/wallet/payment_uri.dart` |
`test/packages/wallet/payment_uri_test.dart` | 4 |
| `lib/packages/wallet/wallet_account.dart` |
`test/packages/wallet/wallet_account_test.dart` | 7 |
| `lib/packages/wallet/wallet.dart` |
`test/packages/wallet/wallet_test.dart` | 11 |
| `lib/packages/wallet/eip7702_signer.dart` |
`test/packages/wallet/eip7702_signer_test.dart` | 4 |

## What each file covers
- **fast_hash:** FNV-1a determinism, distinct inputs differ, ordering
matters, unicode safety, empty-string offset basis.
- **jwt_decoder:** well-formed payload parsing, segment-count errors,
non-map payload rejection, all three valid base64url padding lengths,
illegal length-mod-4 rejection.
- **payment_uri:** empty-amount short form, dotted and comma-locale
amounts, preservation of decimal precision.
- **wallet_account:** BIP-44 derivation path format, deterministic
Hardhat account #0 address from the standard test mnemonic, distinct
indices, signMessage shape + determinism + sensitivity to addressIndex.
- **wallet:** `SoftwareWallet` walletType + primary/current account
identity + `selectAccount` semantics + id/name mutability; `DebugWallet`
sign refusal.
- **eip7702_signer:** hardware-credential refusal (BitBox cannot sign
EIP-7702), software-credential signing, signature determinism,
nonce-sensitivity.

## Why
Stage 2 of the coverage push tracked in the README features matrix
([#322](#322)). These six
files are pure-Dart with no platform dependencies, so they were the
cheapest meaningful coverage win available — no service mocks, no widget
rendering, no merge-conflict risk with the three in-flight test PRs
([#319](#319),
[#320](#320),
[#321](#321)).

## Test plan
- [x] `flutter analyze` clean on all six new test files (locally on
Flutter 3.38.5)
- [x] `flutter test test/packages/utils/ test/packages/wallet/` — 38 /
38 passing
- [ ] CI green on this branch
- [ ] Spot-check: coverage artifact from #323 (once that lands) shows
non-zero coverage on the six files above
@TaprootFreak TaprootFreak merged commit 117ca68 into feat/kyc-cubit-unit-tests May 15, 2026
@TaprootFreak TaprootFreak deleted the feat/fake-bitbox-credentials branch May 15, 2026 07:22
TaprootFreak added a commit that referenced this pull request May 15, 2026
## Summary
Replace the bare README with a full project-overview document modelled
on the `zk-coins/app` README. Adds:

1. **Contributing rule:** 100% test coverage on the activated surface is
required to merge into `develop`. Defensive code is exempted via `//
coverage:ignore-*`. Branch protection enforces.
2. **Coverage scope:** `lib/packages/**` + every `cubits/`/`bloc/`
directory under `lib/screens/<feature>/`. Widget files render-tested via
`testWidgets` only.
3. **Coverage infrastructure roadmap:** honest list of what still needs
to land before the rule is enforceable (CI `--coverage` step, lcov
threshold gate, branch protection, build-time feature flags, inline
ignore annotations).
4. **Features matrix:** every user-facing function, its activation
status, the triage decision (`mvp` / `defer` / `planned`), and the tests
that currently cover it.
5. **Triage gaps:** explicit list of `mvp` features still below 100%,
with links to the in-flight PRs
([#319](#319),
[#320](#320),
[#321](#321)) that close
them.
6. **Testing tiers:** ties the matrix to the 4-tier model in
[#314](#314).
7. Original **Getting started** section preserved at the end.

## Why
The repo has no source of truth for "what does this wallet actually do"
or "what is its test-coverage commitment". Tests in `test/` exist but
are unbound to features — a reviewer cannot tell at a glance whether a
feature added in a PR has the required coverage. This document closes
that gap.

The coverage rule deliberately mirrors `zk-coins/app` exactly (same
wording, same scope shape) so that reviewers familiar with one project
transfer their expectations to the other. The 4-tier testing model is
owned by [#314](#314);
this README references it but does not duplicate it.

## What this PR is _not_
- It does **not** wire `flutter test --coverage` into CI. That is item 1
of the roadmap and will land in a follow-up.
- It does **not** add branch protection. That is item 3 of the roadmap.
- It does **not** introduce build-time feature flags. That is item 4 of
the roadmap and is a prerequisite for the rule being realistic across
all features.
- It does **not** add any new tests — the in-flight test PRs
([#319](#319),
[#320](#320),
[#321](#321)) own that
work.

Until the roadmap items land, the rule is aspirational, explicitly noted
as such in the README, and not yet a merge blocker.

## Test plan
- [ ] Visual review of the rendered README on GitHub (tables align, all
links resolve)
- [ ] Confirm every `widget`-tagged row points at an existing
`*_test.dart` file in `test/`
- [ ] Confirm every "in flight" reference points at an open PR
- [ ] Confirm "Coverage scope" wording matches what we want to measure
once the lcov gate lands
TaprootFreak added a commit that referenced this pull request May 15, 2026
## Summary
Stage 3 of the coverage push. Adds 38 unit tests across four
previously-untested service files in \`lib/packages/service/\` that have
no DFX backend coupling. \`mocktail\` mocks for the repository / bitbox
dependencies; no real I/O.

| Service | Test file | Cases |
| --- | --- | --- |
| \`session_cache.dart\` |
\`test/packages/service/session_cache_test.dart\` | 9 |
| \`settings_service.dart\` |
\`test/packages/service/settings_service_test.dart\` | 6 |
| \`app_store.dart\` | \`test/packages/service/app_store_test.dart\` | 6
|
| \`wallet_service.dart\` |
\`test/packages/service/wallet_service_test.dart\` | 17 |

## What each file covers
- **session_cache:** auth-token in-memory-only lifecycle (no disk
write); signature/address persistence through \`CacheRepository\`;
\`loadSignature\` preserves in-memory state when already set; tolerates
missing repo entries; \`clear\` wipes both keys, auth token, and memory.
- **settings_service:** terms / software-terms read + write delegation;
the two flags are independent.
- **app_store:** wallet-not-set throws; primaryAddress derived through
current account; selectAccount-driven address change; \`apiConfig\`
getter re-evaluates on every access (network mode can switch at
runtime); sessionCache passthrough.
- **wallet_service:** \`createSeedWallet\` (bip39 generation +
persistence, does NOT mark current); \`restoreWallet\` (seed persistence
+ marks current); \`createDebugWallet\` (view-wallet path);
\`getWalletById\` (software / debug branches + null-repo error);
\`setCurrentWallet\`; \`getCurrentWallet\` (resolves through id);
\`deleteCurrentWallet\`; \`hasWallet\` (both branches); \`validateSeed\`
(valid / invalid / empty / wrong-checksum).

## Excluded (with reasons)
- **biometric_service.dart** — the \`LocalAuthentication\` instance is
constructed in the field initializer with no seam. Testing requires
either a prod refactor (out of scope here) or platform-channel test
plumbing. Flagged in the README features matrix as a triage gap.
- **transaction_history_service.dart** — hits \`app_store.httpClient\`
(a final \`http.Client\` field) which is not currently injectable. Same
constraint as the DFX services.
- **price_service.dart** — abstract interface only; no implementation to
test yet.
- **wallet_service \`createBitboxWallet\` / getWalletById bitbox
branch** — belongs to Tier 1 with \`FakeBitboxCredentials\` (now landed
via #319/#320). Will be covered in a follow-up.

## Test plan
- [x] \`flutter analyze\` clean (locally on Flutter 3.38.5)
- [x] \`flutter test test/packages/service/\` — 38 / 38 passing on these
new files
- [ ] CI green
TaprootFreak added a commit that referenced this pull request May 15, 2026
## Summary
Stage 4 of the coverage push. Adds bloc_test specs for five screen-level
cubits/blocs that previously only had widget-level coverage. All run in
pure Dart (no widget pumping); mocktail for the service / repository /
SoftwareWallet boundaries.

| Cubit | Test file | Cases |
| --- | --- | --- |
| \`legal_disclaimer_cubit.dart\` |
\`test/screens/legal/cubit/legal_disclaimer_cubit_test.dart\` | 7 |
| \`restore_wallet/cubit/validate_seed/validate_seed_cubit.dart\` |
\`test/screens/restore_wallet/cubit/validate_seed_cubit_test.dart\` | 7
|
|
\`transaction_history/cubits/filter/transaction_history_filter_cubit.dart\`
|
\`test/screens/transaction_history/cubits/transaction_history_filter_cubit_test.dart\`
| 6 |
| \`verify_seed/cubit/verify_seed_cubit.dart\` |
\`test/screens/verify_seed/cubit/verify_seed_cubit_test.dart\` | 6 |
| \`pin/bloc/setup_pin/setup_pin_cubit.dart\` |
\`test/screens/pin/setup_pin_cubit_test.dart\` | 11 |

## What each file covers

- **legal_disclaimer_cubit:** initial step-0 state, nextStep advance,
full walk to last step, onComplete callback fires on last step (no
emit), no-op without callback on last step, previousStep moves back,
no-op at step 0.
- **validate_seed_cubit:** initial uncomplete; \`checkSeedLength\` for
12 valid words / fewer-than-12 / 12 words with an out-of-wordlist token
/ extra inner whitespace tolerated; \`validateSeed\` delegates to
\`WalletService\` for both branches.
- **transaction_history_filter_cubit:** subscribes to the repo stream
with the configured asset + address, default 1-year-back \`startDate\`,
stream pushes populate \`all\` + \`filtered\`, date-window filter
narrows \`filtered\` without touching \`all\`, boundaries are inclusive,
filter re-applies on subsequent stream emissions.
- **verify_seed_cubit:** 4 random ascending word indices within seed
length, debug-mode pre-fill, \`canVerify\` reflects all four slots
filled, \`updateWord\` trims + lowercases + clears \`hasError\`,
\`verify\` returns true and marks the wallet current on match,
\`verify\` returns false / flags \`hasError\` / does NOT mark current on
mismatch.
- **setup_pin_cubit:** initial state, \`addDigit\` appends / 6-digit
cap, \`deleteDigit\` / no-op on empty, create→confirm transition on 6th
digit, matching confirm-pin persists salt + hash and emits
\`isComplete\` (exercises real PBKDF2 via \`compute()\`), mismatching
confirm-pin resets + flags \`mismatch\`, \`reset\` returns to initial,
\`isBiometricAvailable\` + \`enableBiometrics\` passthrough to
\`BiometricService\`.

## Notes
- The \`setup_pin_cubit\` matching-pin test runs a real **600k-iteration
PBKDF2** through \`compute()\`. On the Flutter-test isolate shim this
takes ~12 s on a Mac Studio. The test uses a 30 s timeout to keep CI
honest without flaking.
- \`SecureStorage.setPinSalt\` takes a \`Uint8List\`, which is a
restricted type — a \`Fake\` subclass is illegal, so the mocktail
fallback is registered with a real \`Uint8List(0)\` instance instead.

## Excluded (and why)
- \`pin/bloc/verify_pin/verify_pin_cubit.dart\` — talks to
\`SecureStorage.verifyPin\` which already exercises PBKDF2. Adding a
full happy-path test would compound the slow-test cost. Will be covered
in a follow-up.
- \`restore_wallet/cubit/restore_wallet_cubit.dart\` — coupled to
\`WalletService.restoreWallet\` (already covered in
\`wallet_service_test.dart\`) plus screen navigation; small marginal
value here, fits better in a Tier 1 integration test.
- Buy / sell / KYC cubits — owned by other recent PRs (#312/#319/#321
area) and would invite merge conflicts.
- Bitbox-coupled cubits (\`sell_bitbox\`, \`hardware_connect_bitbox\`) —
belong with Tier 1 \`FakeBitboxCredentials\` (now landed via #319/#320)
in a follow-up.

## Test plan
- [x] \`flutter analyze test/screens/\` clean
- [x] \`flutter test\` on all 5 new files — 37 / 37 passing (~8 s total
locally)
- [ ] CI green
TaprootFreak added a commit that referenced this pull request May 15, 2026
Closes Acceptance Criterion #6 of #314 — _\"Documentation:
\`docs/testing.md\` explains when to use which tier, with examples.\"_

## Summary

A single page that captures everything we've learned across Phase 0
(#319) and Phase 1 Foundation (#320 / #321) about how to test
BitBox-touching code without re-deriving the rules every PR:

- **Tier matrix** (0–4) with hardware / CI status for each, so reviewers
can tell at a glance what a test does and doesn't prove
- **Decision tree** for picking a tier — \"if a Tier 0 test would have
to mock the very thing under test, drop down a tier\"
- **Tier 0 patterns** with concrete excerpts from real test files:
- Cubit / Bloc tests (\`bloc_test\` + \`mocktail.Mock\`) pointing at
\`test/screens/kyc/cubits/kyc/kyc_cubit_test.dart\`
- Widget tests (\`pumpApp\` + \`MockCubit\`) pointing at
\`test/screens/kyc/steps/kyc_email_page_test.dart\`
- Service + HTTP tests (\`http/testing\` \`MockClient\` +
\`_MockAppStore\`) pointing at
\`test/packages/service/dfx/dfx_bank_account_service_test.dart\`,
including the **\`setAuthToken('test-jwt')\` pre-seed** trick for
DFXAuthService-derived services (the gotcha that broke CI on #321)
- **Tier 1 patterns** for \`FakeBitboxCredentials\` with the
\`FakeBitboxBehavior\` matrix and the disconnect-flip-to-success
reconnect pattern, pointing at
\`test/integration/kyc_sign_flow_test.dart\`
- **Tiers 2–4** marked deferred with status pointers back to the
corresponding phase of #314
- **Mocktail gotchas** (\`Future\` + \`thenAnswer\`,
\`registerFallbackValue\`, private mocks)
- **Add-tests checklist** for PRs touching \`KycCubit\` /
\`Eip712Signer\` / \`DFXAuthService\` / \`BitboxCredentials\` /
\`bitbox_flutter\`

CONTRIBUTING.md's Testing section gets a one-line cross-link.

## Test plan

- [x] Doc compiles / renders (no broken intra-repo links in the
markdown)
- [x] Every code example matches a real file already on \`develop\`
- [x] \`flutter analyze\` — no new issues (4 pre-existing errors in
\`test/screens/home/home_bloc_test.dart\` are unrelated to this PR;
verified they exist on \`develop\` too)
TaprootFreak added a commit that referenced this pull request May 23, 2026
- fake_async example: add bitbox_flutter.dart import and inline the
  pairedServiceSync idiom so the snippet is copy-paste compilable
- platform-coupled section: real-plugin counterpart is Tier 2/3, not
  Tier 1/2 (Tier 1 is FakeBitboxCredentials by definition); add the
  CONTRIBUTING.md footnote-165 caveat that the annotation is the
  documenting form today
- README: PRs #319/#320/#321/#322/#323 are all merged since
  2026-05-15; replace "in flight"/"landing in"/"extended in" with
  "added via" / "have closed" / "partially covered after"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant