Skip to content

test+fix: codebase audit — 48 tests, 10 emit-after-close fixes, DB testability#525

Merged
TaprootFreak merged 4 commits into
developfrom
test/audit-phase-1-to-4
May 23, 2026
Merged

test+fix: codebase audit — 48 tests, 10 emit-after-close fixes, DB testability#525
TaprootFreak merged 4 commits into
developfrom
test/audit-phase-1-to-4

Conversation

@davidleomay
Copy link
Copy Markdown
Contributor

@davidleomay davidleomay commented May 22, 2026

Summary

Implements the high-priority items from the test engineering audit (#506).

4 commits:

  • test: add 45 tests from codebase audit (#506) — regression guards and bug-proving tests
  • fix: add isClosed guards to 8 cubits to prevent emit-after-close — fixes the 8 bugs those tests exposed
  • fix: cancel subscription in FilterCubit + isClosed guards in BuyConverterCubit — 2 more cubits
  • test: malformed JSON response tests for remaining 5 DFX services — completes Phase 3

Rebase note

This branch is 24 commits behind develop. Rebase has merge conflicts in buy_payment_info_cubit_test.dart and sell_payment_info_cubit_test.dart because develop refactored those cubits (removed DFXPriceService dependency, moved min-amount validation to API). The conflicts are in test files only — the resolution is: take develop's test structure and re-add the 3 new tests per file (BitboxNotConnected, emit-after-close, negative amount/comma).

Bug fixes (10 cubits)

Added if (isClosed) return; after every await in async methods that call emit(). Without these guards, navigating away from a screen while an HTTP request is in-flight throws StateError: Cannot emit new states after calling close.

Cubit Trigger
SellConfirmCubit User swipes modal during payment confirmation
SellBankAccountsCubit User taps back before bank accounts load
SellPaymentInfoCubit User navigates away during payment info fetch
SellConverterCubit Debounce timer fires after screen disposal
BuyPaymentInfoCubit User taps back during payment info fetch
BuyConverterCubit Same debounce pattern as sell
TransactionHistoryMultiReceiptCubit User leaves during PDF generation
TransactionHistoryReceiptCubit User scrolls away during receipt download
SettingsTaxReportCubit User navigates away during tax report generation (5-30s)
TransactionHistoryFilterCubit Stream subscription leaked on close (now stored + cancelled)

Source change (non-behavioral)

  • AppDatabase.forTesting(QueryExecutor)@visibleForTesting constructor for in-memory DB tests. Zero production impact.

Tests added (53 total)

Category Count Purpose
Emit-after-close 10 Proves/guards isClosed bugs
Crypto regression 1 Non-ASCII signing (#289)
Equality regression 3 BuyPaymentInfo field equality (#207)
BitboxNotConnectedException 1 Missing exception path in BuyPaymentInfoCubit
DashboardBloc error survival 3 Unhandled exceptions in 3 event handlers
Financial boundaries 4 Negative amounts, comma normalization, Infinity/NaN
JSON parsing (malformed responses) 13 All 11 DFX services + balance + registration
SecureStorage corruption 2 Missing colon separator, empty ciphertext
DB schema/migration 4 Table creation, wallet insert, FK integrity
Parse fixed edge cases 5 Empty, multi-dot, dot, negative, zero
Settings/URI pins 2 hideAmounts session-only, PaymentURI encoding
Wallet persistence failure 2 Repository exception during create/restore
Filter subscription cleanup 1 Stream subscription cancelled on close
BuyConverterCubit close 1 Debounce timer + isClosed

Remaining from #506 (not in this PR)

Phase 4.1-4.3: Storage atomicity (separate PR needed)

These are real bugs but change database behavior and need careful review:

  • deleteWallet doesn't cascade — only deletes walletAccountInfos, not walletInfos (encrypted seed persists). Already tracked as WalletStorage.deleteWallet leaves orphan walletInfos rows (encrypted seeds never deleted) #498.
  • insertDfxTransaction non-atomic — two separate inserts without a Drift transaction() wrapper. If the DFX details insert fails, the Transaction row is committed as an orphan.
  • saveBalance/saveAsset TOCTOU races — check-then-act pattern without atomicity. Needs INSERT OR REPLACE or Drift upsert.

Phase 2: Security design decisions (need discussion)

  • EIP-712 signRegistration() domain missing chainId — unlike signDelegation() which includes it. Registration signatures are theoretically replayable across chains. Needs backend coordination.
  • WebView accepts any URI schemejavascript:, data:, file:// pass through without validation. Currently unreachable (both call sites hardcode empty amount), but should be guarded.

Phase 6: Integration tests (separate scope)

5 cross-layer flows that require full DI container setup:

  1. PIN verify -> wallet load -> dashboard (regression for f9b89ea)
  2. Create wallet -> background -> resume -> seed cleared (regression for feat(lifecycle): drop mnemonic on app-hidden #485, Onboarding mnemonic in CreateWalletState.wallet is not dropped on app-hidden #489)
  3. Buy flow: switch currency -> payment details update (regression for fix: update BuyPaymentInfo equality to detect IBAN changes #207)
  4. KYC flow: existing DFX customer merge (regression for fix(kyc): existing DFX customer merge — close misroute, surface failures #466)
  5. BitBox disconnect mid-sign -> reconnect -> retry (regression for fix(bitbox): recover from BLE/USB disconnect without losing wallet session #461)

Test plan

  • All 53 new tests pass (emit-after-close tests pass after isClosed fix)
  • All existing tests pass (10 pre-existing loading failures unchanged)
  • flutter analyze clean on changed source files
  • No behavioral change to production code (only isClosed guards + subscription cleanup)

Closes #506

Emit-after-close tests for 8 cubits (all fail — proving missing
isClosed guards): SellConfirmCubit, SellBankAccountsCubit,
SellPaymentInfoCubit, SellConverterCubit, BuyPaymentInfoCubit,
TransactionHistoryMultiReceiptCubit, TransactionHistoryReceiptCubit,
SettingsTaxReportCubit.

Regression guards:
- Non-ASCII signing (wallet_account, regression for #289)
- BuyPaymentInfo equality across all fields (regression for #207)
- BuyPaymentInfoCubit BitboxNotConnectedException path
- DashboardBloc error survival on service failure (3 handlers)
- Sell cubit: negative amount passthrough, comma normalization gap
- DfxBrokerbotService: Infinity/NaN input, malformed JSON (4 methods)
- BalanceService, BuyPaymentInfoService, RegistrationService: malformed JSON
- SecureStorage: corrupted ciphertext (missing colon, empty string)
- parseFixed edge cases (empty, multi-dot, dot, zero)
- PaymentURI encoding pin, SettingsBloc hideAmounts session-only pin
- WalletService persistence failure resilience (2 tests)
- AppDatabase: @VisibleForTesting constructor + schema/migration tests
Adds `if (isClosed) return;` after every `await` in async methods
that call `emit()`. Without these guards, navigating away from a
screen while an HTTP request is in-flight throws `StateError:
Cannot emit new states after calling close`.

Fixed cubits:
- SellConfirmCubit
- SellBankAccountsCubit (add, deactivate, _loadBankAccounts)
- SellPaymentInfoCubit (getPaymentInfo)
- SellConverterCubit (onFiatChanged, onSharesChanged, onCurrencyChanged)
- BuyPaymentInfoCubit (getPaymentInfo)
- TransactionHistoryMultiReceiptCubit (generateReceipt)
- TransactionHistoryReceiptCubit (generateReceipt)
- SettingsTaxReportCubit (generateTaxReport)
…rterCubit

TransactionHistoryFilterCubit: store the stream subscription and
cancel it in close(). Previously the subscription leaked, causing
potential emit-after-close on screen disposal.

BuyConverterCubit: add isClosed guards in all three Timer callbacks
and onCurrencyChanged (same pattern as the SellConverterCubit fix in
the previous commit).

Tests added:
- FilterCubit: close cancels subscription (no emit after close)
- BuyConverterCubit: does not emit after close
- SellPaymentInfoService: malformed JSON response
Covers getUser (kyc), getTickets (support), getBalanceReport (pdf),
getPortfolioHistory (account), getBankAccounts (bank account).
Same pattern as the brokerbot/balance/buy/registration/sell tests
from the first commit — verifies FormatException on non-JSON 200.
@davidleomay davidleomay force-pushed the test/audit-phase-1-to-4 branch from 58178be to 25200f0 Compare May 22, 2026 14:30
@TaprootFreak TaprootFreak merged commit bf06eb9 into develop May 23, 2026
5 checks passed
@TaprootFreak TaprootFreak deleted the test/audit-phase-1-to-4 branch May 23, 2026 09:20
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.

Test engineering audit: 6-phase plan to close remaining coverage gaps

2 participants