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: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<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() {
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));
*/
});
});
}

View File

@ -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<void> 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<void> 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<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(
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<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);
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<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);
}
@ -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);
}
}

View File

@ -20,7 +20,6 @@ const veryLongWaitS = 10; // seconds
extension AppWidgetTester on WidgetTester {
/// pumping
Future<void> 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));
}