diff --git a/integration_test/oath_test.dart b/integration_test/oath_test.dart new file mode 100644 index 00000000..e540bd7e --- /dev/null +++ b/integration_test/oath_test.dart @@ -0,0 +1,186 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:yubico_authenticator/android/init.dart' as android; +import 'package:yubico_authenticator/app/views/no_device_screen.dart'; +import 'package:yubico_authenticator/core/state.dart'; +import 'package:yubico_authenticator/desktop/init.dart' as desktop; +import 'package:yubico_authenticator/oath/views/oath_screen.dart'; + +Future addDelay(int ms) async { + await Future.delayed(Duration(milliseconds: ms)); +} + +int randomNum(int max) { + var r = Random.secure(); + return r.nextInt(max); +} + +String randomPadded() { + return randomNum(999).toString().padLeft(3, '0'); +} + +String generateRandomIssuer() { + return 'i' + randomPadded(); +} + +String generateRandomName() { + return 'n' + randomPadded(); +} + +String generateRandomSecret() { + final random = Random.secure(); + return base64Encode(List.generate(10, (_) => random.nextInt(256))); +} + +void main() { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; + + group('OATH tests', () { + /// For these tests there are defined Keys in manage_password_dialog.dart + testWidgets('set password', (WidgetTester tester) async { + final Widget initializedApp; + if (isDesktop) { + initializedApp = await desktop.initialize([]); + } else if (isAndroid) { + initializedApp = await android.initialize(); + } else { + throw UnimplementedError('Platform not supported'); + } + + await tester.pumpWidget(initializedApp); + await tester.pump(const Duration(milliseconds: 500)); + + expect(find.byType(NoDeviceScreen), findsNothing, reason: 'No YubiKey connected'); + expect(find.byType(OathScreen), findsOneWidget); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pump(const Duration(milliseconds: 300)); + + await tester.tap(find.text('Set password')); + await tester.pump(const Duration(milliseconds: 300)); + + var first_password = 'aaa111'; + + /// TODO: I don't understand why these Keys don't work as intended + await tester.enterText(find.byKey(const Key('new oath password')), first_password); + await tester.enterText(find.byKey(const Key('confirm oath password')), first_password); + await tester.pump(); + + await tester.tap(find.text('Save')); + await tester.pump(const Duration(milliseconds: 300)); + + /// TODO: verification of state here: restarting app and entering password + await tester.pump(const Duration(seconds: 3)); + }); + testWidgets('change password', (WidgetTester tester) async { + final Widget initializedApp; + if (isDesktop) { + initializedApp = await desktop.initialize([]); + } else if (isAndroid) { + initializedApp = await android.initialize(); + } else { + throw UnimplementedError('Platform not supported'); + } + + await tester.pumpWidget(initializedApp); + await tester.pump(const Duration(milliseconds: 500)); + + expect(find.byType(NoDeviceScreen), findsNothing, reason: 'No YubiKey connected'); + expect(find.byType(OathScreen), findsOneWidget); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pump(const Duration(milliseconds: 300)); + + await tester.tap(find.text('Manage password')); + await tester.pump(const Duration(milliseconds: 300)); + + var current_password = 'aaa111'; + var second_password = 'bbb222'; + + /// TODO: I don't understand why these Keys don't work as intended + await tester.enterText(find.byKey(const Key('current oath password')), current_password); + await tester.enterText(find.byKey(const Key('new oath password')), second_password); + await tester.enterText(find.byKey(const Key('confirm oath password')), second_password); + await tester.pump(); + + await tester.tap(find.text('Save')); + await tester.pump(const Duration(milliseconds: 300)); + + /// TODO: verification of state here: restarting app and entering password + await tester.pump(const Duration(seconds: 3)); + }); + testWidgets('remove password', (WidgetTester tester) async { + final Widget initializedApp; + if (isDesktop) { + initializedApp = await desktop.initialize([]); + } else if (isAndroid) { + initializedApp = await android.initialize(); + } else { + throw UnimplementedError('Platform not supported'); + } + + await tester.pumpWidget(initializedApp); + await tester.pump(const Duration(milliseconds: 500)); + + expect(find.byType(NoDeviceScreen), findsNothing, reason: 'No YubiKey connected'); + expect(find.byType(OathScreen), findsOneWidget); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pump(const Duration(milliseconds: 300)); + + await tester.tap(find.text('Manage password')); + await tester.pump(const Duration(milliseconds: 300)); + + var second_password = 'bbb222'; + await tester.enterText(find.byKey(const Key('current oath password')), second_password); + await tester.pump(); + + await tester.tap(find.text('Remove password')); + await tester.pump(const Duration(milliseconds: 300)); + + /// TODO: verification of state here: restarting app and entering password + await tester.pump(const Duration(seconds: 3)); + }); + }); + group('TOTP tests', () { + testWidgets('first TOTP test', (WidgetTester tester) async { + final Widget initializedApp; + if (isDesktop) { + initializedApp = await desktop.initialize([]); + } else if (isAndroid) { + initializedApp = await android.initialize(); + } else { + throw UnimplementedError('Platform not supported'); + } + + await tester.pumpWidget(initializedApp); + await tester.pump(const Duration(milliseconds: 500)); + + expect(find.byType(NoDeviceScreen), findsNothing, reason: 'No YubiKey connected'); + expect(find.byType(OathScreen), findsOneWidget); + }); + }); + group('HOTP tests', () { + testWidgets('first HOTP test', (WidgetTester tester) async { + final Widget initializedApp; + if (isDesktop) { + initializedApp = await desktop.initialize([]); + } else if (isAndroid) { + initializedApp = await android.initialize(); + } else { + throw UnimplementedError('Platform not supported'); + } + + await tester.pumpWidget(initializedApp); + await tester.pump(const Duration(milliseconds: 500)); + + expect(find.byType(NoDeviceScreen), findsNothing, reason: 'No YubiKey connected'); + expect(find.byType(OathScreen), findsOneWidget); + }); + }); +} diff --git a/lib/oath/views/manage_password_dialog.dart b/lib/oath/views/manage_password_dialog.dart index adca4afd..a5da15aa 100755 --- a/lib/oath/views/manage_password_dialog.dart +++ b/lib/oath/views/manage_password_dialog.dart @@ -11,12 +11,10 @@ import '../state.dart'; class ManagePasswordDialog extends ConsumerStatefulWidget { final DevicePath path; final OathState state; - const ManagePasswordDialog(this.path, this.state, {Key? key}) - : super(key: key); + const ManagePasswordDialog(this.path, this.state, {Key? key}) : super(key: key); @override - ConsumerState createState() => - _ManagePasswordDialogState(); + ConsumerState createState() => _ManagePasswordDialogState(); } class _ManagePasswordDialogState extends ConsumerState { @@ -26,9 +24,7 @@ class _ManagePasswordDialogState extends ConsumerState { bool _currentIsWrong = false; _submit() async { - final result = await ref - .read(oathStateProvider(widget.path).notifier) - .setPassword(_currentPassword, _newPassword); + final result = await ref.read(oathStateProvider(widget.path).notifier).setPassword(_currentPassword, _newPassword); if (result) { Navigator.of(context).pop(); showMessage(context, 'Password set'); @@ -61,6 +57,7 @@ class _ManagePasswordDialogState extends ConsumerState { style: Theme.of(context).textTheme.headline6, ), TextField( + key: const Key('current oath password'), autofocus: true, obscureText: true, decoration: InputDecoration( @@ -81,9 +78,8 @@ class _ManagePasswordDialogState extends ConsumerState { child: const Text('Remove password'), onPressed: _currentPassword.isNotEmpty ? () async { - final result = await ref - .read(oathStateProvider(widget.path).notifier) - .unsetPassword(_currentPassword); + final result = + await ref.read(oathStateProvider(widget.path).notifier).unsetPassword(_currentPassword); if (result) { Navigator.of(context).pop(); showMessage(context, 'Password removed'); @@ -99,9 +95,7 @@ class _ManagePasswordDialogState extends ConsumerState { OutlinedButton( child: const Text('Clear saved password'), onPressed: () async { - await ref - .read(oathStateProvider(widget.path).notifier) - .forgetPassword(); + await ref.read(oathStateProvider(widget.path).notifier).forgetPassword(); Navigator.of(context).pop(); showMessage(context, 'Password forgotten'); }, @@ -115,6 +109,7 @@ class _ManagePasswordDialogState extends ConsumerState { style: Theme.of(context).textTheme.headline6, ), TextField( + key: const Key('new oath password'), autofocus: !widget.state.hasKey, obscureText: true, decoration: InputDecoration( @@ -129,6 +124,7 @@ class _ManagePasswordDialogState extends ConsumerState { }, ), TextField( + key: const Key('confirm oath password'), obscureText: true, decoration: InputDecoration( border: const OutlineInputBorder(),