This commit is contained in:
Adam Velebil 2022-09-12 07:01:10 +02:00
parent ec232f2c9c
commit 605e70c7c9
No known key found for this signature in database
GPG Key ID: AC6D6B9D715FC084
3 changed files with 67 additions and 248 deletions

View File

@ -1,35 +1,10 @@
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/core/state.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 'oath_test_util.dart';
import 'test_util.dart'; import 'test_util.dart';
Future<void> addDelay(int ms) async {
await Future<void>.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() { void main() {
var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
@ -37,24 +12,24 @@ void main() {
var startupParams = {}; var startupParams = {};
if (isAndroid) { if (isAndroid) {
// default android parameters /// default android parameters
startupParams = {'dlg.beta.enabled': false, 'delay.startup': 5}; startupParams = {'dlg.beta.enabled': false, 'delay.startup': 5};
testWidgets('Android app boot', (WidgetTester tester) async { testWidgets('Android app boot', (WidgetTester tester) async {
// delay first start /// delay first start
await tester.startUp(startupParams); await tester.startUp(startupParams);
// remove delay.startup
/// remove delay.startup
startupParams = {'dlg.beta.enabled': false}; startupParams = {'dlg.beta.enabled': false};
}); });
} }
group('OATH UI tests', () { group('OATH UI validation', () {
// Validates that expected UI is present
testWidgets('Menu items exist', (WidgetTester tester) async { testWidgets('Menu items exist', (WidgetTester tester) async {
await tester.startUp(startupParams); await tester.startUp(startupParams);
await tester.tapDeviceButton(); await tester.tapDeviceButton();
expect(find.byKey(OathDeviceMenu.addAccountKey), findsOneWidget); expect(find.byKey(deviceMenuAddAccountKey), findsOneWidget);
expect(find.byKey(OathDeviceMenu.setManagePasswordKey), findsOneWidget); expect(find.byKey(deviceMenuSetManagePasswordKey), findsOneWidget);
expect(find.byKey(OathDeviceMenu.resetKey), findsOneWidget); expect(find.byKey(deviceMenuResetOathKey), findsOneWidget);
}); });
}); });
@ -82,7 +57,7 @@ void main() {
await tester.addAccount(testAccount, quiet: false); await tester.addAccount(testAccount, quiet: false);
}); });
// deletes accounts created in previous test /// deletes accounts created in previous test
testWidgets('Delete account', (WidgetTester tester) async { testWidgets('Delete account', (WidgetTester tester) async {
await tester.startUp(startupParams); await tester.startUp(startupParams);
@ -97,14 +72,14 @@ void main() {
expect(await tester.findAccount(testAccount), isNull); expect(await tester.findAccount(testAccount), isNull);
}); });
// adds an account, renames, verifies /// adds an account, renames, verifies
testWidgets('Rename account', (WidgetTester tester) async { testWidgets('Rename account', (WidgetTester tester) async {
await tester.startUp(startupParams); await tester.startUp(startupParams);
var testAccount = var testAccount =
const Account(issuer: 'IssuerToRename', name: 'NameToRename'); const Account(issuer: 'IssuerToRename', name: 'NameToRename');
// delete account if it exists /// delete account if it exists
await tester.deleteAccount(testAccount); await tester.deleteAccount(testAccount);
await tester.deleteAccount( await tester.deleteAccount(
const Account(issuer: 'RenamedIssuer', name: 'RenamedName')); const Account(issuer: 'RenamedIssuer', name: 'RenamedName'));
@ -115,8 +90,8 @@ void main() {
}); });
group('OATH Password Quick tests', () { group('OATH Password Quick tests', () {
// note that the password groups should be run as whole /// note that the password groups should be run as whole
// this is quick test as we cannot restart android app during 1 testrun /// this is quick test as we cannot restart android app during 1 testrun
testWidgets('OATH: set oath password', (WidgetTester tester) async { testWidgets('OATH: set oath password', (WidgetTester tester) async {
await tester.startUp(startupParams); await tester.startUp(startupParams);
await tester.setOathPassword('aaa111'); await tester.setOathPassword('aaa111');
@ -124,189 +99,14 @@ void main() {
/// note - we cannot 'restart' the app to [unlockOathApp] /// 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 { testWidgets('OATH: remove oath password', (WidgetTester tester) async {
await tester.startUp(startupParams); await tester.startUp(startupParams);
await tester.removeOathPassword('aaa111'); 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));
*/
});
});
} }

View File

@ -6,17 +6,11 @@ import 'package:yubico_authenticator/oath/views/account_view.dart';
import 'test_util.dart'; import 'test_util.dart';
class OathDeviceMenu { /// when connecting YubiKey with OATH password
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
const passwordValidateEditKey = Key('oath password'); const passwordValidateEditKey = Key('oath password');
const unlockOathBtnKey = Key('oath unlock'); 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 newOathPasswordEntryKey = Key('new oath password');
const currentOathPasswordEntryKey = Key('current oath password'); const currentOathPasswordEntryKey = Key('current oath password');
const confirmOathPasswordEditKey = Key('confirm 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 renameAccountEditIssuerKey = Key('oath.dlg.rename_account.edit.issuer');
const renameAccountEditNameKey = Key('oath.dlg.rename_account.edit.name'); 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 { class Account {
final String? issuer; final String? issuer;
final String name; final String name;
@ -48,14 +46,14 @@ extension OathFunctions on WidgetTester {
/// Opens the device menu and taps the "Add account" menu item /// Opens the device menu and taps the "Add account" menu item
Future<void> tapAddAccount() async { Future<void> tapAddAccount() async {
await tapDeviceButton(); await tapDeviceButton();
await tap(find.byKey(OathDeviceMenu.addAccountKey).hitTestable()); await tap(find.byKey(deviceMenuAddAccountKey).hitTestable());
await longWait(); await longWait();
} }
/// Opens the device menu and taps the "Set/Manage password" menu item /// Opens the device menu and taps the "Set/Manage password" menu item
Future<void> tapSetOrManagePassword() async { Future<void> tapSetOrManagePassword() async {
await tapDeviceButton(); await tapDeviceButton();
await tap(find.byKey(OathDeviceMenu.setManagePasswordKey)); await tap(find.byKey(deviceMenuSetManagePasswordKey));
await longWait(); await longWait();
} }
@ -69,8 +67,8 @@ extension OathFunctions on WidgetTester {
await tapAddAccount(); await tapAddAccount();
if (isAndroid) { if (isAndroid) {
// on android a QR Scanner starts /// on android a QR Scanner starts
// we want to do a manual addition /// we want to do a manual addition
var manualEntryBtn = find.byKey(qrScannerEnterManuallyKey).hitTestable(); var manualEntryBtn = find.byKey(qrScannerEnterManuallyKey).hitTestable();
if (manualEntryBtn.evaluate().isEmpty) { if (manualEntryBtn.evaluate().isEmpty) {
printToConsole('Allow camera permission'); printToConsole('Allow camera permission');
@ -107,8 +105,8 @@ extension OathFunctions on WidgetTester {
} }
Finder findAccountList() { Finder findAccountList() {
// cannot use hitTestable because Toasts block the Account list var accountList =
var accountList = find.byType(AccountList).hitTestable(at: Alignment.topCenter); find.byType(AccountList).hitTestable(at: Alignment.topCenter);
expect(accountList, findsOneWidget); expect(accountList, findsOneWidget);
return accountList; return accountList;
} }
@ -118,7 +116,7 @@ extension OathFunctions on WidgetTester {
} }
Future<AccountView?> findAccount(Account a, {bool quiet = true}) async { Future<AccountView?> 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( var matchingAccounts = find.descendant(
of: findAccountList(), of: findAccountList(),
matching: find.byWidgetPredicate( 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(); var evaluated = matchingAccounts.evaluate();
return evaluated.isEmpty return evaluated.isEmpty
? null ? null
@ -159,7 +157,7 @@ extension OathFunctions on WidgetTester {
} }
Future<void> deleteAccount(Account a, {bool quiet = true}) async { Future<void> deleteAccount(Account a, {bool quiet = true}) async {
// only delete account if it exists /// only delete account if it exists
var accountView = await findAccount(a); var accountView = await findAccount(a);
if (accountView == null) { if (accountView == null) {
testLog(quiet, 'Account to delete does not exist: $a'); testLog(quiet, 'Account to delete does not exist: $a');
@ -172,13 +170,13 @@ extension OathFunctions on WidgetTester {
await tap(deleteIconButton); await tap(deleteIconButton);
await longWait(); await longWait();
// TODO check dialog shows correct information about account /// TODO check dialog shows correct information about account
var deleteButton = find.byKey(deleteAccountBtnKey).hitTestable(); var deleteButton = find.byKey(deleteAccountBtnKey).hitTestable();
expect(deleteButton, findsOneWidget); expect(deleteButton, findsOneWidget);
await tap(deleteButton); await tap(deleteButton);
await longWait(); await longWait();
// try to find account /// try to find account
var deletedAccountView = await findAccount(a); var deletedAccountView = await findAccount(a);
expect(deletedAccountView, isNull); expect(deletedAccountView, isNull);
if (deletedAccountView == null) { if (deletedAccountView == null) {
@ -204,7 +202,7 @@ extension OathFunctions on WidgetTester {
await tap(renameIconButton); await tap(renameIconButton);
await longWait(); await longWait();
// fill new info /// fill new info
var issuerTextField = find.byKey(renameAccountEditIssuerKey).hitTestable(); var issuerTextField = find.byKey(renameAccountEditIssuerKey).hitTestable();
await tap(issuerTextField); await tap(issuerTextField);
await enterText(issuerTextField, newIssuer ?? ''); await enterText(issuerTextField, newIssuer ?? '');
@ -218,14 +216,14 @@ extension OathFunctions on WidgetTester {
await tap(saveButton); await tap(saveButton);
await longWait(); await longWait();
// now the account dialog is shown /// now the account dialog is shown
// TODO verify it shows correct issuer and name /// 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 tapAt(const Offset(10, 10));
await longWait(); await longWait();
// verify accounts in the list /// verify accounts in the list
var renamedAccount = Account(issuer: newIssuer, name: newName); var renamedAccount = Account(issuer: newIssuer, name: newName);
var renamedAccountView = await findAccount(renamedAccount); var renamedAccountView = await findAccount(renamedAccount);
await shortWait(); await shortWait();
@ -254,7 +252,32 @@ extension OathFunctions on WidgetTester {
await tap(find.byKey(oathPasswordSaveBntKey)); await tap(find.byKey(oathPasswordSaveBntKey));
await longWait(); 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<void> 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); expect(find.byKey(oathPasswordSaveBntKey).hitTestable(), findsNothing);
} }
@ -267,7 +290,6 @@ extension OathFunctions on WidgetTester {
await tap(unlockButton); await tap(unlockButton);
await longWait(); await longWait();
// after unlocking, the unlock button is not hittable
expect(find.byKey(unlockOathBtnKey).hitTestable(), findsNothing); expect(find.byKey(unlockOathBtnKey).hitTestable(), findsNothing);
} }
@ -283,8 +305,6 @@ extension OathFunctions on WidgetTester {
await tap(find.byKey(oathPasswordRemoveBntKey)); await tap(find.byKey(oathPasswordRemoveBntKey));
await longWait(); await longWait();
// after tapping Save, the dialog is closed and the save button does not exist
expect(find.byKey(oathPasswordRemoveBntKey).hitTestable(), findsNothing); expect(find.byKey(oathPasswordRemoveBntKey).hitTestable(), findsNothing);
} }
} }

View File

@ -20,7 +20,6 @@ const veryLongWaitS = 10; // seconds
extension AppWidgetTester on WidgetTester { extension AppWidgetTester on WidgetTester {
/// pumping
Future<void> shortestWait() async { Future<void> shortestWait() async {
await pump(const Duration(milliseconds: shortestWaitMs)); await pump(const Duration(milliseconds: shortestWaitMs));
} }
@ -48,7 +47,7 @@ extension AppWidgetTester on WidgetTester {
if (isAndroid) { if (isAndroid) {
return AndroidTestUtils.startUp(this, startUpParams); return AndroidTestUtils.startUp(this, startUpParams);
} else { } else {
// desktop /// desktop
return await pumpWidget( return await pumpWidget(
await getAuthenticatorApp(), const Duration(milliseconds: 2000)); await getAuthenticatorApp(), const Duration(milliseconds: 2000));
} }