This commit is contained in:
Adam Velebil 2022-10-04 10:26:55 +02:00
commit b041d4cca6
No known key found for this signature in database
GPG Key ID: AC6D6B9D715FC084
8 changed files with 151 additions and 9 deletions

View File

@ -31,7 +31,7 @@ from yubikit.core import require_version, NotSupportedError, TRANSPORT
from yubikit.core.smartcard import SmartCardConnection
from yubikit.core.otp import OtpConnection
from yubikit.core.fido import FidoConnection
from yubikit.management import ManagementSession, DeviceConfig, Mode
from yubikit.management import ManagementSession, DeviceConfig, Mode, CAPABILITY
from ykman.device import list_all_devices
from dataclasses import asdict
from time import sleep
@ -62,7 +62,7 @@ class ManagementNode(RpcNode):
return actions
def _await_reboot(self, serial, usb_enabled):
ifaces = usb_enabled.usb_interfaces
ifaces = CAPABILITY(usb_enabled or 0).usb_interfaces
# Prefer to use the "same" connection type as before
if self._connection_type.usb_interface in ifaces:

View File

@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:yubico_authenticator/app/views/keys.dart' as app_keys;
import 'package:yubico_authenticator/management/views/keys.dart'
as management_keys;
import 'test_util.dart';
Key _getCapabilityWidgetKey(bool isUsb, String name) =>
Key('management.keys.capability.${isUsb ? 'usb' : 'nfc'}.$name');
Future<FilterChip?> _getCapabilityWidget(Key key) async {
return find.byKey(key).hitTestable().evaluate().single.widget as FilterChip;
}
void main() {
var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
group('Management UI tests', () {
appTest('Drawer items exist', (WidgetTester tester) async {
await tester.openDrawer();
expect(find.byKey(app_keys.managementAppDrawer), findsOneWidget);
});
});
group('Change OTP', () {
appTest('Disable OTP', (WidgetTester tester) async {
await tester.openManagementScreen();
// find USB OTP capability
var usbOtpKey = _getCapabilityWidgetKey(true, 'OTP');
var otpChip = await _getCapabilityWidget(usbOtpKey);
if (otpChip != null) {
// we expect OTP to be enabled on the Key for this test
expect(otpChip.selected, equals(true));
await tester.tap(find.byKey(usbOtpKey));
await tester.pump(const Duration(milliseconds: 500));
await tester.tap(find.byKey(management_keys.saveButtonKey));
// long wait
await tester.pump(const Duration(milliseconds: 2500));
// no management screen visible now
expect(find.byKey(management_keys.screenKey), findsNothing);
await tester.longWait();
}
});
appTest('Enable OTP', (WidgetTester tester) async {
await tester.openManagementScreen();
// find USB OTP capability
var usbOtpKey = _getCapabilityWidgetKey(true, 'OTP');
var otpChip = await _getCapabilityWidget(usbOtpKey);
if (otpChip != null) {
expect(otpChip.selected, equals(false));
await tester.tap(find.byKey(usbOtpKey));
await tester.pump(const Duration(milliseconds: 500));
await tester.tap(find.byKey(management_keys.saveButtonKey));
// long wait
await tester.pump(const Duration(milliseconds: 2500));
// no management screen visible now
expect(find.byKey(management_keys.screenKey), findsNothing);
await tester.longWait();
}
});
});
}

View File

@ -3,7 +3,9 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:yubico_authenticator/app/views/device_button.dart';
import 'package:yubico_authenticator/app/views/keys.dart' as app_keys;
import 'package:yubico_authenticator/app/views/keys.dart';
import 'package:yubico_authenticator/core/state.dart';
import 'package:yubico_authenticator/management/views/keys.dart';
import 'android/util.dart' as android_test_util;
import 'approved_yubikeys.dart';
@ -67,6 +69,42 @@ extension AppWidgetTester on WidgetTester {
await pump(const Duration(milliseconds: 500));
}
/// Drawer helpers
bool hasDrawer() => scaffoldGlobalKey.currentState!.hasDrawer;
/// Open drawer
Future<void> openDrawer() async {
if (hasDrawer()) {
scaffoldGlobalKey.currentState!.openDrawer();
await pump(const Duration(milliseconds: 500));
}
}
/// Close drawer
Future<void> closeDrawer() async {
if (hasDrawer()) {
scaffoldGlobalKey.currentState!.closeDrawer();
await pump(const Duration(milliseconds: 500));
}
}
/// Is drawer opened?
/// If there is no drawer say it is open (all items are available)
bool isDrawerOpened() =>
hasDrawer() == false || scaffoldGlobalKey.currentState!.isDrawerOpen;
/// Management screen
Future<void> openManagementScreen() async {
if (!isDrawerOpened()) {
await openDrawer();
}
await tap(find.byKey(managementAppDrawer));
await pump(const Duration(milliseconds: 500));
expect(find.byKey(screenKey), findsOneWidget);
}
Future<void> startUp([Map<dynamic, dynamic> startUpParams = const {}]) async {
var result = isAndroid == true
? await android_test_util.startUp(this, startUpParams)

View File

@ -2,17 +2,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'device_button.dart';
import 'keys.dart';
import 'main_drawer.dart';
class AppPage extends ConsumerWidget {
final Key _scaffoldKey = GlobalKey();
final Widget? title;
final Widget child;
final List<Widget> actions;
final List<PopupMenuEntry> keyActions;
final bool centered;
final Widget Function(List<PopupMenuEntry>)? actionButtonBuilder;
AppPage({
const AppPage({
super.key,
this.title,
required this.child,
@ -82,7 +82,7 @@ class AppPage extends ConsumerWidget {
Scaffold _buildScaffold(BuildContext context, WidgetRef ref, bool hasDrawer) {
return Scaffold(
key: _scaffoldKey,
key: scaffoldGlobalKey,
appBar: AppBar(
titleSpacing: 8,
title: title,

View File

@ -1,6 +1,11 @@
import 'package:flutter/material.dart';
// global keys
final scaffoldGlobalKey = GlobalKey<ScaffoldState>();
const _prefix = 'app.keys';
const deviceInfoListTile = Key('$_prefix.device_info_list_tile');
const noDeviceAvatar = Key('$_prefix.no_device_avatar');
const noDeviceAvatar = Key('$_prefix.no_device_avatar');
// drawer items
const managementAppDrawer = Key('$_prefix.drawer.management');

View File

@ -11,6 +11,7 @@ import '../../settings_page.dart';
import '../message.dart';
import '../models.dart';
import '../state.dart';
import 'keys.dart';
extension on Application {
IconData get _icon {
@ -93,6 +94,7 @@ class MainPageDrawer extends ConsumerWidget {
titleText:
AppLocalizations.of(context)!.mainDrawer_txt_applications,
icon: Icon(Application.management._icon),
key: managementAppDrawer,
onTap: () {
if (shouldPop) Navigator.of(context).pop();
showBlurDialog(

View File

@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
const _prefix = 'management.keys';
// used to build Keys
const _capabilityKeyPrefix = '$_prefix.capability';
const usbCapabilityKeyPrefix = '$_capabilityKeyPrefix.usb';
const nfcCapabilityKeyPrefix = '$_capabilityKeyPrefix.nfc';
const screenKey = Key('$_prefix.screen.management');
const saveButtonKey = Key('$_prefix.management.btn.save');

View File

@ -1,7 +1,7 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:collection/collection.dart';
import '../../app/message.dart';
import '../../app/models.dart';
@ -11,14 +11,21 @@ import '../../widgets/custom_icons.dart';
import '../../widgets/responsive_dialog.dart';
import '../models.dart';
import '../state.dart';
import 'keys.dart' as management_keys;
final _mapEquals = const DeepCollectionEquality().equals;
enum _CapabilityType {
usb, nfc
}
class _CapabilityForm extends StatelessWidget {
final _CapabilityType type;
final int capabilities;
final int enabled;
final Function(int) onChanged;
const _CapabilityForm({
required this.type,
required this.capabilities,
required this.enabled,
required this.onChanged,
@ -26,6 +33,9 @@ class _CapabilityForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
final keyPrefix = (type == _CapabilityType.usb)
? management_keys.usbCapabilityKeyPrefix
: management_keys.nfcCapabilityKeyPrefix;
return Wrap(
spacing: 8,
runSpacing: 16,
@ -33,6 +43,7 @@ class _CapabilityForm extends StatelessWidget {
.where((c) => capabilities & c.value != 0)
.map((c) => FilterChip(
label: Text(c.name),
key: Key('$keyPrefix.${c.name}'),
selected: enabled & c.value != 0,
onSelected: (_) {
onChanged(enabled ^ c.value);
@ -94,6 +105,7 @@ class _CapabilitiesForm extends StatelessWidget {
horizontalTitleGap: 0,
),
_CapabilityForm(
type: _CapabilityType.usb,
capabilities: usbCapabilities,
enabled: enabled[Transport.usb] ?? 0,
onChanged: (value) {
@ -114,6 +126,7 @@ class _CapabilitiesForm extends StatelessWidget {
horizontalTitleGap: 0,
),
_CapabilityForm(
type: _CapabilityType.nfc,
capabilities: nfcCapabilities,
enabled: enabled[Transport.nfc] ?? 0,
onChanged: (value) {
@ -128,7 +141,9 @@ class _CapabilitiesForm extends StatelessWidget {
class ManagementScreen extends ConsumerStatefulWidget {
final YubiKeyData deviceData;
const ManagementScreen(this.deviceData, {super.key});
const ManagementScreen(this.deviceData)
: super(key: management_keys.screenKey);
@override
ConsumerState<ConsumerStatefulWidget> createState() =>
@ -285,6 +300,7 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
actions: [
TextButton(
onPressed: canSave ? _submitForm : null,
key: management_keys.saveButtonKey,
child: Text(AppLocalizations.of(context)!.mgmt_save),
),
],