Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
167 changes: 167 additions & 0 deletions test/screens/buy/cubits/buy_converter_cubit_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_brokerbot_service.dart';
import 'package:realunit_wallet/packages/service/dfx/models/brokerbot/dfx_buy_price_dto.dart';
import 'package:realunit_wallet/packages/service/dfx/models/brokerbot/dfx_buy_shares_dto.dart';
import 'package:realunit_wallet/screens/buy/cubits/buy_converter/buy_converter_cubit.dart';
import 'package:realunit_wallet/styles/currency.dart';

class _MockBrokerbotService extends Mock implements DfxBrokerbotService {}

void main() {
late _MockBrokerbotService service;

setUpAll(() {
registerFallbackValue(Currency.chf);
});

setUp(() {
service = _MockBrokerbotService();
});

group('$BuyConverterCubit', () {
test('initial state is empty with CHF', () {
final cubit = BuyConverterCubit(service);

expect(cubit.state.fiatText, '');
expect(cubit.state.sharesText, '');
expect(cubit.state.currency, Currency.chf);
expect(cubit.state.loading, isFalse);
});

test('onFiatChanged debounces, then writes the converted shares', () async {
when(() => service.getBuyShares(any(), any())).thenAnswer(
(_) async => BrokerbotBuySharesDto(
shares: 7,
pricePerShare: 12.5,
availableShares: 100,
),
);

final cubit = BuyConverterCubit(service);
await cubit.onFiatChanged('100');
// Past the 100ms debounce.
await Future<void>.delayed(const Duration(milliseconds: 250));

expect(cubit.state.fiatText, '100');
expect(cubit.state.sharesText, '7');
expect(cubit.state.loading, isFalse);
verify(() => service.getBuyShares('100', Currency.chf)).called(1);
});

test('onFiatChanged debounces — only the latest value reaches the service', () async {
when(() => service.getBuyShares(any(), any())).thenAnswer(
(_) async => BrokerbotBuySharesDto(
shares: 9,
pricePerShare: 1,
availableShares: 99,
),
);

final cubit = BuyConverterCubit(service);
// Three rapid keystrokes; only the last should reach the service.
await cubit.onFiatChanged('1');
await cubit.onFiatChanged('12');
await cubit.onFiatChanged('123');
await Future<void>.delayed(const Duration(milliseconds: 250));

verify(() => service.getBuyShares('123', any())).called(1);
verifyNever(() => service.getBuyShares('1', any()));
verifyNever(() => service.getBuyShares('12', any()));
});

test('onFiatChanged keeps state intact when the service throws', () async {
when(() => service.getBuyShares(any(), any()))
.thenAnswer((_) async => throw Exception('bad input'));

final cubit = BuyConverterCubit(service);
await cubit.onFiatChanged('-1');
await Future<void>.delayed(const Duration(milliseconds: 250));

expect(cubit.state.fiatText, '-1');
expect(cubit.state.sharesText, '');
expect(cubit.state.loading, isFalse);
});

test('onSharesChanged debounces, then writes the converted fiat with matching fractional digits', () async {
when(() => service.getBuyPrice(any(), any())).thenAnswer(
(_) async => BrokerbotBuyPriceDto(
totalCost: 125.5,
pricePerShare: 25.1,
availableShares: 100,
),
);

final cubit = BuyConverterCubit(service);
// Input has 3 fractional digits → output has 3.
await cubit.onSharesChanged('5.000');
await Future<void>.delayed(const Duration(milliseconds: 250));

expect(cubit.state.sharesText, '5.000');
expect(cubit.state.fiatText, '125.500');
});

test('onSharesChanged uses 2 fractional digits when input has no dot', () async {
when(() => service.getBuyPrice(any(), any())).thenAnswer(
(_) async => BrokerbotBuyPriceDto(
totalCost: 125.5,
pricePerShare: 25.1,
availableShares: 100,
),
);

final cubit = BuyConverterCubit(service);
await cubit.onSharesChanged('5');
await Future<void>.delayed(const Duration(milliseconds: 250));

expect(cubit.state.fiatText, '125.50');
});

test('onCurrencyChanged refetches shares with the new currency and emits both fields', () async {
when(() => service.getBuyShares(any(), any())).thenAnswer(
(_) async => BrokerbotBuySharesDto(
shares: 3,
pricePerShare: 10,
availableShares: 100,
),
);

final cubit = BuyConverterCubit(service);
await cubit.onCurrencyChanged(Currency.eur);

expect(cubit.state.currency, Currency.eur);
expect(cubit.state.sharesText, '3');
expect(cubit.state.loading, isFalse);
verify(() => service.getBuyShares('', Currency.eur)).called(1);
});

test('onCurrencyChanged still flips currency even on service error', () async {
when(() => service.getBuyShares(any(), any()))
.thenAnswer((_) async => throw Exception('throttle'));

final cubit = BuyConverterCubit(service);
await cubit.onCurrencyChanged(Currency.eur);

expect(cubit.state.currency, Currency.eur);
expect(cubit.state.loading, isFalse);
});

test('close() cancels pending debounce timers', () async {
when(() => service.getBuyShares(any(), any())).thenAnswer(
(_) async => BrokerbotBuySharesDto(
shares: 1,
pricePerShare: 1,
availableShares: 1,
),
);

final cubit = BuyConverterCubit(service);
await cubit.onFiatChanged('5');
// Close before the 100ms debounce fires.
await cubit.close();
await Future<void>.delayed(const Duration(milliseconds: 250));

verifyNever(() => service.getBuyShares(any(), any()));
});
});
}
86 changes: 86 additions & 0 deletions test/screens/sell/cubits/sell_balance_cubit_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import 'dart:async';

import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:realunit_wallet/models/balance.dart';
import 'package:realunit_wallet/packages/config/api_config.dart';
import 'package:realunit_wallet/packages/config/network_mode.dart';
import 'package:realunit_wallet/packages/repository/balance_repository.dart';
import 'package:realunit_wallet/packages/service/app_store.dart';
import 'package:realunit_wallet/packages/utils/default_assets.dart';
import 'package:realunit_wallet/screens/sell/cubits/sell_balance/sell_balance_cubit.dart';

class _MockBalanceRepository extends Mock implements BalanceRepository {}

class _MockAppStore extends Mock implements AppStore {}

class _FakeBalance extends Fake implements Balance {}

const _wallet = '0x000000000000000000000000000000000000beef';

void main() {
late _MockBalanceRepository repo;
late _MockAppStore appStore;
late StreamController<Balance> controller;

setUpAll(() {
registerFallbackValue(_FakeBalance());
});

setUp(() {
repo = _MockBalanceRepository();
appStore = _MockAppStore();
controller = StreamController<Balance>();
when(() => appStore.apiConfig)
.thenReturn(const ApiConfig(networkMode: NetworkMode.mainnet));
when(() => appStore.primaryAddress).thenReturn(_wallet);
when(() => repo.watchBalance(any())).thenAnswer((_) => controller.stream);
});

tearDown(() async {
await controller.close();
});

group('$SellBalanceCubit', () {
test('initial state is a zero balance derived from appStore', () {
final cubit = SellBalanceCubit(repo, appStore);

expect(cubit.state.chainId, realUnitAsset.chainId);
expect(cubit.state.contractAddress, realUnitAsset.address);
expect(cubit.state.walletAddress, _wallet);
expect(cubit.state.balance, BigInt.zero);
expect(cubit.state.asset, realUnitAsset);
});

test('subscribes to BalanceRepository.watchBalance on init', () {
SellBalanceCubit(repo, appStore);

verify(() => repo.watchBalance(any())).called(1);
});

test('emits balance updates pushed through the repo stream', () async {
final cubit = SellBalanceCubit(repo, appStore);

final updated = Balance(
chainId: realUnitAsset.chainId,
contractAddress: realUnitAsset.address,
walletAddress: _wallet,
balance: BigInt.from(7000),
asset: realUnitAsset,
);
final ready = cubit.stream.firstWhere((b) => b.balance == BigInt.from(7000));
controller.add(updated);
await ready.timeout(const Duration(seconds: 1));

expect(cubit.state.balance, BigInt.from(7000));
});

test('close() cancels the subscription cleanly', () async {
final cubit = SellBalanceCubit(repo, appStore);

await cubit.close();

expect(cubit.isClosed, isTrue);
});
});
}
123 changes: 123 additions & 0 deletions test/screens/sell/cubits/sell_bank_accounts_cubit_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_bank_account_service.dart';
import 'package:realunit_wallet/packages/service/dfx/models/bank_account/dto/bank_account_dto.dart';
import 'package:realunit_wallet/screens/sell/cubits/sell_bank_accounts/sell_bank_accounts_cubit.dart';

class _MockBankAccountService extends Mock implements DfxBankAccountService {}

BankAccountDto _dto({int id = 1, String? label, bool isActive = true}) =>
BankAccountDto(
id: id,
iban: 'CH56 0483 5012 3456 78$id',
label: label,
isActive: isActive,
isDefault: false,
);

void main() {
late _MockBankAccountService service;

setUp(() {
service = _MockBankAccountService();
});

group('$SellBankAccountsCubit', () {
test('reaches Success with the mapped accounts on construction', () async {
when(() => service.getBankAccounts())
.thenAnswer((_) async => [_dto(id: 1, label: 'Main'), _dto(id: 2)]);

final cubit = SellBankAccountsCubit(service);
await cubit.stream.firstWhere((s) => s is SellBankAccountsSuccess);

final success = cubit.state as SellBankAccountsSuccess;
expect(success.accounts.map((a) => a.id), [1, 2]);
expect(success.accounts.first.name, 'Main');
});

test('reaches LoadFailure when getBankAccounts throws', () async {
when(() => service.getBankAccounts())
.thenAnswer((_) async => throw Exception('boom'));

final cubit = SellBankAccountsCubit(service);
await cubit.stream.firstWhere((s) => s is SellBankAccountsLoadFailure);

expect(cubit.state, isA<SellBankAccountsLoadFailure>());
});

test('add() calls createBankAccount and re-fetches the list', () async {
var calls = 0;
when(() => service.getBankAccounts()).thenAnswer((_) async {
calls++;
if (calls == 1) return [_dto(id: 1)];
return [_dto(id: 1), _dto(id: 2, label: 'Added')];
});
when(() => service.createBankAccount(any(), any()))
.thenAnswer((_) async => _dto(id: 2, label: 'Added'));

final cubit = SellBankAccountsCubit(service);
await cubit.stream.firstWhere((s) => s is SellBankAccountsSuccess);

await cubit.add(iban: 'CH99', label: 'Added');

final success = cubit.state as SellBankAccountsSuccess;
expect(success.accounts.map((a) => a.id), [1, 2]);
verify(() => service.createBankAccount('CH99', 'Added')).called(1);
});

test('add() emits AddFailure on createBankAccount error and keeps prior accounts', () async {
when(() => service.getBankAccounts()).thenAnswer((_) async => [_dto(id: 1)]);
when(() => service.createBankAccount(any(), any()))
.thenAnswer((_) async => throw Exception('invalid iban'));

final cubit = SellBankAccountsCubit(service);
await cubit.stream.firstWhere((s) => s is SellBankAccountsSuccess);

await cubit.add(iban: 'NOPE');

expect(cubit.state, isA<SellBankAccountsAddFailure>());
expect((cubit.state as SellBankAccountsAddFailure).message, contains('invalid iban'));
expect(cubit.state.accounts, hasLength(1));
});

test('deactivate() calls updateBankAccount(isActive=false) and re-fetches', () async {
var calls = 0;
when(() => service.getBankAccounts()).thenAnswer((_) async {
calls++;
if (calls == 1) return [_dto(id: 1, isActive: true)];
return [_dto(id: 1, isActive: false)];
});
when(() => service.updateBankAccount(
id: any(named: 'id'),
isActive: any(named: 'isActive'),
)).thenAnswer((_) async => _dto(id: 1, isActive: false));

final cubit = SellBankAccountsCubit(service);
await cubit.stream.firstWhere((s) => s is SellBankAccountsSuccess);

final target = (cubit.state as SellBankAccountsSuccess).accounts.first;
await cubit.deactivate(bankAccount: target);

verify(() => service.updateBankAccount(id: 1, isActive: false)).called(1);
final success = cubit.state as SellBankAccountsSuccess;
expect(success.accounts.first.isActive, isFalse);
});

test('deactivate() emits UpdateFailure on updateBankAccount error', () async {
when(() => service.getBankAccounts()).thenAnswer((_) async => [_dto(id: 1)]);
when(() => service.updateBankAccount(
id: any(named: 'id'),
isActive: any(named: 'isActive'),
)).thenAnswer((_) async => throw Exception('500'));

final cubit = SellBankAccountsCubit(service);
await cubit.stream.firstWhere((s) => s is SellBankAccountsSuccess);

final target = (cubit.state as SellBankAccountsSuccess).accounts.first;
await cubit.deactivate(bankAccount: target);

expect(cubit.state, isA<SellBankAccountsUpdateFailure>());
expect(cubit.state.accounts, hasLength(1));
});
});
}
Loading
Loading