mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-26 03:32:39 +03:00
Merge PR #240.
This commit is contained in:
commit
b041d4cca6
@ -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:
|
||||
|
70
integration_test/management_test.dart
Normal file
70
integration_test/management_test.dart
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
|
@ -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(
|
||||
|
11
lib/management/views/keys.dart
Normal file
11
lib/management/views/keys.dart
Normal 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');
|
@ -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),
|
||||
),
|
||||
],
|
||||
|
Loading…
Reference in New Issue
Block a user