Skip to content

Commit 9fdab7e

Browse files
committed
test(joinstr): cover cubit orchestration and guardrails
1 parent 0cf9981 commit 9fdab7e

1 file changed

Lines changed: 211 additions & 0 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import 'package:bb_mobile/core/entities/signer_entity.dart';
2+
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
3+
import 'package:bb_mobile/core/wallet/domain/usecases/get_wallets_usecase.dart';
4+
import 'package:bb_mobile/features/joinstr/domain/joinstr.dart';
5+
import 'package:bb_mobile/features/joinstr/domain/usecases/initiate_joinstr_pool_usecase.dart';
6+
import 'package:bb_mobile/features/joinstr/domain/usecases/join_joinstr_pool_usecase.dart';
7+
import 'package:bb_mobile/features/joinstr/domain/usecases/list_joinstr_pools_usecase.dart';
8+
import 'package:bb_mobile/features/joinstr/presentation/joinstr_cubit.dart';
9+
import 'package:bb_mobile/features/joinstr/presentation/joinstr_state.dart';
10+
import 'package:flutter_test/flutter_test.dart';
11+
import 'package:mocktail/mocktail.dart';
12+
13+
class _MockGetWallets extends Mock implements GetWalletsUsecase {}
14+
15+
class _MockListPools extends Mock implements ListJoinstrPoolsUsecase {}
16+
17+
class _MockJoinPool extends Mock implements JoinJoinstrPoolUsecase {}
18+
19+
class _MockInitiatePool extends Mock implements InitiateJoinstrPoolUsecase {}
20+
21+
Wallet _wallet({
22+
String origin = 'w-testnet',
23+
Network network = Network.bitcoinTestnet,
24+
ScriptType scriptType = ScriptType.bip84,
25+
SignerEntity signer = SignerEntity.local,
26+
}) {
27+
return Wallet(
28+
origin: origin,
29+
network: network,
30+
xpubFingerprint: 'ffffffff',
31+
masterFingerprint: 'eeeeeeee',
32+
scriptType: scriptType,
33+
xpub: 'xpub',
34+
externalPublicDescriptor: 'wpkh([eeeeeeee/84h/1h/0h]xpub/0/*)',
35+
internalPublicDescriptor: 'wpkh([eeeeeeee/84h/1h/0h]xpub/1/*)',
36+
signer: signer,
37+
signerDevice: null,
38+
balanceSat: BigInt.zero,
39+
);
40+
}
41+
42+
JoinstrPool _pool(int denominationSat) => JoinstrPool(
43+
id: 'pool-$denominationSat',
44+
rawJson: '{}',
45+
denominationSat: denominationSat,
46+
peers: 2,
47+
expiresAtUnixSec: 1793500000,
48+
relay: 'wss://nos.lol',
49+
feeRateSatPerVb: 1,
50+
publicKey: 'pk',
51+
);
52+
53+
void main() {
54+
late _MockGetWallets getWallets;
55+
late _MockListPools listPools;
56+
late _MockJoinPool joinPool;
57+
late _MockInitiatePool initiatePool;
58+
59+
JoinstrCubit build() => JoinstrCubit(
60+
getWalletsUsecase: getWallets,
61+
listJoinstrPoolsUsecase: listPools,
62+
joinJoinstrPoolUsecase: joinPool,
63+
initiateJoinstrPoolUsecase: initiatePool,
64+
);
65+
66+
setUpAll(() {
67+
registerFallbackValue(_wallet());
68+
registerFallbackValue(_pool(100000));
69+
});
70+
71+
setUp(() {
72+
getWallets = _MockGetWallets();
73+
listPools = _MockListPools();
74+
joinPool = _MockJoinPool();
75+
initiatePool = _MockInitiatePool();
76+
});
77+
78+
group('load', () {
79+
test('errors when no local-signer wallet is available', () async {
80+
when(
81+
() => getWallets.execute(onlyBitcoin: any(named: 'onlyBitcoin')),
82+
).thenAnswer((_) async => [_wallet(signer: SignerEntity.none)]);
83+
84+
final cubit = build();
85+
await cubit.load();
86+
87+
expect(cubit.state.status, JoinstrStatus.error);
88+
expect(cubit.state.error?.issue, JoinstrIssue.watchOnlyWallet);
89+
verifyNever(() => listPools.execute());
90+
});
91+
92+
test('selects a supported testnet wallet over a mainnet one', () async {
93+
when(
94+
() => getWallets.execute(onlyBitcoin: any(named: 'onlyBitcoin')),
95+
).thenAnswer(
96+
(_) async => [
97+
_wallet(origin: 'w-mainnet', network: Network.bitcoinMainnet),
98+
_wallet(origin: 'w-testnet', network: Network.bitcoinTestnet),
99+
],
100+
);
101+
when(() => listPools.execute()).thenAnswer((_) async => []);
102+
103+
final cubit = build();
104+
await cubit.load();
105+
106+
expect(cubit.state.wallet?.id, 'w-testnet');
107+
expect(cubit.state.error, isNull);
108+
expect(cubit.state.status, JoinstrStatus.idle);
109+
});
110+
111+
test(
112+
'surfaces mainnetNotSupported when only a mainnet wallet exists',
113+
() async {
114+
when(
115+
() => getWallets.execute(onlyBitcoin: any(named: 'onlyBitcoin')),
116+
).thenAnswer((_) async => [_wallet(network: Network.bitcoinMainnet)]);
117+
118+
final cubit = build();
119+
await cubit.load();
120+
121+
expect(cubit.state.status, JoinstrStatus.error);
122+
expect(cubit.state.error?.issue, JoinstrIssue.mainnetNotSupported);
123+
},
124+
);
125+
});
126+
127+
group('refreshPools', () {
128+
test('moves to idle with the usecase result', () async {
129+
when(
130+
() => listPools.execute(),
131+
).thenAnswer((_) async => [_pool(100000), _pool(500000)]);
132+
133+
final cubit = build();
134+
await cubit.refreshPools();
135+
136+
expect(cubit.state.status, JoinstrStatus.idle);
137+
expect(cubit.state.pools.map((p) => p.denominationSat), [100000, 500000]);
138+
});
139+
});
140+
141+
group('initiatePool validation', () {
142+
test('rejects an out-of-range config before calling the usecase', () async {
143+
when(
144+
() => getWallets.execute(onlyBitcoin: any(named: 'onlyBitcoin')),
145+
).thenAnswer((_) async => [_wallet()]);
146+
when(() => listPools.execute()).thenAnswer((_) async => []);
147+
148+
final cubit = build();
149+
await cubit.load();
150+
cubit.peersChanged('1'); // below the 2-peer minimum
151+
152+
await cubit.initiatePool();
153+
154+
expect(cubit.state.status, JoinstrStatus.error);
155+
expect(cubit.state.error?.issue, JoinstrIssue.invalidPoolConfig);
156+
verifyNever(
157+
() => initiatePool.execute(
158+
wallet: any(named: 'wallet'),
159+
denominationSat: any(named: 'denominationSat'),
160+
peers: any(named: 'peers'),
161+
feeRateSatPerVb: any(named: 'feeRateSatPerVb'),
162+
),
163+
);
164+
});
165+
});
166+
167+
group('joinPool', () {
168+
test('runs then reports the broadcast txid', () async {
169+
when(
170+
() => getWallets.execute(onlyBitcoin: any(named: 'onlyBitcoin')),
171+
).thenAnswer((_) async => [_wallet()]);
172+
when(() => listPools.execute()).thenAnswer((_) async => []);
173+
when(
174+
() => joinPool.execute(
175+
wallet: any(named: 'wallet'),
176+
pool: any(named: 'pool'),
177+
),
178+
).thenAnswer((_) async => 'txid-abc');
179+
180+
final cubit = build();
181+
await cubit.load();
182+
await cubit.joinPool(_pool(100000));
183+
184+
expect(cubit.state.status, JoinstrStatus.done);
185+
expect(cubit.state.txId, 'txid-abc');
186+
});
187+
188+
test(
189+
'maps a JoinstrException from the usecase to the error state',
190+
() async {
191+
when(
192+
() => getWallets.execute(onlyBitcoin: any(named: 'onlyBitcoin')),
193+
).thenAnswer((_) async => [_wallet()]);
194+
when(() => listPools.execute()).thenAnswer((_) async => []);
195+
when(
196+
() => joinPool.execute(
197+
wallet: any(named: 'wallet'),
198+
pool: any(named: 'pool'),
199+
),
200+
).thenThrow(JoinstrException(JoinstrIssue.noEligibleCoin));
201+
202+
final cubit = build();
203+
await cubit.load();
204+
await cubit.joinPool(_pool(100000));
205+
206+
expect(cubit.state.status, JoinstrStatus.error);
207+
expect(cubit.state.error?.issue, JoinstrIssue.noEligibleCoin);
208+
},
209+
);
210+
});
211+
}

0 commit comments

Comments
 (0)