244 lines
9.0 KiB
Dart
244 lines
9.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:sxchnge_analytics/sxchnge_analytics.dart';
|
|
|
|
import 'package:sxchnge_auth/sxchnge_auth.dart';
|
|
|
|
import 'package:sxchnge_app/main.dart';
|
|
import 'package:sxchnge_app/providers/locale.dart';
|
|
|
|
Future<void> tapFactor(WidgetTester t, String key) async {
|
|
final finder = find.byKey(ValueKey(key));
|
|
await t.ensureVisible(finder);
|
|
await t.tap(finder);
|
|
await t.pumpAndSettle();
|
|
}
|
|
|
|
Future<DummyTwoFactorService> openFactor(
|
|
WidgetTester t, {
|
|
String path = '/settings',
|
|
bool enabled = false,
|
|
String locale = 'en',
|
|
FactorMethod method = FactorMethod.email,
|
|
DummyAnalyticsService? analytics,
|
|
}) async {
|
|
t.platformDispatcher.defaultRouteNameTestValue = path;
|
|
addTearDown(t.platformDispatcher.clearDefaultRouteNameTestValue);
|
|
final service = DummyTwoFactorService(
|
|
enabled: enabled,
|
|
delay: Duration.zero,
|
|
method: method,
|
|
);
|
|
await t.pumpWidget(
|
|
SxchngeApp(
|
|
twoFactorService: service,
|
|
analyticsService: analytics,
|
|
localeProvider: LocaleProvider(initialLocale: Locale(locale)),
|
|
),
|
|
);
|
|
await t.pumpAndSettle();
|
|
addTearDown(() => t.pumpWidget(const SizedBox.shrink()));
|
|
return service;
|
|
}
|
|
|
|
Future<void> signIn(WidgetTester t) async {
|
|
await t.enterText(find.byKey(const ValueKey('email')), 'person@example.com');
|
|
await t.enterText(find.byKey(const ValueKey('password')), 'example-password');
|
|
await tapFactor(t, 'submit');
|
|
}
|
|
|
|
void main() {
|
|
testWidgets('settings enable and disable only after verified code', (
|
|
t,
|
|
) async {
|
|
final service = await openFactor(t);
|
|
expect(find.text('Off'), findsOneWidget);
|
|
await tapFactor(t, 'two-factor-setting');
|
|
expect((await t.runAsync(service.fetch))!.enabled, false);
|
|
await t.enterText(find.byKey(const ValueKey('two-factor-code')), '000000');
|
|
await tapFactor(t, 'two-factor-confirm');
|
|
expect(find.byKey(const ValueKey('two-factor-error')), findsOneWidget);
|
|
expect((await t.runAsync(service.fetch))!.enabled, false);
|
|
await t.enterText(find.byKey(const ValueKey('two-factor-code')), '123456');
|
|
await tapFactor(t, 'two-factor-confirm');
|
|
expect(find.text('On'), findsOneWidget);
|
|
expect((await t.runAsync(service.fetch))!.enabled, true);
|
|
await tapFactor(t, 'two-factor-setting');
|
|
expect(find.textContaining('turn off two-factor'), findsOneWidget);
|
|
await t.enterText(find.byKey(const ValueKey('two-factor-code')), '123456');
|
|
await tapFactor(t, 'two-factor-confirm');
|
|
expect(find.text('Off'), findsOneWidget);
|
|
expect((await t.runAsync(service.fetch))!.enabled, false);
|
|
});
|
|
for (final entry in {
|
|
FactorError.expired: 'This verification has expired.',
|
|
FactorError.locked: 'Too many incorrect attempts.',
|
|
FactorError.unavailable: 'Could not complete verification.',
|
|
}.entries) {
|
|
testWidgets(
|
|
'${entry.key.name} is recoverable without enabling protection',
|
|
(t) async {
|
|
final service = await openFactor(t);
|
|
await tapFactor(t, 'two-factor-setting');
|
|
service.nextError = entry.key;
|
|
await t.enterText(
|
|
find.byKey(const ValueKey('two-factor-code')),
|
|
'123456',
|
|
);
|
|
await tapFactor(t, 'two-factor-confirm');
|
|
expect(find.textContaining(entry.value), findsOneWidget);
|
|
await tapFactor(t, 'two-factor-cancel');
|
|
expect(find.text('Off'), findsOneWidget);
|
|
expect((await t.runAsync(service.fetch))!.enabled, false);
|
|
},
|
|
);
|
|
}
|
|
testWidgets('challenge creation failure is measured and can retry', (
|
|
t,
|
|
) async {
|
|
final analytics = DummyAnalyticsService();
|
|
final service = await openFactor(t, analytics: analytics);
|
|
service.nextError = FactorError.unavailable;
|
|
await tapFactor(t, 'two-factor-setting');
|
|
expect(
|
|
find.textContaining('Could not complete verification.'),
|
|
findsOneWidget,
|
|
);
|
|
expect(
|
|
analytics.events.any(
|
|
(e) =>
|
|
e.step == JourneyStep.factorChallenge &&
|
|
e.failure == JourneyFailure.unavailable,
|
|
),
|
|
true,
|
|
);
|
|
expect(analytics.events.where((e) => e.terminal), isEmpty);
|
|
await tapFactor(t, 'two-factor-setting');
|
|
expect(find.byKey(const ValueKey('two-factor-code')), findsOneWidget);
|
|
await tapFactor(t, 'two-factor-cancel');
|
|
});
|
|
testWidgets('cancel enrollment leaves setting unchanged', (t) async {
|
|
final service = await openFactor(t);
|
|
await tapFactor(t, 'two-factor-setting');
|
|
await tapFactor(t, 'two-factor-cancel');
|
|
expect(find.text('Off'), findsOneWidget);
|
|
expect((await t.runAsync(service.fetch))!.enabled, false);
|
|
});
|
|
testWidgets('login is not terminal until factor and completion succeed', (
|
|
t,
|
|
) async {
|
|
final analytics = DummyAnalyticsService();
|
|
await openFactor(t, path: '/', enabled: true, analytics: analytics);
|
|
await signIn(t);
|
|
expect(find.byKey(const ValueKey('two-factor-code')), findsOneWidget);
|
|
expect(find.byKey(const ValueKey('preview-result')), findsNothing);
|
|
expect(
|
|
analytics.events.where((e) => e.kind == JourneyKind.login && e.terminal),
|
|
isEmpty,
|
|
);
|
|
await t.enterText(find.byKey(const ValueKey('two-factor-code')), '123456');
|
|
await tapFactor(t, 'two-factor-confirm');
|
|
expect(find.byKey(const ValueKey('account-eur')), findsOneWidget);
|
|
final login = analytics.events
|
|
.where((e) => e.kind == JourneyKind.login)
|
|
.toList();
|
|
expect(login.where((e) => e.terminal).length, 1);
|
|
expect(login.map((e) => e.journeyId).toSet().length, 1);
|
|
expect(
|
|
login.map((e) => e.properties).toString(),
|
|
isNot(contains('123456')),
|
|
);
|
|
});
|
|
testWidgets('cancel login factor returns without a successful session', (
|
|
t,
|
|
) async {
|
|
await openFactor(t, path: '/', enabled: true);
|
|
await signIn(t);
|
|
await tapFactor(t, 'two-factor-cancel');
|
|
expect(find.byKey(const ValueKey('email')), findsOneWidget);
|
|
expect(find.byKey(const ValueKey('preview-result')), findsNothing);
|
|
expect(
|
|
t
|
|
.widget<TextFormField>(find.byKey(const ValueKey('password')))
|
|
.controller!
|
|
.text,
|
|
isEmpty,
|
|
);
|
|
});
|
|
for (final kind in ['top-up', 'exchange', 'payout']) {
|
|
testWidgets(
|
|
'$kind confirms specific transaction and cancel preserves review',
|
|
(t) async {
|
|
await openFactor(t, path: '/$kind', enabled: true);
|
|
await t.enterText(find.byKey(const ValueKey('transfer-amount')), '10');
|
|
if (kind == 'payout') {
|
|
await t.enterText(
|
|
find.byKey(const ValueKey('transfer-recipient')),
|
|
'Sample recipient',
|
|
);
|
|
}
|
|
await tapFactor(t, 'transfer-review');
|
|
await tapFactor(t, 'transfer-confirm');
|
|
expect(find.byKey(const ValueKey('two-factor-code')), findsOneWidget);
|
|
expect(find.textContaining('No money moves'), findsOneWidget);
|
|
expect(find.text('Fee'), findsOneWidget);
|
|
await tapFactor(t, 'two-factor-cancel');
|
|
expect(find.byKey(const ValueKey('transfer-confirm')), findsOneWidget);
|
|
expect(find.byKey(const ValueKey('operation-pending')), findsNothing);
|
|
},
|
|
);
|
|
}
|
|
testWidgets(
|
|
'money moves after verification and confirmation, without another submit',
|
|
(t) async {
|
|
await openFactor(t, path: '/exchange', enabled: true);
|
|
await t.enterText(find.byKey(const ValueKey('transfer-amount')), '10');
|
|
await tapFactor(t, 'transfer-review');
|
|
await tapFactor(t, 'transfer-confirm');
|
|
await t.enterText(
|
|
find.byKey(const ValueKey('two-factor-code')),
|
|
'123456',
|
|
);
|
|
await tapFactor(t, 'two-factor-confirm');
|
|
expect(find.byKey(const ValueKey('operation-pending')), findsOneWidget);
|
|
expect(find.textContaining('amount and fee have left'), findsOneWidget);
|
|
},
|
|
);
|
|
testWidgets('authenticator uses SXCHNGE page without email or resend UI', (
|
|
t,
|
|
) async {
|
|
await openFactor(
|
|
t,
|
|
path: '/',
|
|
enabled: true,
|
|
method: FactorMethod.authenticator,
|
|
);
|
|
await signIn(t);
|
|
expect(find.textContaining('authenticator app'), findsOneWidget);
|
|
expect(find.byKey(const ValueKey('two-factor-resend')), findsNothing);
|
|
expect(find.text('a•••@example.com'), findsNothing);
|
|
});
|
|
testWidgets('Russian factor screen fits small viewport and enlarged text', (
|
|
t,
|
|
) async {
|
|
t.view.physicalSize = const Size(320, 1100);
|
|
t.view.devicePixelRatio = 1;
|
|
t.platformDispatcher.textScaleFactorTestValue = 1.6;
|
|
addTearDown(t.view.resetPhysicalSize);
|
|
addTearDown(t.view.resetDevicePixelRatio);
|
|
addTearDown(t.platformDispatcher.clearTextScaleFactorTestValue);
|
|
await openFactor(t, locale: 'ru');
|
|
await tapFactor(t, 'two-factor-setting');
|
|
expect(find.text('Ещё одна проверка.'), findsOneWidget);
|
|
expect(t.takeException(), null);
|
|
await tapFactor(t, 'two-factor-cancel');
|
|
});
|
|
testWidgets('missing challenge deep link returns to login', (t) async {
|
|
await openFactor(t, path: '/two-factor');
|
|
expect(find.byKey(const ValueKey('email')), findsOneWidget);
|
|
expect(find.byKey(const ValueKey('two-factor-code')), findsNothing);
|
|
});
|
|
}
|