added all tests relating to OATH options, and a first TOTP test (max

accounts)
This commit is contained in:
Joakim Troëng 2022-07-06 22:50:38 +02:00
parent b29917de8c
commit 19bb535fda
No known key found for this signature in database
GPG Key ID: BE887BDFCD88A558
3 changed files with 123 additions and 110 deletions

View File

@ -4,7 +4,6 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:yubico_authenticator/app/views/no_device_screen.dart';
import 'package:yubico_authenticator/oath/views/account_list.dart'; import 'package:yubico_authenticator/oath/views/account_list.dart';
import 'package:yubico_authenticator/oath/views/oath_screen.dart'; import 'package:yubico_authenticator/oath/views/oath_screen.dart';
@ -24,11 +23,11 @@ String randomPadded() {
} }
String generateRandomIssuer() { String generateRandomIssuer() {
return 'i' + randomPadded(); return 'i${randomPadded()}';
} }
String generateRandomName() { String generateRandomName() {
return 'n' + randomPadded(); return 'n${randomPadded()}';
} }
String generateRandomSecret() { String generateRandomSecret() {
@ -40,111 +39,141 @@ void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
group('OATH tests', () { group('OATH Options', () {
/// For these tests there are defined Keys in manage_password_dialog.dart /*
testWidgets('set password', (WidgetTester tester) async { These tests verify that all oath options are verified to function correctly by:
1. setting firsPassword and verifying it
2. logging in and changing to secondPassword and verifying it
3. changing to thirdPassword
4. removing thirdPassword
*/
testWidgets('OATH: set firstPassword', (WidgetTester tester) async {
await tester.pumpWidget(await getAuthenticatorApp()); await tester.pumpWidget(await getAuthenticatorApp());
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(NoDeviceScreen), findsNothing, var firstPassword = 'aaa111';
reason: 'No YubiKey connected');
expect(find.byType(OathScreen), findsOneWidget);
await tester.tap(find.byType(FloatingActionButton)); /// expect(find.byType(OathScreen), findsOneWidget); <<< I am not certain if this is needed.
await tester.pump(const Duration(milliseconds: 300));
await tester.tap(find.byIcon(Icons.tune));
await tester.pump(const Duration(milliseconds: 100));
await tester.tap(find.text('Set password')); await tester.tap(find.text('Set password'));
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 100));
var first_password = 'aaa111'; await tester.enterText(find.byKey(const Key('new oath password')), firstPassword);
await tester.pump();
/// TODO: I don't understand why these Keys don't work as intended await tester.enterText(find.byKey(const Key('confirm oath password')), firstPassword);
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.pump();
await tester.tap(find.text('Save')); await tester.tap(find.text('Save'));
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 100));
/// TODO: verification of state here: restarting app and entering password await tester.pump(const Duration(milliseconds: 1000));
await tester.pump(const Duration(seconds: 3));
}); });
testWidgets('change password', (WidgetTester tester) async { testWidgets('OATH: verify firstPassword', (WidgetTester tester) async {
await tester.pumpWidget(await getAuthenticatorApp()); await tester.pumpWidget(await getAuthenticatorApp());
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(NoDeviceScreen), findsNothing, var firstPassword = 'aaa111';
reason: 'No YubiKey connected');
expect(find.byType(OathScreen), findsOneWidget);
await tester.tap(find.byType(FloatingActionButton)); await tester.enterText(find.byKey(const Key('oath password')), firstPassword);
await tester.pump(const Duration(milliseconds: 300)); await tester.pump();
/// TODO: verification of state here: see that list of accounts is shown
await tester.pump(const Duration(milliseconds: 1000));
});
testWidgets('OATH: set secondPassword', (WidgetTester tester) async {
await tester.pumpWidget(await getAuthenticatorApp());
await tester.pump(const Duration(milliseconds: 500));
var firstPassword = 'aaa111';
var secondPassword = 'bbb222';
await tester.enterText(find.byKey(const Key('oath password')), firstPassword);
await tester.pump();
await tester.tap(find.byKey(const Key('oath unlock')));
await tester.tap(find.byIcon(Icons.tune));
await tester.pump(const Duration(milliseconds: 100));
await tester.tap(find.text('Manage password')); await tester.tap(find.text('Manage password'));
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 100));
var current_password = 'aaa111'; await tester.enterText(find.byKey(const Key('current oath password')), firstPassword);
var second_password = 'bbb222'; await tester.pump();
await tester.enterText(find.byKey(const Key('new oath password')), secondPassword);
/// TODO: I don't understand why these Keys don't work as intended await tester.pump();
await tester.enterText( await tester.enterText(find.byKey(const Key('confirm oath password')), secondPassword);
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.pump();
await tester.tap(find.text('Save')); await tester.tap(find.text('Save'));
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 100));
/// TODO: verification of state here: restarting app and entering password await tester.pump(const Duration(milliseconds: 1000));
await tester.pump(const Duration(seconds: 3));
}); });
testWidgets('remove password', (WidgetTester tester) async { testWidgets('OATH: set thirdPassword', (WidgetTester tester) async {
await tester.pumpWidget(await getAuthenticatorApp()); await tester.pumpWidget(await getAuthenticatorApp());
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(NoDeviceScreen), findsNothing, var secondPassword = 'bbb222';
reason: 'No YubiKey connected'); var thirdPassword = 'ccc333';
expect(find.byType(OathScreen), findsOneWidget);
await tester.tap(find.byType(FloatingActionButton)); await tester.tap(find.byIcon(Icons.tune));
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 100));
await tester.tap(find.text('Manage password')); await tester.tap(find.text('Manage password'));
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 100));
var second_password = 'bbb222'; await tester.enterText(find.byKey(const Key('current oath password')), secondPassword);
await tester.enterText( await tester.pump();
find.byKey(const Key('current oath password')), second_password); await tester.enterText(find.byKey(const Key('new oath password')), thirdPassword);
await tester.pump();
await tester.enterText(find.byKey(const Key('confirm oath password')), thirdPassword);
await tester.pump();
await tester.tap(find.text('Save'));
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 1000));
/// TODO: verification of state here: see that list of accounts is shown
});
testWidgets('OATH: remove thirdPassword', (WidgetTester tester) async {
await tester.pumpWidget(await getAuthenticatorApp());
await tester.pump(const Duration(milliseconds: 500));
var thirdPassword = 'ccc333';
await tester.tap(find.byIcon(Icons.tune));
await tester.pump(const Duration(milliseconds: 100));
await tester.tap(find.text('Manage password'));
await tester.pump(const Duration(milliseconds: 100));
await tester.enterText(find.byKey(const Key('current oath password')), thirdPassword);
await tester.pump(); await tester.pump();
await tester.tap(find.text('Remove password')); await tester.tap(find.text('Remove password'));
await tester.pump(const Duration(milliseconds: 300));
/// TODO: verification of state here: restarting app and entering password /// TODO: verification of state here: see that list of accounts is shown
await tester.pump(const Duration(seconds: 3)); await tester.pump(const Duration(milliseconds: 1000));
}); });
}); });
group('TOTP tests', () { group('TOTP tests', () {
testWidgets('Add 32 TOTP accounts and reset oath', /*
(WidgetTester tester) async { Tests will verify all TOTP functionality, not yet though:
1. Add 32 TOTP accounts
*/
testWidgets('TOTP: Add 32 accounts', (WidgetTester tester) async {
await tester.pumpWidget(await getAuthenticatorApp()); await tester.pumpWidget(await getAuthenticatorApp());
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(NoDeviceScreen), findsNothing,
reason: 'No YubiKey connected');
expect(find.byType(OathScreen), findsOneWidget);
for (var i = 0; i < 32; i += 1) { for (var i = 0; i < 32; i += 1) {
await tester.tap(find.byType(FloatingActionButton)); await tester.tap(find.byKey(const Key('add oath account')));
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 100));
await tester.tap(find.text('Add account'));
await tester.pump(const Duration(milliseconds: 300));
var issuer = generateRandomIssuer(); var issuer = generateRandomIssuer();
var name = generateRandomName(); var name = generateRandomName();
@ -153,53 +182,50 @@ void main() {
/// this random fails: generateRandomSecret(); /// this random fails: generateRandomSecret();
await tester.enterText(find.byKey(const Key('issuer')), issuer); await tester.enterText(find.byKey(const Key('issuer')), issuer);
await tester.pump(const Duration(milliseconds: 5)); await tester.pump(const Duration(milliseconds: 40));
await tester.enterText(find.byKey(const Key('name')), name); await tester.enterText(find.byKey(const Key('name')), name);
await tester.pump(const Duration(milliseconds: 5)); await tester.pump(const Duration(milliseconds: 40));
await tester.enterText(find.byKey(const Key('secret')), secret); await tester.enterText(find.byKey(const Key('secret')), secret);
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 100));
await tester.tap(find.byKey(const Key('save_btn'))); await tester.tap(find.byKey(const Key('save_btn')));
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(OathScreen), findsOneWidget); expect(find.byType(OathScreen), findsOneWidget);
await tester.enterText( await tester.enterText(find.byKey(const Key('search_accounts')), issuer);
find.byKey(const Key('search_accounts')), issuer);
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 100));
expect( expect(find.descendant(of: find.byType(AccountList), matching: find.textContaining(issuer)), findsOneWidget);
find.descendant(
of: find.byType(AccountList),
matching: find.textContaining(issuer)),
findsOneWidget);
await tester.pump(const Duration(milliseconds: 50)); await tester.pump(const Duration(milliseconds: 50));
} }
await tester.pump(const Duration(milliseconds: 3000));
/*
await tester.tap(find.byType(FloatingActionButton)); await tester.tap(find.byType(FloatingActionButton));
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 500));
await tester.tap(find.text('Reset OATH')); await tester.tap(find.text('Reset OATH'));
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 500));
await tester.tap(find.text('Reset')); await tester.tap(find.text('Reset'));
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(seconds: 3)); */
}); });
}); });
/*
group('HOTP tests', () { group('HOTP tests', () {
testWidgets('first HOTP test', (WidgetTester tester) async { testWidgets('first HOTP test', (WidgetTester tester) async {
await tester.pumpWidget(await getAuthenticatorApp()); await tester.pumpWidget(await getAuthenticatorApp());
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(NoDeviceScreen), findsNothing,
reason: 'No YubiKey connected');
expect(find.byType(OathScreen), findsOneWidget); expect(find.byType(OathScreen), findsOneWidget);
}); });
}); });
*/
} }

View File

@ -4,7 +4,6 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:yubico_authenticator/app/views/no_device_screen.dart';
import 'package:yubico_authenticator/oath/views/oath_screen.dart'; import 'package:yubico_authenticator/oath/views/oath_screen.dart';
import 'test_util.dart'; import 'test_util.dart';
@ -23,11 +22,11 @@ String randomPadded() {
} }
String generateRandomIssuer() { String generateRandomIssuer() {
return 'i' + randomPadded(); return 'i${randomPadded()}';
} }
String generateRandomName() { String generateRandomName() {
return 'n' + randomPadded(); return 'n${randomPadded()}';
} }
String generateRandomSecret() { String generateRandomSecret() {
@ -46,10 +45,6 @@ void main() {
await tester.pumpWidget(await getAuthenticatorApp()); await tester.pumpWidget(await getAuthenticatorApp());
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(NoDeviceScreen), findsNothing,
reason: 'No YubiKey connected');
expect(find.byType(OathScreen), findsOneWidget);
/// QUESTION: I want to click the DrawerItem named 'WebAuthn' | 'Authenticator' /// QUESTION: I want to click the DrawerItem named 'WebAuthn' | 'Authenticator'
/// await tester.tap(find.byType(DrawerItem.titleText == 'WebAuthn')); /// await tester.tap(find.byType(DrawerItem.titleText == 'WebAuthn'));
/// which can be found in main_drawer.dart, how do I make sure I call the right /// which can be found in main_drawer.dart, how do I make sure I call the right
@ -90,10 +85,6 @@ void main() {
await tester.pumpWidget(await getAuthenticatorApp()); await tester.pumpWidget(await getAuthenticatorApp());
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(NoDeviceScreen), findsNothing,
reason: 'No YubiKey connected');
expect(find.byType(OathScreen), findsOneWidget);
/// QUESTION: I want to click the DrawerItem named 'WebAuthn' | 'Authenticator' /// QUESTION: I want to click the DrawerItem named 'WebAuthn' | 'Authenticator'
/// await tester.tap(find.byType(DrawerItem.titleText == 'WebAuthn')); /// await tester.tap(find.byType(DrawerItem.titleText == 'WebAuthn'));
/// which can be found in main_drawer.dart, how do I make sure I call the right /// which can be found in main_drawer.dart, how do I make sure I call the right
@ -127,8 +118,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 30000)); await tester.pump(const Duration(milliseconds: 30000));
/// The following should report the success, if there are no accounts. /// The following should report the success, if there are no accounts.
expect(find.byType(OathScreen), findsNothing, expect(find.byType(OathScreen), findsNothing, reason: 'FIDO successfully reset.');
reason: 'FIDO successfully reset.');
await tester.pump(const Duration(seconds: 3)); await tester.pump(const Duration(seconds: 3));
}); });

View File

@ -36,9 +36,8 @@ class OathScreen extends ConsumerWidget {
title: const Text('Authenticator'), title: const Text('Authenticator'),
cause: error, cause: error,
), ),
data: (oathState) => oathState.locked data: (oathState) =>
? _LockedView(devicePath, oathState) oathState.locked ? _LockedView(devicePath, oathState) : _UnlockedView(devicePath, oathState),
: _UnlockedView(devicePath, oathState),
); );
} }
} }
@ -64,8 +63,7 @@ class _LockedView extends ConsumerWidget {
action: (context) { action: (context) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => builder: (context) => ManagePasswordDialog(devicePath, oathState),
ManagePasswordDialog(devicePath, oathState),
); );
}, },
), ),
@ -124,8 +122,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isEmpty = ref.watch(credentialListProvider(widget.devicePath) final isEmpty = ref.watch(credentialListProvider(widget.devicePath).select((value) => value?.isEmpty == true));
.select((value) => value?.isEmpty == true));
if (isEmpty) { if (isEmpty) {
return MessagePage( return MessagePage(
title: const Text('Authenticator'), title: const Text('Authenticator'),
@ -137,8 +134,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
return Actions( return Actions(
actions: { actions: {
SearchIntent: CallbackAction(onInvoke: (_) { SearchIntent: CallbackAction(onInvoke: (_) {
searchController.selection = TextSelection( searchController.selection = TextSelection(baseOffset: 0, extentOffset: searchController.text.length);
baseOffset: 0, extentOffset: searchController.text.length);
searchFocus.requestFocus(); searchFocus.requestFocus();
return null; return null;
}), }),
@ -191,6 +187,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
List<Widget> _buildActions(BuildContext context, bool isEmpty) { List<Widget> _buildActions(BuildContext context, bool isEmpty) {
return [ return [
OutlinedButton.icon( OutlinedButton.icon(
key: const Key('add oath account'),
style: isEmpty ? AppTheme.primaryOutlinedButtonStyle(context) : null, style: isEmpty ? AppTheme.primaryOutlinedButtonStyle(context) : null,
label: const Text('Add account'), label: const Text('Add account'),
icon: const Icon(Icons.person_add_alt_1), icon: const Icon(Icons.person_add_alt_1),
@ -211,14 +208,12 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
onPressed: () { onPressed: () {
showBottomMenu(context, [ showBottomMenu(context, [
MenuAction( MenuAction(
text: text: widget.oathState.hasKey ? 'Manage password' : 'Set password',
widget.oathState.hasKey ? 'Manage password' : 'Set password',
icon: const Icon(Icons.password), icon: const Icon(Icons.password),
action: (context) { action: (context) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => builder: (context) => ManagePasswordDialog(widget.devicePath, widget.oathState),
ManagePasswordDialog(widget.devicePath, widget.oathState),
); );
}, },
), ),
@ -287,6 +282,7 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
), ),
const SizedBox(height: 16.0), const SizedBox(height: 16.0),
TextField( TextField(
key: const Key('oath password'),
controller: _passwordController, controller: _passwordController,
autofocus: true, autofocus: true,
obscureText: _isObscure, obscureText: _isObscure,
@ -339,6 +335,7 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: ElevatedButton( child: ElevatedButton(
key: const Key('oath unlock'),
onPressed: _passwordController.text.isNotEmpty ? _submit : null, onPressed: _passwordController.text.isNotEmpty ? _submit : null,
child: const Text('Unlock'), child: const Text('Unlock'),
), ),