Skip to content

Commit a20ad8e

Browse files
test(ledger): add unit tests
1 parent b3d07e2 commit a20ad8e

2 files changed

Lines changed: 221 additions & 0 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import 'package:bb_mobile/core/entities/signer_device_entity.dart';
2+
import 'package:bb_mobile/core/ledger/data/datasources/ledger_device_datasource.dart';
3+
import 'package:bb_mobile/core/ledger/data/models/ledger_device_model.dart';
4+
import 'package:bb_mobile/core/ledger/data/repositories/ledger_device_repository_impl.dart';
5+
import 'package:bb_mobile/core/ledger/domain/entities/ledger_device_entity.dart';
6+
import 'package:bb_mobile/core/ledger/domain/errors/ledger_exception.dart';
7+
import 'package:bb_mobile/core/ledger/domain/errors/ledger_failure.dart';
8+
import 'package:bb_mobile/core/settings/data/settings_repository.dart';
9+
import 'package:bb_mobile/core/utils/result.dart';
10+
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
11+
import 'package:bb_mobile/features/import_watch_only_wallet/watch_only_wallet_entity.dart';
12+
import 'package:flutter_test/flutter_test.dart';
13+
import 'package:mocktail/mocktail.dart';
14+
15+
class _MockLedgerDeviceDatasource extends Mock
16+
implements LedgerDeviceDatasource {}
17+
18+
class _MockSettingsRepository extends Mock implements SettingsRepository {}
19+
20+
void main() {
21+
late _MockLedgerDeviceDatasource datasource;
22+
late _MockSettingsRepository settingsRepository;
23+
late LedgerDeviceRepositoryImpl repository;
24+
25+
const device = LedgerDeviceEntity(
26+
id: 'device-1',
27+
name: 'Nano X',
28+
connectionType: LedgerConnectionType.usb,
29+
deviceType: SignerDeviceEntity.ledgerNanoX,
30+
);
31+
32+
setUpAll(() {
33+
registerFallbackValue(device.toModel());
34+
});
35+
36+
setUp(() {
37+
datasource = _MockLedgerDeviceDatasource();
38+
settingsRepository = _MockSettingsRepository();
39+
repository = LedgerDeviceRepositoryImpl(
40+
datasource: datasource,
41+
settingsRepository: settingsRepository,
42+
);
43+
});
44+
45+
group('LedgerDeviceRepositoryImpl (the sanitization boundary)', () {
46+
test('maps a semantic LedgerException to its typed failure', () async {
47+
when(
48+
() => datasource.connectDevice(any()),
49+
).thenThrow(const PermissionDeniedLedgerException());
50+
51+
final result = await repository.connectDevice(device);
52+
53+
expect(result, isA<Err<Null, LedgerFailure>>());
54+
expect(
55+
(result as Err).failure,
56+
isA<LedgerPermissionDeniedFailure>(),
57+
);
58+
});
59+
60+
test(
61+
'interprets a raw APDU 6985 device string as rejected-by-user, '
62+
'keeping the raw reason for logs only',
63+
() async {
64+
when(
65+
() => datasource.signPsbt(
66+
any(),
67+
psbt: any(named: 'psbt'),
68+
derivationPath: any(named: 'derivationPath'),
69+
scriptType: any(named: 'scriptType'),
70+
),
71+
).thenThrow(Exception('Ledger error: 0x6985 rejected'));
72+
73+
final result = await repository.signPsbt(
74+
device,
75+
psbt: 'psbt',
76+
derivationPath: "m/84'/0'/0'",
77+
scriptType: ScriptType.bip84,
78+
);
79+
80+
final failure = (result as Err).failure;
81+
expect(failure, isA<LedgerRejectedByUserFailure>());
82+
// The raw reason is retained for logs/Sentry, never rendered by the UI.
83+
expect(failure.logMessage, contains('6985'));
84+
},
85+
);
86+
87+
test(
88+
'maps an unrecognized raw exception to a sanitized unexpected failure '
89+
'without leaking the message to the UI surface',
90+
() async {
91+
when(
92+
() => datasource.signPsbt(
93+
any(),
94+
psbt: any(named: 'psbt'),
95+
derivationPath: any(named: 'derivationPath'),
96+
scriptType: any(named: 'scriptType'),
97+
),
98+
).thenThrow(Exception('bdk: internal descriptor parse blew up'));
99+
100+
final result = await repository.signPsbt(
101+
device,
102+
psbt: 'psbt',
103+
derivationPath: "m/84'/0'/0'",
104+
scriptType: ScriptType.bip84,
105+
);
106+
107+
expect((result as Err).failure, isA<LedgerUnexpectedFailure>());
108+
},
109+
);
110+
111+
test('returns Ok with the value on success', () async {
112+
when(
113+
() => datasource.signPsbt(
114+
any(),
115+
psbt: any(named: 'psbt'),
116+
derivationPath: any(named: 'derivationPath'),
117+
scriptType: any(named: 'scriptType'),
118+
),
119+
).thenAnswer((_) async => 'deadbeef');
120+
121+
final result = await repository.signPsbt(
122+
device,
123+
psbt: 'psbt',
124+
derivationPath: "m/84'/0'/0'",
125+
scriptType: ScriptType.bip84,
126+
);
127+
128+
expect(result, isA<Ok<String, LedgerFailure>>());
129+
expect((result as Ok).value, 'deadbeef');
130+
});
131+
132+
test(
133+
'getWatchOnlyWallet sanitizes a throwing foreign dependency '
134+
'(settings fetch) into an unexpected failure',
135+
() async {
136+
when(
137+
() => settingsRepository.fetch(),
138+
).thenThrow(Exception('STORAGE UNAVAILABLE'));
139+
140+
final result = await repository.getWatchOnlyWallet(
141+
device,
142+
label: 'Ledger',
143+
);
144+
145+
expect(result, isA<Err<WatchOnlyWalletEntity, LedgerFailure>>());
146+
expect((result as Err).failure, isA<LedgerUnexpectedFailure>());
147+
},
148+
);
149+
});
150+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import 'package:bb_mobile/core/entities/signer_device_entity.dart';
2+
import 'package:bb_mobile/core/ledger/domain/entities/ledger_device_entity.dart';
3+
import 'package:bb_mobile/core/ledger/domain/errors/ledger_failure.dart';
4+
import 'package:bb_mobile/core/ledger/domain/repositories/ledger_device_repository.dart';
5+
import 'package:bb_mobile/core/ledger/domain/usecases/get_ledger_watch_only_wallet_usecase.dart';
6+
import 'package:bb_mobile/core/ledger/domain/usecases/sign_psbt_ledger_usecase.dart';
7+
import 'package:bb_mobile/core/utils/result.dart';
8+
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
9+
import 'package:bb_mobile/features/import_watch_only_wallet/watch_only_wallet_entity.dart';
10+
import 'package:flutter_test/flutter_test.dart';
11+
import 'package:mocktail/mocktail.dart';
12+
13+
class _MockLedgerDeviceRepository extends Mock
14+
implements LedgerDeviceRepository {}
15+
16+
void main() {
17+
late _MockLedgerDeviceRepository repository;
18+
19+
const device = LedgerDeviceEntity(
20+
id: 'device-1',
21+
name: 'Nano X',
22+
connectionType: LedgerConnectionType.usb,
23+
deviceType: SignerDeviceEntity.ledgerNanoX,
24+
);
25+
26+
setUp(() {
27+
repository = _MockLedgerDeviceRepository();
28+
});
29+
30+
group('SignPsbtLedgerUsecase', () {
31+
test('forwards the repository failure unchanged (no raw leak)', () async {
32+
final usecase = SignPsbtLedgerUsecase(repository: repository);
33+
when(
34+
() => repository.signPsbt(
35+
any(),
36+
psbt: any(named: 'psbt'),
37+
derivationPath: any(named: 'derivationPath'),
38+
scriptType: any(named: 'scriptType'),
39+
),
40+
).thenAnswer((_) async => const Err(LedgerRejectedByUserFailure()));
41+
42+
final result = await usecase.execute(
43+
device,
44+
psbt: 'psbt',
45+
derivationPath: "m/84'/0'/0'",
46+
scriptType: ScriptType.bip84,
47+
);
48+
49+
expect((result as Err).failure, isA<LedgerRejectedByUserFailure>());
50+
});
51+
});
52+
53+
group('GetLedgerWatchOnlyWalletUsecase', () {
54+
test('forwards the repository failure unchanged (no raw leak)', () async {
55+
final usecase = GetLedgerWatchOnlyWalletUsecase(repository: repository);
56+
when(
57+
() => repository.getWatchOnlyWallet(
58+
any(),
59+
label: any(named: 'label'),
60+
scriptType: any(named: 'scriptType'),
61+
account: any(named: 'account'),
62+
),
63+
).thenAnswer((_) async => const Err(LedgerUnexpectedFailure('raw boom')));
64+
65+
final result = await usecase.execute(label: 'Ledger', device: device);
66+
67+
expect(result, isA<Err<WatchOnlyWalletEntity, LedgerFailure>>());
68+
expect((result as Err).failure, isA<LedgerUnexpectedFailure>());
69+
});
70+
});
71+
}

0 commit comments

Comments
 (0)