diff --git a/integration_test/oath_test.dart b/integration_test/oath_test.dart index 7ea4fc6b..a73c479e 100644 --- a/integration_test/oath_test.dart +++ b/integration_test/oath_test.dart @@ -1,35 +1,10 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:yubico_authenticator/core/state.dart'; -import 'package:yubico_authenticator/oath/views/account_list.dart'; -import 'package:yubico_authenticator/oath/views/oath_screen.dart'; import 'oath_test_util.dart'; import 'test_util.dart'; -Future addDelay(int ms) async { - await Future.delayed(Duration(milliseconds: ms)); -} - -String generateIssuer(int index) { - return 'issuer_${index.toString().padLeft(4, '0')}'; -} - -String generateName(int index) { - return 'name_${index.toString().padLeft(4, '0')}'; -} - -String base32(int i) { - var m = (i % 32); - return m < 26 ? String.fromCharCode(65 + m) : '${2 + m - 26}'; -} - -/// generates 16 chars Base32 string -String generateSecret(int index) { - return List.generate(16, (_) => base32(index)).toString(); -} - void main() { var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; @@ -37,24 +12,24 @@ void main() { var startupParams = {}; if (isAndroid) { - // default android parameters + /// default android parameters startupParams = {'dlg.beta.enabled': false, 'delay.startup': 5}; testWidgets('Android app boot', (WidgetTester tester) async { - // delay first start + /// delay first start await tester.startUp(startupParams); - // remove delay.startup + + /// remove delay.startup startupParams = {'dlg.beta.enabled': false}; }); } - group('OATH UI tests', () { - // Validates that expected UI is present + group('OATH UI validation', () { testWidgets('Menu items exist', (WidgetTester tester) async { await tester.startUp(startupParams); await tester.tapDeviceButton(); - expect(find.byKey(OathDeviceMenu.addAccountKey), findsOneWidget); - expect(find.byKey(OathDeviceMenu.setManagePasswordKey), findsOneWidget); - expect(find.byKey(OathDeviceMenu.resetKey), findsOneWidget); + expect(find.byKey(deviceMenuAddAccountKey), findsOneWidget); + expect(find.byKey(deviceMenuSetManagePasswordKey), findsOneWidget); + expect(find.byKey(deviceMenuResetOathKey), findsOneWidget); }); }); @@ -82,7 +57,7 @@ void main() { await tester.addAccount(testAccount, quiet: false); }); - // deletes accounts created in previous test + /// deletes accounts created in previous test testWidgets('Delete account', (WidgetTester tester) async { await tester.startUp(startupParams); @@ -97,14 +72,14 @@ void main() { expect(await tester.findAccount(testAccount), isNull); }); - // adds an account, renames, verifies + /// adds an account, renames, verifies testWidgets('Rename account', (WidgetTester tester) async { await tester.startUp(startupParams); var testAccount = const Account(issuer: 'IssuerToRename', name: 'NameToRename'); - // delete account if it exists + /// delete account if it exists await tester.deleteAccount(testAccount); await tester.deleteAccount( const Account(issuer: 'RenamedIssuer', name: 'RenamedName')); @@ -115,8 +90,8 @@ void main() { }); group('OATH Password Quick tests', () { - // note that the password groups should be run as whole - // this is quick test as we cannot restart android app during 1 testrun + /// note that the password groups should be run as whole + /// this is quick test as we cannot restart android app during 1 testrun testWidgets('OATH: set oath password', (WidgetTester tester) async { await tester.startUp(startupParams); await tester.setOathPassword('aaa111'); @@ -124,189 +99,14 @@ void main() { /// note - we cannot 'restart' the app to [unlockOathApp] + /// testWidgets('OATH: replace oath password', (WidgetTester tester) async { + /// await tester.startUp(startupParams); + /// await tester.replaceOathPassword('aaa111', 'bbb222'); + /// }); + testWidgets('OATH: remove oath password', (WidgetTester tester) async { await tester.startUp(startupParams); await tester.removeOathPassword('aaa111'); }); }); - - group('OATH Password tests', skip: true, () { - /* - 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.startUp(); - - var firstPassword = 'aaa111'; - - await tester.tapSetOrManagePassword(); - - await tester.enterText( - find.byKey(const Key('new oath password')), firstPassword); - await tester.pump(); - await tester.enterText( - find.byKey(const Key('confirm oath password')), firstPassword); - await tester.pump(); - - await tester.tap(find.text('Save')); - await tester.pump(const Duration(milliseconds: 100)); - - await tester.pump(const Duration(milliseconds: 1000)); - }); - testWidgets('OATH: verify firstPassword', (WidgetTester tester) async { - await tester.startUp(); - - var firstPassword = 'aaa111'; - - await tester.enterText( - find.byKey(const Key('oath password')), firstPassword); - 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.startUp(); - - 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.tapSetOrManagePassword(); - - await tester.enterText( - find.byKey(const Key('current oath password')), firstPassword); - await tester.pump(); - await tester.enterText( - find.byKey(const Key('new oath password')), secondPassword); - - await tester.pump(); - await tester.enterText( - find.byKey(const Key('confirm oath password')), secondPassword); - await tester.pump(); - - await tester.tap(find.text('Save')); - await tester.pump(const Duration(milliseconds: 100)); - - await tester.pump(const Duration(milliseconds: 1000)); - }); - testWidgets('OATH: set thirdPassword', (WidgetTester tester) async { - await tester.startUp(); - - var secondPassword = 'bbb222'; - var thirdPassword = 'ccc333'; - - await tester.enterText( - find.byKey(const Key('oath password')), secondPassword); - await tester.pump(); - await tester.tap(find.byKey(const Key('oath unlock'))); - - await tester.tapSetOrManagePassword(); - - await tester.enterText( - find.byKey(const Key('current oath password')), secondPassword); - await tester.pump(); - 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.startUp(); - - var thirdPassword = 'ccc333'; - - await tester.enterText( - find.byKey(const Key('oath password')), thirdPassword); - await tester.pump(); - await tester.tap(find.byKey(const Key('oath unlock'))); - - await tester.tapSetOrManagePassword(); - - await tester.enterText( - find.byKey(const Key('current oath password')), thirdPassword); - await tester.pump(); - - await tester.tap(find.text('Remove password')); - - /// TODO: verification of state here: see that list of accounts is shown - await tester.pump(const Duration(milliseconds: 1000)); - }); - }); - group('TOTP tests', skip: true, () { - /* - Tests will verify all TOTP functionality, not yet though: - 1. Add 32 TOTP accounts - */ - testWidgets('TOTP: Add 32 accounts', skip: true, - (WidgetTester tester) async { - await tester.startUp(); - - for (var i = 0; i < 32; i++) { - await tester.tapAddAccount(); - - var issuer = generateIssuer(i); - var name = generateName(i); - var secret = generateSecret(i); - - await tester.enterText(find.byKey(const Key('issuer')), issuer); - await tester.pump(const Duration(milliseconds: 40)); - await tester.enterText(find.byKey(const Key('name')), name); - await tester.pump(const Duration(milliseconds: 40)); - await tester.enterText(find.byKey(const Key('secret')), secret); - - await tester.pump(const Duration(milliseconds: 100)); - - await tester.tap(find.byKey(const Key('save_btn'))); - - await tester.pump(const Duration(milliseconds: 500)); - - expect(find.byType(OathScreen), findsOneWidget); - - await tester.enterText( - find.byKey(const Key('search_accounts')), issuer); - - await tester.pump(const Duration(milliseconds: 100)); - - expect( - find.descendant( - of: find.byType(AccountList), - matching: find.textContaining(issuer)), - findsOneWidget); - - await tester.pump(const Duration(milliseconds: 50)); - } - await tester.pump(const Duration(milliseconds: 3000)); - /* - TODO: - await tester.tap(find.byType(FloatingActionButton)); - await tester.pump(const Duration(milliseconds: 500)); - - await tester.tap(find.text('Reset OATH')); - await tester.pump(const Duration(milliseconds: 500)); - - await tester.tap(find.text('Reset')); - await tester.pump(const Duration(milliseconds: 500)); - - */ - }); - }); } diff --git a/integration_test/oath_test_util.dart b/integration_test/oath_test_util.dart index c3163cfa..d7eaccd3 100644 --- a/integration_test/oath_test_util.dart +++ b/integration_test/oath_test_util.dart @@ -6,17 +6,11 @@ import 'package:yubico_authenticator/oath/views/account_view.dart'; import 'test_util.dart'; -class OathDeviceMenu { - static const addAccountKey = Key('add oath account'); - static const setManagePasswordKey = Key('set or manage oath password'); - static const resetKey = Key('reset oath app'); -} - -// when connecting YubiKey with OATH password +/// when connecting YubiKey with OATH password const passwordValidateEditKey = Key('oath password'); const unlockOathBtnKey = Key('oath unlock'); -// when setting or changing existing YubiKey OATH password +/// when setting or changing existing YubiKey OATH password const newOathPasswordEntryKey = Key('new oath password'); const currentOathPasswordEntryKey = Key('current oath password'); const confirmOathPasswordEditKey = Key('confirm oath password'); @@ -29,6 +23,10 @@ const renameAccountBtnSaveKey = Key('oath.dlg.rename_account.btn.save'); const renameAccountEditIssuerKey = Key('oath.dlg.rename_account.edit.issuer'); const renameAccountEditNameKey = Key('oath.dlg.rename_account.edit.name'); +const deviceMenuAddAccountKey = Key('add oath account'); +const deviceMenuSetManagePasswordKey = Key('set or manage oath password'); +const deviceMenuResetOathKey = Key('reset oath app'); + class Account { final String? issuer; final String name; @@ -48,14 +46,14 @@ extension OathFunctions on WidgetTester { /// Opens the device menu and taps the "Add account" menu item Future tapAddAccount() async { await tapDeviceButton(); - await tap(find.byKey(OathDeviceMenu.addAccountKey).hitTestable()); + await tap(find.byKey(deviceMenuAddAccountKey).hitTestable()); await longWait(); } /// Opens the device menu and taps the "Set/Manage password" menu item Future tapSetOrManagePassword() async { await tapDeviceButton(); - await tap(find.byKey(OathDeviceMenu.setManagePasswordKey)); + await tap(find.byKey(deviceMenuSetManagePasswordKey)); await longWait(); } @@ -69,8 +67,8 @@ extension OathFunctions on WidgetTester { await tapAddAccount(); if (isAndroid) { - // on android a QR Scanner starts - // we want to do a manual addition + /// on android a QR Scanner starts + /// we want to do a manual addition var manualEntryBtn = find.byKey(qrScannerEnterManuallyKey).hitTestable(); if (manualEntryBtn.evaluate().isEmpty) { printToConsole('Allow camera permission'); @@ -107,8 +105,8 @@ extension OathFunctions on WidgetTester { } Finder findAccountList() { - // cannot use hitTestable because Toasts block the Account list - var accountList = find.byType(AccountList).hitTestable(at: Alignment.topCenter); + var accountList = + find.byType(AccountList).hitTestable(at: Alignment.topCenter); expect(accountList, findsOneWidget); return accountList; } @@ -118,7 +116,7 @@ extension OathFunctions on WidgetTester { } Future findAccount(Account a, {bool quiet = true}) async { - // find an AccountView with issuer/name in the account list + /// find an AccountView with issuer/name in the account list var matchingAccounts = find.descendant( of: findAccountList(), matching: find.byWidgetPredicate( @@ -138,7 +136,7 @@ extension OathFunctions on WidgetTester { } }); - // return the AccountView if there is only one found + /// return the AccountView if there is only one found var evaluated = matchingAccounts.evaluate(); return evaluated.isEmpty ? null @@ -159,7 +157,7 @@ extension OathFunctions on WidgetTester { } Future deleteAccount(Account a, {bool quiet = true}) async { - // only delete account if it exists + /// only delete account if it exists var accountView = await findAccount(a); if (accountView == null) { testLog(quiet, 'Account to delete does not exist: $a'); @@ -172,13 +170,13 @@ extension OathFunctions on WidgetTester { await tap(deleteIconButton); await longWait(); - // TODO check dialog shows correct information about account + /// TODO check dialog shows correct information about account var deleteButton = find.byKey(deleteAccountBtnKey).hitTestable(); expect(deleteButton, findsOneWidget); await tap(deleteButton); await longWait(); - // try to find account + /// try to find account var deletedAccountView = await findAccount(a); expect(deletedAccountView, isNull); if (deletedAccountView == null) { @@ -204,7 +202,7 @@ extension OathFunctions on WidgetTester { await tap(renameIconButton); await longWait(); - // fill new info + /// fill new info var issuerTextField = find.byKey(renameAccountEditIssuerKey).hitTestable(); await tap(issuerTextField); await enterText(issuerTextField, newIssuer ?? ''); @@ -218,14 +216,14 @@ extension OathFunctions on WidgetTester { await tap(saveButton); await longWait(); - // now the account dialog is shown - // TODO verify it shows correct issuer and name + /// now the account dialog is shown + /// TODO verify it shows correct issuer and name - // close the account dialog by tapping out of it + /// close the account dialog by tapping out of it await tapAt(const Offset(10, 10)); await longWait(); - // verify accounts in the list + /// verify accounts in the list var renamedAccount = Account(issuer: newIssuer, name: newName); var renamedAccountView = await findAccount(renamedAccount); await shortWait(); @@ -254,7 +252,32 @@ extension OathFunctions on WidgetTester { await tap(find.byKey(oathPasswordSaveBntKey)); await longWait(); - // after tapping Save, the dialog is closed and the save button does not exist + /// after tapping Save, the dialog is closed and the save button does not exist + expect(find.byKey(oathPasswordSaveBntKey).hitTestable(), findsNothing); + } + + Future replaceOathPassword( + String currentPassword, String newPassword) async { + await tapSetOrManagePassword(); + + await shortWait(); + + var currentPasswordEntry = find.byKey(currentOathPasswordEntryKey); + await tap(currentPasswordEntry); + await enterText(currentPasswordEntry, currentPassword); + await shortWait(); + var newPasswordEntry = find.byKey(newOathPasswordEntryKey); + await tap(newPasswordEntry); + await enterText(newPasswordEntry, newPassword); + await shortWait(); + var confirmPasswordEntry = find.byKey(confirmOathPasswordEditKey); + await tap(confirmPasswordEntry); + await enterText(confirmPasswordEntry, newPassword); + await shortWait(); + + await tap(find.byKey(oathPasswordSaveBntKey)); + await longWait(); + expect(find.byKey(oathPasswordSaveBntKey).hitTestable(), findsNothing); } @@ -267,7 +290,6 @@ extension OathFunctions on WidgetTester { await tap(unlockButton); await longWait(); - // after unlocking, the unlock button is not hittable expect(find.byKey(unlockOathBtnKey).hitTestable(), findsNothing); } @@ -283,8 +305,6 @@ extension OathFunctions on WidgetTester { await tap(find.byKey(oathPasswordRemoveBntKey)); await longWait(); - // after tapping Save, the dialog is closed and the save button does not exist expect(find.byKey(oathPasswordRemoveBntKey).hitTestable(), findsNothing); } - } diff --git a/integration_test/test_util.dart b/integration_test/test_util.dart index a39a9ccf..7f5ae6dc 100644 --- a/integration_test/test_util.dart +++ b/integration_test/test_util.dart @@ -20,7 +20,6 @@ const veryLongWaitS = 10; // seconds extension AppWidgetTester on WidgetTester { - /// pumping Future shortestWait() async { await pump(const Duration(milliseconds: shortestWaitMs)); } @@ -48,7 +47,7 @@ extension AppWidgetTester on WidgetTester { if (isAndroid) { return AndroidTestUtils.startUp(this, startUpParams); } else { - // desktop + /// desktop return await pumpWidget( await getAuthenticatorApp(), const Duration(milliseconds: 2000)); }