import 'package:flutter_test/flutter_test.dart'; import 'package:sxchnge_auth/sxchnge_auth.dart'; import 'package:sxchnge_kyc/sxchnge_kyc.dart'; import 'package:sxchnge_wallet/sxchnge_wallet.dart'; void main() { for (final kind in TransferKind.values) { test( '${kind.name} requires matching approval before funds change', () async { final factor = DummyTwoFactorService( enabled: true, delay: Duration.zero, ); final wallet = DummyWalletService( DummyIdentityService(delay: Duration.zero), factors: factor, delay: Duration.zero, ); final q = await wallet.quote( TransferRequest( kind: kind, source: 'eur', amount: 1000, destination: kind == TransferKind.exchange ? 'usd' : null, recipient: 'Sample recipient', ), ); await expectLater( wallet.submit(q.id, 'request'), throwsA(isA()), ); expect((await wallet.fetch()).accounts.first.available, 7500); expect((await wallet.fetch()).operations, isEmpty); final c = (await factor.begin( FactorPurpose.values.byName(kind.name), q.id, ))!; final g = await factor.verify(c.id, '123456'); final result = await wallet.submit(q.id, 'request', approval: g.token); expect((await wallet.submit(q.id, 'request')).id, result.id); expect((await wallet.fetch()).operations.length, 1); }, ); } test('concurrent retries consume one approval and debit once', () async { final factor = DummyTwoFactorService(enabled: true, delay: Duration.zero); final wallet = DummyWalletService( DummyIdentityService(delay: Duration.zero), factors: factor, delay: Duration.zero, ); final q = await wallet.quote( const TransferRequest( kind: TransferKind.payout, source: 'eur', amount: 1000, recipient: 'Sample recipient', ), ); final c = (await factor.begin(FactorPurpose.payout, q.id))!; final grant = await factor.verify(c.id, '123456'); final results = await Future.wait([ wallet.submit(q.id, 'same-request', approval: grant.token), wallet.submit(q.id, 'same-request', approval: grant.token), ]); expect(results[0].id, results[1].id); final snapshot = await wallet.fetch(); expect(snapshot.operations.length, 1); expect(snapshot.accounts.first.available, 6400); }); test('proof for another quote cannot authorize payout', () async { final factor = DummyTwoFactorService(enabled: true, delay: Duration.zero); final wallet = DummyWalletService( DummyIdentityService(delay: Duration.zero), factors: factor, delay: Duration.zero, ); final q = await wallet.quote( const TransferRequest( kind: TransferKind.payout, source: 'eur', amount: 1000, recipient: 'Sample recipient', ), ); final c = (await factor.begin(FactorPurpose.payout, 'different-quote'))!; final g = await factor.verify(c.id, '123456'); await expectLater( wallet.submit(q.id, 'request', approval: g.token), throwsA(isA()), ); expect((await wallet.fetch()).accounts.first.available, 7500); }); test( 'quote expired during factor verification does not move money', () async { var now = DateTime(2026, 9, 12); final factor = DummyTwoFactorService( enabled: true, delay: Duration.zero, now: () => now, ); final wallet = DummyWalletService( DummyIdentityService(delay: Duration.zero), factors: factor, delay: Duration.zero, now: () => now, ); final q = await wallet.quote( const TransferRequest( kind: TransferKind.exchange, source: 'eur', destination: 'usd', amount: 1000, ), ); final c = (await factor.begin(FactorPurpose.exchange, q.id))!; final g = await factor.verify(c.id, '123456'); now = now.add(const Duration(seconds: 61)); await expectLater( wallet.submit(q.id, 'request', approval: g.token), throwsA( isA().having( (e) => e.code, 'code', WalletError.expiredQuote, ), ), ); expect((await wallet.fetch()).accounts.first.available, 7500); }, ); }