Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions integration_test/bip85_derivation_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
import 'package:bb_mobile/core/wallet/domain/usecases/create_default_wallets_usecase.dart';
import 'package:bb_mobile/locator.dart';
import 'package:bb_mobile/main.dart';
import 'support/integration_test_profile.dart';
import 'package:bip39_mnemonic/bip39_mnemonic.dart' as bip39;
import 'package:bip39_mnemonic/bip39_mnemonic.dart';
import 'package:bip85_entropy/bip85_entropy.dart' as bip85;
import 'package:flutter_test/flutter_test.dart';

Future<void> main({bool isInitialized = false}) async {
TestWidgetsFlutterBinding.ensureInitialized();
if (!isInitialized) await Bull.init();
await initializeIntegrationTestApp(isInitialized: isInitialized);

final sqlite = locator<SqliteDatabase>();
final seedRepository = locator<SeedRepository>();
Expand Down
4 changes: 2 additions & 2 deletions integration_test/coins_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import 'package:bb_mobile/core/wallet/domain/repositories/wallet_utxo_repository
import 'package:bb_mobile/core/wallet/domain/usecases/prepare_bitcoin_send_usecase.dart';
import 'package:bb_mobile/features/settings/domain/usecases/set_environment_usecase.dart';
import 'package:bb_mobile/locator.dart';
import 'package:bb_mobile/main.dart';
import 'support/integration_test_profile.dart';
import 'package:drift/drift.dart' show driftRuntimeOptions;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
Expand All @@ -36,7 +36,7 @@ import 'package:flutter_test/flutter_test.dart';
// Run via `make integration-test` (auto-aggregated by tools/gen_all_test.dart).
Future<void> main({bool isInitialized = false}) async {
TestWidgetsFlutterBinding.ensureInitialized();
if (!isInitialized) await Bull.init();
await initializeIntegrationTestApp(isInitialized: isInitialized);

final frozenDatasource = locator<FrozenWalletUtxoDatasource>();
final utxoRepository = locator<WalletUtxoRepository>();
Expand Down
4 changes: 2 additions & 2 deletions integration_test/exchange_rate_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import 'package:bb_mobile/core/exchange/domain/usecases/convert_sats_to_currency
import 'package:bb_mobile/core/exchange/domain/usecases/get_available_currencies_usecase.dart';
import 'package:bb_mobile/core/utils/constants.dart';
import 'package:bb_mobile/locator.dart';
import 'package:bb_mobile/main.dart';
import 'support/integration_test_profile.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

Future<void> main({bool isInitialized = false}) async {
TestWidgetsFlutterBinding.ensureInitialized();
if (!isInitialized) await Bull.init();
await initializeIntegrationTestApp(isInitialized: isInitialized);

final bitcoinPriceDatasource = locator<BullbitcoinApiDatasource>(
instanceName: 'mainnetExchangeApiDatasource',
Expand Down
4 changes: 2 additions & 2 deletions integration_test/payjoin_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ import 'package:bb_mobile/core/wallet/domain/usecases/prepare_bitcoin_send_useca
import 'package:bb_mobile/features/send/domain/usecases/sign_bitcoin_tx_usecase.dart';
import 'package:bb_mobile/features/settings/domain/usecases/set_environment_usecase.dart';
import 'package:bb_mobile/locator.dart';
import 'package:bb_mobile/main.dart';
import 'support/integration_test_profile.dart';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart' show TestWidgetsFlutterBinding;
import 'package:test/test.dart';

Future<void> main({bool isInitialized = false}) async {
TestWidgetsFlutterBinding.ensureInitialized();
if (!isInitialized) await Bull.init();
await initializeIntegrationTestApp(isInitialized: isInitialized);

late Wallet receiverWallet;
late Wallet senderWallet;
Expand Down
4 changes: 2 additions & 2 deletions integration_test/payment_request_test.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import 'package:bb_mobile/core/utils/payment_request.dart';
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
import 'package:bb_mobile/main.dart';
import 'support/integration_test_profile.dart';
import 'package:flutter_test/flutter_test.dart';

Future<void> main({bool isInitialized = false}) async {
TestWidgetsFlutterBinding.ensureInitialized();
if (!isInitialized) await Bull.init();
await initializeIntegrationTestApp(isInitialized: isInitialized);

const btcAddress = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq';

Expand Down
21 changes: 14 additions & 7 deletions integration_test/recoverbull_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import 'package:bb_mobile/core/utils/recoverbull_bip85.dart';
import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
import 'package:bb_mobile/locator.dart';
import 'package:bb_mobile/main.dart';
import 'support/integration_test_profile.dart';
import 'package:bip39_mnemonic/bip39_mnemonic.dart' as bip39;
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';

Future<void> main({bool isInitialized = false}) async {
TestWidgetsFlutterBinding.ensureInitialized();
if (!isInitialized) await Bull.init();
await initializeIntegrationTestApp(isInitialized: isInitialized);

final initializeTorUsecase = locator<InitTorUsecase>();
final restoreVaultUsecase = locator<RestoreVaultUsecase>();
Expand Down Expand Up @@ -106,16 +106,23 @@ Future<void> main({bool isInitialized = false}) async {
final restored = await restoreVaultUsecase.execute(
decryptedVault: decryptedVault,
);
expect(restored, isA<Ok<Null, RecoverBullCoreFailure>>());
expect(restored, isA<Ok<List<String>, RecoverBullCoreFailure>>());
final restoredWalletIds =
(restored as Ok<List<String>, RecoverBullCoreFailure>).value;

final wallets = await walletRepository.getWallets(
final defaultWallets = await walletRepository.getWallets(
onlyDefaults: true,
onlyBitcoin: true,
environment: Environment.mainnet,
);
expect(defaultWallets, hasLength(2));
expect(
restoredWalletIds.toSet(),
defaultWallets.map((wallet) => wallet.id).toSet(),
);

expect(wallets.length, 1);
final wallet = wallets.first;
final wallet = defaultWallets.singleWhere(
(wallet) => wallet.network.isBitcoin,
);
expect(wallet.masterFingerprint, isNotEmpty);
final seed = await seedRepository.get(wallet.masterFingerprint);
final seedModel = SeedModel.fromEntity(seed);
Expand Down
4 changes: 2 additions & 2 deletions integration_test/sqlite_transactions_test.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import 'package:bb_mobile/core/storage/sqlite_database.dart';
import 'package:bb_mobile/core/transactions/domain/transaction_port.dart';
import 'package:bb_mobile/locator.dart';
import 'package:bb_mobile/main.dart';
import 'support/integration_test_profile.dart';
import 'package:flutter_test/flutter_test.dart';

Future<void> main({bool isInitialized = false}) async {
TestWidgetsFlutterBinding.ensureInitialized();
if (!isInitialized) await Bull.init();
await initializeIntegrationTestApp(isInitialized: isInitialized);

final sqlite = locator<SqliteDatabase>();
final transactionPort = locator<TransactionPort>();
Expand Down
4 changes: 2 additions & 2 deletions integration_test/sqlite_wallet_metadata_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import 'package:bb_mobile/core/wallet/data/models/wallet_metadata_model.dart';
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
import 'package:bb_mobile/core/wallet/wallet_metadata_service.dart';
import 'package:bb_mobile/locator.dart';
import 'package:bb_mobile/main.dart';
import 'support/integration_test_profile.dart';
import 'package:flutter_test/flutter_test.dart';

Future<void> main({bool isInitialized = false}) async {
TestWidgetsFlutterBinding.ensureInitialized();
if (!isInitialized) await Bull.init();
await initializeIntegrationTestApp(isInitialized: isInitialized);

final sqlite = locator<SqliteDatabase>();

Expand Down
80 changes: 80 additions & 0 deletions integration_test/support/get_paid_test_harness.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import 'package:bb_mobile/core/storage/data/datasources/key_value_storage/key_value_storage_datasource.dart';
import 'package:bb_mobile/core/storage/app_data_directory.dart';
import 'package:bb_mobile/core/storage/sqlite_database.dart';
import 'package:bb_mobile/core/utils/constants.dart';
import 'package:bb_mobile/core/wallet/data/datasources/wallet_metadata_datasource.dart';
import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
import 'package:bb_mobile/features/bullnym/domain/bullnym_client_port.dart';
import 'package:bb_mobile/features/invoices/application/usecases/create_invoice_usecase.dart';
import 'package:bb_mobile/features/wallet_backup/domain/repositories/wallet_backup_remote_repository.dart';
import 'package:bb_mobile/features/wallet_backup/watchers/wallet_backup_coordinator.dart';
import 'package:bb_mobile/locator.dart';

import 'fake_bullnym_client.dart';

/// Replaces the Bullnym transport after [Bull.init] without leaving any
/// long-lived consumer bound to the previous client.
///
/// In particular, the wallet-backup remote repository and the invoice-create
/// use case are lazy singletons. Resetting only [BullnymClientPort] would leave
/// those objects using the transport they captured when first constructed.
Future<void> installFakeBullnymClient(FakeBullnymClient fake) async {
await locator.resetLazySingleton<WalletBackupCoordinator>();
await locator.resetLazySingleton<WalletBackupRemoteRepository>();
await locator.resetLazySingleton<CreateInvoiceUsecase>();

await locator.unregister<BullnymClientPort>();
locator.registerLazySingleton<BullnymClientPort>(() => fake);

locator<WalletBackupCoordinator>().start();
}

/// Removes the local state owned by Get Paid recovery journeys while leaving
/// the fake Bullnym server untouched.
///
/// Child rows are removed before their manifest parents. Settings and network
/// configuration deliberately survive so the scenario keeps the environment
/// selected by the integration runner. The backup coordinator deliberately
/// remains stopped: restarting it here could race the caller's explicit
/// fresh-install bootstrap with automatic recovery from the preserved fake
/// remote backup.
Future<void> wipeGetPaidLocalState() async {
if (!AppDataDirectory.isIsolatedTestProfile) {
throw StateError(
'Refusing destructive integration cleanup outside an isolated profile',
);
}

await locator.resetLazySingleton<WalletBackupCoordinator>();

final database = locator<SqliteDatabase>();
final walletMetadata = locator<WalletMetadataDatasource>();
final walletRepository = locator<WalletRepository>();
final walletIds = (await walletMetadata.fetchAll())
.map((wallet) => wallet.id)
.toList(growable: false);

await database.transaction(() async {
await database.managers.keychainManifestNostrKeys.delete();
await database.managers.keychainManifestWalletBindings.delete();
await database.managers.keychainManifestEntries.delete();
await database.managers.walletBackupStates.delete();
await database.managers.transactions.delete();
await database.managers.labels.delete();
await database.managers.frozenUtxos.delete();
await database.managers.bip85Derivations.delete();
});

// Removing only the Drift metadata leaves the native BDK/LWK wallet files
// behind. A deterministic re-create can then collide with those files and
// silently leave the next scenario without both default wallets. Exercise
// the real repository deletion path so the database and native stores move
// together.
for (final walletId in walletIds) {
await walletRepository.deleteWallet(walletId: walletId);
}

await locator<KeyValueStorageDatasource<String>>(
instanceName: LocatorInstanceNameConstants.secureStorageDatasource,
).deleteAll();
}
18 changes: 18 additions & 0 deletions integration_test/support/integration_test_profile.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:bb_mobile/core/storage/app_data_directory.dart';
import 'package:bb_mobile/main.dart';

/// Fails before application initialization unless the test process owns a
/// disposable profile.
///
/// This guard is shared by the generated aggregate runner and direct IDE/test
/// invocations. It prevents either path from opening a developer's production
/// database, native wallet files, or secure-storage namespace.
Future<void> initializeIntegrationTestApp({required bool isInitialized}) async {
if (!AppDataDirectory.isIsolatedTestProfile) {
throw StateError(
'Integration tests require BULL_INTEGRATION_DATA_DIR; '
'run `make integration-test`',
);
}
if (!isInitialized) await Bull.init();
}
31 changes: 12 additions & 19 deletions lib/core/exchange/domain/entity/order.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ part 'order.freezed.dart';
enum FiatCurrency {
usd('USD', decimals: 2, symbol: '\$'),
cad('CAD', decimals: 2, symbol: '\$'),
crc('CRC', decimals: 2, symbol: '₡'),
crc('CRC', decimals: 0, symbol: '₡'),
eur('EUR', decimals: 2, symbol: '€'),
mxn('MXN', decimals: 2, symbol: '\$'),
ars('ARS', decimals: 2, symbol: '\$'),
Expand All @@ -17,25 +17,18 @@ enum FiatCurrency {
final int decimals;
final String symbol;

static FiatCurrency fromCode(String code) {
switch (code.toUpperCase()) {
case 'USD':
return FiatCurrency.usd;
case 'CAD':
return FiatCurrency.cad;
case 'CRC':
return FiatCurrency.crc;
case 'EUR':
return FiatCurrency.eur;
case 'MXN':
return FiatCurrency.mxn;
case 'ARS':
return FiatCurrency.ars;
case 'COP':
return FiatCurrency.cop;
default:
throw Exception('Unknown FiatCurrency: $code');
static FiatCurrency? tryFromCode(String code) {
final normalized = code.toUpperCase();
for (final currency in values) {
if (currency.code == normalized) return currency;
}
return null;
}

static FiatCurrency fromCode(String code) {
final currency = tryFromCode(code);
if (currency != null) return currency;
throw Exception('Unknown FiatCurrency: $code');
}
}

Expand Down
2 changes: 1 addition & 1 deletion lib/core/mixins/privacy_screen.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:no_screenshot/no_screenshot.dart';

mixin PrivacyScreen {
Future<void> enableScreenPrivacy() async =>
Future<bool> enableScreenPrivacy() async =>
await NoScreenshot.instance.screenshotOff();

Future<void> disableScreenPrivacy() async =>
Expand Down
11 changes: 11 additions & 0 deletions lib/core/recoverbull/domain/usecases/allow_permission_usecase.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'package:bb_mobile/core/recoverbull/data/repository/recoverbull_repository.dart';

class AllowPermissionUsecase {
final RecoverBullRepository _recoverBullRepository;

AllowPermissionUsecase({required this._recoverBullRepository});

Future<void> execute(bool isGranted) async {
await _recoverBullRepository.allowPermission(isGranted);
}
}
11 changes: 11 additions & 0 deletions lib/core/recoverbull/domain/usecases/fetch_permission_usecase.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'package:bb_mobile/core/recoverbull/data/repository/recoverbull_repository.dart';

class FetchPermissionUsecase {
final RecoverBullRepository _recoverBullRepository;

FetchPermissionUsecase({required this._recoverBullRepository});

Future<bool> execute() async {
return await _recoverBullRepository.fetchPermission();
}
}
43 changes: 43 additions & 0 deletions lib/core/storage/app_data_directory.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'dart:io';

import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

/// Resolves the directory that owns Bull's local application data.
///
/// Normal builds always use the platform documents directory. The integration
/// target supplies an absolute, disposable directory so destructive recovery
/// journeys cannot touch a developer's real profile.
abstract final class AppDataDirectory {
static const _isolatedTestPath = String.fromEnvironment(
'BULL_INTEGRATION_DATA_DIR',
);

static bool get isIsolatedTestProfile => _isolatedTestPath.isNotEmpty;

static Future<Directory> resolve() async {
if (!isIsolatedTestProfile) {
return getApplicationDocumentsDirectory();
}

final directory = Directory(_isolatedTestPath);
final lexicalTemp = p.normalize(Directory.systemTemp.absolute.path);
final lexicalRequested = p.normalize(directory.absolute.path);
if (!directory.isAbsolute || !p.isWithin(lexicalTemp, lexicalRequested)) {
throw StateError(
'BULL_INTEGRATION_DATA_DIR must be inside the system temp directory',
);
}

final normalizedDirectory = Directory(lexicalRequested);
await normalizedDirectory.create(recursive: true);
final resolvedTemp = await Directory.systemTemp.resolveSymbolicLinks();
final resolvedRequested = await normalizedDirectory.resolveSymbolicLinks();
if (!p.isWithin(resolvedTemp, resolvedRequested)) {
throw StateError(
'BULL_INTEGRATION_DATA_DIR must not escape the system temp directory',
);
}
return Directory(resolvedRequested);
}
}
Loading