Add reusable two-factor verification for login and money operations (#32)
This commit is contained in:
1 parent
b7a58b1a87
commit
fb40f860d2
41 files changed
+2439
-43
No files matched your search
@@ -0,0 +1,137 @@
|
||||
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<FactorException>()),
|
||||
);
|
||||
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<FactorException>()),
|
||||
);
|
||||
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<WalletException>().having(
|
||||
(e) => e.code,
|
||||
'code',
|
||||
WalletError.expiredQuote,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect((await wallet.fetch()).accounts.first.available, 7500);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user