yubioath-flutter/integration_test/oath_test_util.dart

336 lines
10 KiB
Dart
Raw Normal View History

2022-10-04 13:12:54 +03:00
/*
* Copyright (C) 2022 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
2022-09-12 07:34:49 +03:00
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:yubico_authenticator/core/state.dart';
2022-09-12 13:58:17 +03:00
import 'package:yubico_authenticator/oath/keys.dart' as keys;
2022-09-12 07:34:49 +03:00
import 'package:yubico_authenticator/oath/views/account_list.dart';
import 'package:yubico_authenticator/oath/views/account_view.dart';
import 'android/util.dart';
2022-09-12 07:34:49 +03:00
import 'test_util.dart';
class Account {
final String? issuer;
final String name;
final String secret;
const Account({
this.issuer,
this.name = '',
this.secret = 'abcdefghabcdefgh',
});
@override
String toString() => '$issuer/$name';
}
extension OathFunctions on WidgetTester {
/// Opens the device menu and taps the "Add account" menu item
Future<void> tapAddAccount() async {
2023-01-11 12:04:15 +03:00
await tapActionIconButton();
2022-09-12 13:58:17 +03:00
await tap(find.byKey(keys.addAccountAction).hitTestable());
2022-09-12 07:34:49 +03:00
await longWait();
}
/// Opens the device menu and taps the "Set/Manage password" menu item
Future<void> tapSetOrManagePassword() async {
2023-01-11 12:04:15 +03:00
await tapActionIconButton();
2022-09-12 13:58:17 +03:00
await tap(find.byKey(keys.setOrManagePasswordAction));
2022-09-12 07:34:49 +03:00
await longWait();
}
Future<void> addAccount(Account a, {bool quiet = true}) async {
var accountView = await findAccount(a);
if (accountView != null) {
testLog(quiet, 'Account already exists: $a');
return;
}
await tapAddAccount();
if (isAndroid) {
await grantCameraPermissions(this);
2022-09-12 07:34:49 +03:00
}
2022-09-12 13:58:17 +03:00
var issuerText = find.byKey(keys.issuerField).hitTestable();
2022-09-12 07:34:49 +03:00
await tap(issuerText);
await enterText(issuerText, a.issuer ?? '');
await shortWait();
2022-09-12 13:58:17 +03:00
var nameText = find.byKey(keys.nameField).hitTestable();
2022-09-12 07:34:49 +03:00
await tap(nameText);
await enterText(nameText, a.name);
await shortWait();
2022-09-12 13:58:17 +03:00
var secretText = find.byKey(keys.secretField).hitTestable();
2022-09-12 07:34:49 +03:00
await tap(secretText);
await enterText(secretText, a.secret);
await shortWait();
2022-09-12 13:58:17 +03:00
await tap(find.byKey(keys.saveButton));
2022-09-12 07:34:49 +03:00
/// TODO:
/// the following pump is because of NEO keys
await pump(const Duration(seconds: 1));
2022-09-12 07:34:49 +03:00
accountView = await findAccount(a);
expect(accountView, isNotNull);
if (accountView != null) {
testLog(quiet, 'Added account $a');
}
}
Finder findAccountList() {
2022-09-12 08:01:10 +03:00
var accountList =
find.byType(AccountList).hitTestable(at: Alignment.topCenter);
2022-09-12 07:34:49 +03:00
expect(accountList, findsOneWidget);
return accountList;
}
Future<AccountList?> getAccountList() async {
return findAccountList().evaluate().single.widget as AccountList;
}
Future<AccountView?> findAccount(Account a, {bool quiet = true}) async {
2022-09-12 13:58:17 +03:00
if (find.byKey(keys.noAccountsView).hitTestable().evaluate().isNotEmpty) {
/// if there is no OATH account on the YubiKey, the app shows
/// No accounts [MessagePage]
return null;
}
2023-01-02 20:02:32 +03:00
await shortWait();
2023-02-27 13:14:25 +03:00
2022-09-12 08:01:10 +03:00
/// find an AccountView with issuer/name in the account list
2022-09-12 07:34:49 +03:00
var matchingAccounts = find.descendant(
of: findAccountList(),
matching: find.byWidgetPredicate(
(widget) =>
widget is AccountView &&
widget.credential.name == a.name &&
widget.credential.issuer == a.issuer,
skipOffstage: false));
matchingAccounts.evaluate().forEach((element) {
var widget = element.widget;
if (widget is AccountView) {
testLog(quiet,
'Found ${widget.credential.issuer}/${widget.credential.name} matching account');
} else {
printToConsole('Unexpected widget type found: $widget');
}
});
2022-09-12 08:01:10 +03:00
/// return the AccountView if there is only one found
2022-09-12 07:34:49 +03:00
var evaluated = matchingAccounts.evaluate();
return evaluated.isEmpty
? null
: evaluated.length != 1
? null
: evaluated.single.widget as AccountView;
}
Future<void> openAccountDialog(Account a) async {
var accountView = await findAccount(a);
expect(accountView, isNotNull);
if (accountView != null) {
2023-02-27 13:14:25 +03:00
final accountFinder = find.byWidget(accountView);
await ensureVisible(accountFinder);
final codeButtonFinder = find.descendant(
of: accountFinder, matching: find.bySubtype<FilledButton>());
await tap(codeButtonFinder);
2022-09-12 07:34:49 +03:00
await shortWait();
}
}
Future<void> deleteAccount(Account a, {bool quiet = true}) async {
2022-09-12 08:01:10 +03:00
/// only delete account if it exists
2022-09-12 07:34:49 +03:00
var accountView = await findAccount(a);
if (accountView == null) {
testLog(quiet, 'Account to delete does not exist: $a');
return;
}
await openAccountDialog(a);
/// click the delete IconButton in the account dialog
2022-09-12 07:34:49 +03:00
var deleteIconButton = find.byIcon(Icons.delete_outline).hitTestable();
expect(deleteIconButton, findsOneWidget);
await tap(deleteIconButton);
await longWait();
2022-09-12 08:01:10 +03:00
/// TODO check dialog shows correct information about account
/// click the delete Button in the delete dialog
2022-09-12 13:58:17 +03:00
var deleteButton = find.byKey(keys.deleteButton).hitTestable();
2022-09-12 07:34:49 +03:00
expect(deleteButton, findsOneWidget);
await tap(deleteButton);
await longWait();
2023-01-02 20:02:32 +03:00
await longWait();
2022-09-12 07:34:49 +03:00
2022-09-12 08:01:10 +03:00
/// try to find account
2022-09-12 07:34:49 +03:00
var deletedAccountView = await findAccount(a);
expect(deletedAccountView, isNull);
if (deletedAccountView == null) {
testLog(quiet, 'Deleted account $a');
}
}
Future<void> renameAccount(
Account a,
String? newIssuer,
String newName, {
bool quiet = true,
}) async {
var accountView = await findAccount(a);
if (accountView == null) {
testLog(quiet, 'Account to rename does not exist: $a');
return;
}
await openAccountDialog(a);
var renameIconButton = find.byIcon(Icons.edit_outlined).hitTestable();
/// only newer FW supports renaming
/// TODO verify this is correct for the FW of the YubiKey
if (renameIconButton.evaluate().isEmpty) {
/// close the dialog and return
2023-01-02 20:02:32 +03:00
testLog(false, 'This YubiKey does not support account renaming');
await tapAt(const Offset(10, 10));
await shortWait();
return;
}
2022-09-12 07:34:49 +03:00
expect(renameIconButton, findsOneWidget);
await tap(renameIconButton);
await longWait();
2022-09-12 08:01:10 +03:00
/// fill new info
2022-09-12 13:58:17 +03:00
var issuerTextField = find.byKey(keys.issuerField).hitTestable();
2022-09-12 07:34:49 +03:00
await tap(issuerTextField);
await enterText(issuerTextField, newIssuer ?? '');
2022-09-12 13:58:17 +03:00
var nameTextField = find.byKey(keys.nameField).hitTestable();
2022-09-12 07:34:49 +03:00
await tap(nameTextField);
await enterText(nameTextField, newName);
await longWait();
2022-09-12 07:34:49 +03:00
2022-09-12 13:58:17 +03:00
var saveButton = find.byKey(keys.saveButton).hitTestable();
2022-09-12 07:34:49 +03:00
expect(saveButton, findsOneWidget);
await tap(saveButton);
await longWait();
2022-09-12 08:01:10 +03:00
/// now the account dialog is shown
/// TODO verify it shows correct issuer and name
2022-09-12 07:34:49 +03:00
2022-09-12 08:01:10 +03:00
/// close the account dialog by tapping out of it
2022-09-12 07:34:49 +03:00
await tapAt(const Offset(10, 10));
await longWait();
2022-09-12 08:01:10 +03:00
/// verify accounts in the list
2022-09-12 07:34:49 +03:00
var renamedAccount = Account(issuer: newIssuer, name: newName);
var renamedAccountView = await findAccount(renamedAccount);
await longWait();
2022-09-12 07:34:49 +03:00
var originalAccountView = await findAccount(a);
expect(renamedAccountView, isNotNull);
expect(originalAccountView, isNull);
if (renamedAccountView != null && originalAccountView == null) {
testLog(quiet, 'Renamed account from $a to $renamedAccount');
}
}
Future<void> setOathPassword(String newPassword) async {
await tapSetOrManagePassword();
await longWait();
2022-09-12 13:58:17 +03:00
var newPasswordEntry = find.byKey(keys.newPasswordField);
2022-09-12 07:34:49 +03:00
await tap(newPasswordEntry);
await enterText(newPasswordEntry, newPassword);
await shortWait();
2022-09-12 13:58:17 +03:00
var confirmPasswordEntry = find.byKey(keys.confirmPasswordField);
2022-09-12 07:34:49 +03:00
await tap(confirmPasswordEntry);
await enterText(confirmPasswordEntry, newPassword);
await shortWait();
2022-09-12 13:58:17 +03:00
await tap(find.byKey(keys.savePasswordButton));
/// TODO:
/// the following pause is because of NEO keys
await pump(const Duration(seconds: 1));
2022-09-12 07:34:49 +03:00
2022-09-12 08:01:10 +03:00
/// after tapping Save, the dialog is closed and the save button does not exist
2022-09-12 13:58:17 +03:00
expect(find.byKey(keys.savePasswordButton).hitTestable(), findsNothing);
2022-09-12 08:01:10 +03:00
}
Future<void> replaceOathPassword(
String currentPassword, String newPassword) async {
await tapSetOrManagePassword();
await shortWait();
2022-09-12 13:58:17 +03:00
var currentPasswordEntry = find.byKey(keys.currentPasswordField);
2022-09-12 08:01:10 +03:00
await tap(currentPasswordEntry);
await enterText(currentPasswordEntry, currentPassword);
await shortWait();
2022-09-12 13:58:17 +03:00
var newPasswordEntry = find.byKey(keys.newPasswordField);
2022-09-12 08:01:10 +03:00
await tap(newPasswordEntry);
await enterText(newPasswordEntry, newPassword);
await shortWait();
2022-09-12 13:58:17 +03:00
var confirmPasswordEntry = find.byKey(keys.confirmPasswordField);
2022-09-12 08:01:10 +03:00
await tap(confirmPasswordEntry);
await enterText(confirmPasswordEntry, newPassword);
await shortWait();
2022-09-12 13:58:17 +03:00
await tap(find.byKey(keys.savePasswordButton));
2022-09-12 08:01:10 +03:00
await longWait();
2022-09-12 13:58:17 +03:00
expect(find.byKey(keys.savePasswordButton).hitTestable(), findsNothing);
2022-09-12 07:34:49 +03:00
}
Future<void> unlockOathSession(String newPassword) async {
2022-09-12 13:58:17 +03:00
var validatePasswordEntry = find.byKey(keys.passwordField);
2022-09-12 07:34:49 +03:00
await tap(validatePasswordEntry);
await enterText(validatePasswordEntry, newPassword);
await shortWait();
2022-09-12 13:58:17 +03:00
var unlockButton = find.byKey(keys.unlockButton);
2022-09-12 07:34:49 +03:00
await tap(unlockButton);
2023-01-02 20:02:32 +03:00
/// TODO:
/// the following pump is because of NEO keys
await pump(const Duration(seconds: 1));
2022-09-12 07:34:49 +03:00
2022-09-12 13:58:17 +03:00
expect(find.byKey(keys.unlockButton).hitTestable(), findsNothing);
2022-09-12 07:34:49 +03:00
}
Future<void> removeOathPassword(String currentPassword) async {
await tapSetOrManagePassword();
await longWait();
2022-09-12 13:58:17 +03:00
var currentPasswordEntry = find.byKey(keys.currentPasswordField);
2022-09-12 07:34:49 +03:00
await tap(currentPasswordEntry);
await enterText(currentPasswordEntry, currentPassword);
await shortWait();
2022-09-12 13:58:17 +03:00
await tap(find.byKey(keys.removePasswordButton));
2023-01-02 20:02:32 +03:00
/// TODO:
/// the following pump is because of NEO keys
await pump(const Duration(seconds: 1));
2022-09-12 07:34:49 +03:00
2022-09-12 13:58:17 +03:00
expect(find.byKey(keys.removePasswordButton).hitTestable(), findsNothing);
2022-09-12 07:34:49 +03:00
}
}