diff --git a/lib/api/impl/privacy_idea_container_api.dart b/lib/api/impl/privacy_idea_container_api.dart index 3a7ab68b..c1ce6c28 100644 --- a/lib/api/impl/privacy_idea_container_api.dart +++ b/lib/api/impl/privacy_idea_container_api.dart @@ -175,7 +175,7 @@ class PiContainerApi implements TokenContainerApi { CONTAINER_CHAL_SIGNATURE: container.signMessage(signMessage), }; - final response = await _ioClient.doPost(url: requestUrl, body: body); + final response = await _ioClient.doPost(url: requestUrl, body: body, sslVerify: container.sslVerify); if (response.statusCode != 200) { final errorResponse = response.asPiErrorResponse(); if (errorResponse != null) throw errorResponse.piServerResultError; @@ -209,7 +209,7 @@ class PiContainerApi implements TokenContainerApi { CONTAINER_CHAL_SIGNATURE: container.signMessage('${challenge.nonce}|${challenge.timeStamp}|${container.serial}|$unregisterUrl'), }; - final response = await _ioClient.doPost(url: unregisterUrl, body: body); + final response = await _ioClient.doPost(url: unregisterUrl, body: body, sslVerify: container.sslVerify); final piResponse = response.asPiServerResponse(); final errorResponse = piResponse?.asError; @@ -227,10 +227,7 @@ class PiContainerApi implements TokenContainerApi { final body = { CONTAINER_SCOPE: requestUrl.toString(), }; - final challengeResponse = await _ioClient.doPost( - url: container.challengeUrl, - body: body, - ); + final challengeResponse = await _ioClient.doPost(url: container.challengeUrl, body: body, sslVerify: container.sslVerify); if (challengeResponse.statusCode != 200) { final errorResponse = challengeResponse.asPiErrorResponse(); if (errorResponse != null) throw errorResponse.piServerResultError; @@ -270,7 +267,7 @@ class PiContainerApi implements TokenContainerApi { CONTAINER_CHAL_SIGNATURE: signature, }; - final response = await _ioClient.doPost(url: container.syncUrl, body: body); + final response = await _ioClient.doPost(url: container.syncUrl, body: body, sslVerify: container.sslVerify); if (response.statusCode != 200) { final piErrorResponse = response.asPiErrorResponse(); if (piErrorResponse != null) throw piErrorResponse.piServerResultError; diff --git a/lib/interfaces/repo/token_container_repository.dart b/lib/interfaces/repo/token_container_repository.dart index a380571e..77132f48 100644 --- a/lib/interfaces/repo/token_container_repository.dart +++ b/lib/interfaces/repo/token_container_repository.dart @@ -23,7 +23,6 @@ import '../../model/token_container.dart'; abstract class TokenContainerRepository { Future loadContainerState(); Future saveContainerState(TokenContainerState containerState); - Future> loadContainerList(); Future saveContainerList(List containerList); Future deleteContainer(String serial); Future deleteAllContainer(); diff --git a/lib/model/pi_server_response.dart b/lib/model/pi_server_response.dart index 33afd6df..b7cf3d3c 100644 --- a/lib/model/pi_server_response.dart +++ b/lib/model/pi_server_response.dart @@ -48,6 +48,7 @@ class PiServerResponse with _$PiServerResponse { const PiServerResponse._(); factory PiServerResponse.success({ + required int statusCode, required dynamic detail, required int id, required String jsonrpc, @@ -61,6 +62,7 @@ class PiServerResponse with _$PiServerResponse { PiSuccessResponse? get asSuccess => this is PiSuccessResponse ? this as PiSuccessResponse : null; factory PiServerResponse.error({ + required int statusCode, required dynamic detail, required int id, required String jsonrpc, @@ -75,7 +77,7 @@ class PiServerResponse with _$PiServerResponse { bool get isError => this is PiErrorResponse; PiErrorResponse? get asError => this is PiErrorResponse ? this as PiErrorResponse : null; - factory PiServerResponse.fromJson(Map json) { + factory PiServerResponse.fromJson(Map json, {int statisCode = 200}) { Logger.debug('Received container sync response: $json'); final map = validateMap( map: json, @@ -102,6 +104,7 @@ class PiServerResponse with _$PiServerResponse { ); if (result[RESULT_STATUS] == true && result.containsKey(RESULT_VALUE)) { return PiServerResponse.success( + statusCode: statisCode, id: map[ID], jsonrpc: map[JSONRPC], resultValue: PiServerResultValue.fromJsonOfType(result[RESULT_VALUE]), @@ -114,6 +117,7 @@ class PiServerResponse with _$PiServerResponse { } if (result[RESULT_STATUS] == false && result.containsKey(RESULT_ERROR)) { return PiServerResponse.error( + statusCode: statisCode, detail: map[DETAIL], id: json[ID], jsonrpc: map[JSONRPC], @@ -132,6 +136,6 @@ class PiServerResponse with _$PiServerResponse { } factory PiServerResponse.fromResponse(Response response) { - return PiServerResponse.fromJson(jsonDecode(response.body)); + return PiServerResponse.fromJson(jsonDecode(response.body), statisCode: response.statusCode); } } diff --git a/lib/model/pi_server_response.freezed.dart b/lib/model/pi_server_response.freezed.dart index 1ea193e9..84b7f7ae 100644 --- a/lib/model/pi_server_response.freezed.dart +++ b/lib/model/pi_server_response.freezed.dart @@ -16,6 +16,7 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$PiServerResponse { + int get statusCode => throw _privateConstructorUsedError; dynamic get detail => throw _privateConstructorUsedError; int get id => throw _privateConstructorUsedError; String get jsonrpc => throw _privateConstructorUsedError; @@ -26,6 +27,7 @@ mixin _$PiServerResponse { @optionalTypeArgs TResult when({ required TResult Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -36,6 +38,7 @@ mixin _$PiServerResponse { String signature) success, required TResult Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -50,6 +53,7 @@ mixin _$PiServerResponse { @optionalTypeArgs TResult? whenOrNull({ TResult? Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -60,6 +64,7 @@ mixin _$PiServerResponse { String signature)? success, TResult? Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -74,6 +79,7 @@ mixin _$PiServerResponse { @optionalTypeArgs TResult maybeWhen({ TResult Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -84,6 +90,7 @@ mixin _$PiServerResponse { String signature)? success, TResult Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -130,7 +137,8 @@ abstract class $PiServerResponseCopyWith { _$PiServerResponseCopyWithImpl>; @useResult $Res call( - {dynamic detail, + {int statusCode, + dynamic detail, int id, String jsonrpc, double time, @@ -155,6 +163,7 @@ class _$PiServerResponseCopyWithImpl @pragma('vm:prefer-inline') @override $Res call({ + Object? statusCode = null, Object? detail = freezed, Object? id = null, Object? jsonrpc = null, @@ -238,6 +253,10 @@ class __$$PiSuccessResponseImplCopyWithImpl Object? signature = null, }) { return _then(_$PiSuccessResponseImpl( + statusCode: null == statusCode + ? _value.statusCode + : statusCode // ignore: cast_nullable_to_non_nullable + as int, detail: freezed == detail ? _value.detail : detail // ignore: cast_nullable_to_non_nullable @@ -279,7 +298,8 @@ class __$$PiSuccessResponseImplCopyWithImpl class _$PiSuccessResponseImpl extends PiSuccessResponse { _$PiSuccessResponseImpl( - {required this.detail, + {required this.statusCode, + required this.detail, required this.id, required this.jsonrpc, required this.resultValue, @@ -289,6 +309,8 @@ class _$PiSuccessResponseImpl required this.signature}) : super._(); + @override + final int statusCode; @override final dynamic detail; @override @@ -308,7 +330,7 @@ class _$PiSuccessResponseImpl @override String toString() { - return 'PiServerResponse<$T>.success(detail: $detail, id: $id, jsonrpc: $jsonrpc, resultValue: $resultValue, time: $time, version: $version, versionNumber: $versionNumber, signature: $signature)'; + return 'PiServerResponse<$T>.success(statusCode: $statusCode, detail: $detail, id: $id, jsonrpc: $jsonrpc, resultValue: $resultValue, time: $time, version: $version, versionNumber: $versionNumber, signature: $signature)'; } @override @@ -316,6 +338,8 @@ class _$PiSuccessResponseImpl return identical(this, other) || (other.runtimeType == runtimeType && other is _$PiSuccessResponseImpl && + (identical(other.statusCode, statusCode) || + other.statusCode == statusCode) && const DeepCollectionEquality().equals(other.detail, detail) && (identical(other.id, id) || other.id == id) && (identical(other.jsonrpc, jsonrpc) || other.jsonrpc == jsonrpc) && @@ -332,6 +356,7 @@ class _$PiSuccessResponseImpl @override int get hashCode => Object.hash( runtimeType, + statusCode, const DeepCollectionEquality().hash(detail), id, jsonrpc, @@ -355,6 +380,7 @@ class _$PiSuccessResponseImpl @optionalTypeArgs TResult when({ required TResult Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -365,6 +391,7 @@ class _$PiSuccessResponseImpl String signature) success, required TResult Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -375,7 +402,7 @@ class _$PiSuccessResponseImpl String signature) error, }) { - return success(detail, id, jsonrpc, resultValue, time, version, + return success(statusCode, detail, id, jsonrpc, resultValue, time, version, versionNumber, signature); } @@ -383,6 +410,7 @@ class _$PiSuccessResponseImpl @optionalTypeArgs TResult? whenOrNull({ TResult? Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -393,6 +421,7 @@ class _$PiSuccessResponseImpl String signature)? success, TResult? Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -403,14 +432,15 @@ class _$PiSuccessResponseImpl String signature)? error, }) { - return success?.call(detail, id, jsonrpc, resultValue, time, version, - versionNumber, signature); + return success?.call(statusCode, detail, id, jsonrpc, resultValue, time, + version, versionNumber, signature); } @override @optionalTypeArgs TResult maybeWhen({ TResult Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -421,6 +451,7 @@ class _$PiSuccessResponseImpl String signature)? success, TResult Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -433,8 +464,8 @@ class _$PiSuccessResponseImpl required TResult orElse(), }) { if (success != null) { - return success(detail, id, jsonrpc, resultValue, time, version, - versionNumber, signature); + return success(statusCode, detail, id, jsonrpc, resultValue, time, + version, versionNumber, signature); } return orElse(); } @@ -474,7 +505,8 @@ class _$PiSuccessResponseImpl abstract class PiSuccessResponse extends PiServerResponse { factory PiSuccessResponse( - {required final dynamic detail, + {required final int statusCode, + required final dynamic detail, required final int id, required final String jsonrpc, required final T resultValue, @@ -484,6 +516,8 @@ abstract class PiSuccessResponse required final String signature}) = _$PiSuccessResponseImpl; PiSuccessResponse._() : super._(); + @override + int get statusCode; @override dynamic get detail; @override @@ -517,7 +551,8 @@ abstract class _$$PiErrorResponseImplCopyWith @pragma('vm:prefer-inline') @override $Res call({ + Object? statusCode = null, Object? detail = freezed, Object? id = null, Object? jsonrpc = null, @@ -550,6 +586,10 @@ class __$$PiErrorResponseImplCopyWithImpl Object? signature = null, }) { return _then(_$PiErrorResponseImpl( + statusCode: null == statusCode + ? _value.statusCode + : statusCode // ignore: cast_nullable_to_non_nullable + as int, detail: freezed == detail ? _value.detail : detail // ignore: cast_nullable_to_non_nullable @@ -591,7 +631,8 @@ class __$$PiErrorResponseImplCopyWithImpl class _$PiErrorResponseImpl extends PiErrorResponse { _$PiErrorResponseImpl( - {required this.detail, + {required this.statusCode, + required this.detail, required this.id, required this.jsonrpc, required this.piServerResultError, @@ -601,6 +642,8 @@ class _$PiErrorResponseImpl required this.signature}) : super._(); + @override + final int statusCode; @override final dynamic detail; @override @@ -622,7 +665,7 @@ class _$PiErrorResponseImpl @override String toString() { - return 'PiServerResponse<$T>.error(detail: $detail, id: $id, jsonrpc: $jsonrpc, piServerResultError: $piServerResultError, time: $time, version: $version, versionNumber: $versionNumber, signature: $signature)'; + return 'PiServerResponse<$T>.error(statusCode: $statusCode, detail: $detail, id: $id, jsonrpc: $jsonrpc, piServerResultError: $piServerResultError, time: $time, version: $version, versionNumber: $versionNumber, signature: $signature)'; } @override @@ -630,6 +673,8 @@ class _$PiErrorResponseImpl return identical(this, other) || (other.runtimeType == runtimeType && other is _$PiErrorResponseImpl && + (identical(other.statusCode, statusCode) || + other.statusCode == statusCode) && const DeepCollectionEquality().equals(other.detail, detail) && (identical(other.id, id) || other.id == id) && (identical(other.jsonrpc, jsonrpc) || other.jsonrpc == jsonrpc) && @@ -646,6 +691,7 @@ class _$PiErrorResponseImpl @override int get hashCode => Object.hash( runtimeType, + statusCode, const DeepCollectionEquality().hash(detail), id, jsonrpc, @@ -668,6 +714,7 @@ class _$PiErrorResponseImpl @optionalTypeArgs TResult when({ required TResult Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -678,6 +725,7 @@ class _$PiErrorResponseImpl String signature) success, required TResult Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -688,14 +736,15 @@ class _$PiErrorResponseImpl String signature) error, }) { - return error(detail, id, jsonrpc, piServerResultError, time, version, - versionNumber, signature); + return error(statusCode, detail, id, jsonrpc, piServerResultError, time, + version, versionNumber, signature); } @override @optionalTypeArgs TResult? whenOrNull({ TResult? Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -706,6 +755,7 @@ class _$PiErrorResponseImpl String signature)? success, TResult? Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -716,14 +766,15 @@ class _$PiErrorResponseImpl String signature)? error, }) { - return error?.call(detail, id, jsonrpc, piServerResultError, time, version, - versionNumber, signature); + return error?.call(statusCode, detail, id, jsonrpc, piServerResultError, + time, version, versionNumber, signature); } @override @optionalTypeArgs TResult maybeWhen({ TResult Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -734,6 +785,7 @@ class _$PiErrorResponseImpl String signature)? success, TResult Function( + int statusCode, dynamic detail, int id, String jsonrpc, @@ -746,8 +798,8 @@ class _$PiErrorResponseImpl required TResult orElse(), }) { if (error != null) { - return error(detail, id, jsonrpc, piServerResultError, time, version, - versionNumber, signature); + return error(statusCode, detail, id, jsonrpc, piServerResultError, time, + version, versionNumber, signature); } return orElse(); } @@ -787,7 +839,8 @@ class _$PiErrorResponseImpl abstract class PiErrorResponse extends PiServerResponse { factory PiErrorResponse( - {required final dynamic detail, + {required final int statusCode, + required final dynamic detail, required final int id, required final String jsonrpc, required final PiServerResultError piServerResultError, @@ -797,6 +850,8 @@ abstract class PiErrorResponse required final String signature}) = _$PiErrorResponseImpl; PiErrorResponse._() : super._(); + @override + int get statusCode; @override dynamic get detail; @override diff --git a/lib/model/riverpod_states/token_container_state.dart b/lib/model/riverpod_states/token_container_state.dart index f2864245..fed69a12 100644 --- a/lib/model/riverpod_states/token_container_state.dart +++ b/lib/model/riverpod_states/token_container_state.dart @@ -39,13 +39,17 @@ class TokenContainerState with _$TokenContainerState { bool get hasFinalizedContainers => containerList.any((container) => container is TokenContainerFinalized); - TokenContainer? containerOf(String containerSerial) => containerList.firstWhereOrNull((container) => container.serial == containerSerial); + TokenContainer? containerOf(String containerSerial) { + if (containerList.isEmpty) return null; + return containerList.firstWhereOrNull((container) => container.serial == containerSerial); + } + static TokenContainerState fromJsonStringList(List jsonStrings) { final containerList = jsonStrings.map((jsonString) => TokenContainer.fromJson(jsonDecode(jsonString))).toList(); return TokenContainerState(containerList: containerList); } - T? currentOf(T container) { + T? currentOf(TokenContainer container) { final current = containerOf(container.serial); if (current is T) { Logger.info('Found current container for ${container.serial}'); diff --git a/lib/model/token_container.dart b/lib/model/token_container.dart index e13acced..2605afd1 100644 --- a/lib/model/token_container.dart +++ b/lib/model/token_container.dart @@ -44,6 +44,7 @@ part 'token_container.g.dart'; class TokenContainer with _$TokenContainer { static const SERIAL = 'serial'; static const eccUtils = EccUtils(); + const TokenContainer._(); Uri get registrationUrl => serverUrl.replace(path: '/container/register/finalize'); diff --git a/lib/model/tokens/totp_token.dart b/lib/model/tokens/totp_token.dart index f73bee37..651e52bd 100644 --- a/lib/model/tokens/totp_token.dart +++ b/lib/model/tokens/totp_token.dart @@ -176,9 +176,9 @@ class TOTPToken extends OTPToken { OTP_AUTH_ISSUER: const ObjectValidator(defaultValue: ''), OTP_AUTH_SERIAL: const ObjectValidatorNullable(), OTP_AUTH_ALGORITHM: stringToAlgorithmsValidator.withDefault(Algorithms.SHA1), - OTP_AUTH_DIGITS: intValidator.withDefault(6), + OTP_AUTH_DIGITS: otpAuthDigitsValidator, OTP_AUTH_SECRET_BASE32: base32Secretvalidator, - OTP_AUTH_PERIOD_SECONDS: intValidator.withDefault(30), + OTP_AUTH_PERIOD_SECONDS: otpAuthPeriodSecondsValidator, OTP_AUTH_IMAGE: const ObjectValidatorNullable(), OTP_AUTH_PIN: boolValidatorNullable, }, diff --git a/lib/processors/scheme_processors/token_container_processor.dart b/lib/processors/scheme_processors/token_container_processor.dart index d33f467c..d4f4cb36 100644 --- a/lib/processors/scheme_processors/token_container_processor.dart +++ b/lib/processors/scheme_processors/token_container_processor.dart @@ -1,3 +1,5 @@ +// ignore_for_file: constant_identifier_names + /* * privacyIDEA Authenticator * @@ -32,6 +34,20 @@ class TokenContainerProcessor extends SchemeProcessor { static const scheme = 'pia'; static const host = 'container'; + static const String ARG_DO_REPLACE = 'doReplace'; + static const String ARG_ADD_DEVICE_INFOS = 'addDeviceInfos'; + static const String ARG_INIT_SYNC = 'initSync'; + + static Map validateArgs(Map args) => validateMap( + map: args, + validators: { + TokenContainerProcessor.ARG_DO_REPLACE: boolValidatorNullable, + TokenContainerProcessor.ARG_ADD_DEVICE_INFOS: boolValidatorNullable, + TokenContainerProcessor.ARG_INIT_SYNC: boolValidatorNullable, + }, + name: 'TokenContainerProcessor#validateArgs', + ); + @override Set get supportedSchemes => {scheme}; diff --git a/lib/repo/secure_token_container_repository.dart b/lib/repo/secure_token_container_repository.dart index 4b91c3eb..bb976b1b 100644 --- a/lib/repo/secure_token_container_repository.dart +++ b/lib/repo/secure_token_container_repository.dart @@ -33,13 +33,6 @@ class SecureTokenContainerRepository extends TokenContainerRepository { return await loadContainerState(); } - @override - Future> loadContainerList() async { - final containerJsonString = await _readAll(); - Logger.warning('Loaded container: $containerJsonString'); - return containerJsonString.values.map((jsonString) => TokenContainer.fromJson(jsonDecode(jsonString))).toList(); - } - @override Future saveContainerList(List containerList) async { Logger.warning('Saving container: $containerList'); diff --git a/lib/utils/object_validator.dart b/lib/utils/object_validator.dart index ed7536e5..356946ee 100644 --- a/lib/utils/object_validator.dart +++ b/lib/utils/object_validator.dart @@ -24,18 +24,26 @@ import '../model/enums/encodings.dart'; import '../model/exception_errors/localized_argument_error.dart'; import 'logger.dart'; -final otpAutjPeriodSecondsValidatorNullable = otpAutjPeriodSecondsValidator.nullable(); -final otpAutjPeriodSecondsValidator = ObjectValidator( - transformer: (v) => int.parse(v), +final otpAutjPeriodSecondsValidatorNullable = otpAuthPeriodSecondsValidator.nullable(); +final otpAuthPeriodSecondsValidator = ObjectValidator( + transformer: (v) { + if (v is int) return v; + return int.parse(v); + }, + defaultValue: 30, allowedValues: (v) => v > 0, ); final otpAuthDigitsValidatorNullable = otpAuthDigitsValidator.nullable(); final otpAuthDigitsValidator = ObjectValidator( - transformer: (v) => int.parse(v), + transformer: (v) { + if (v is int) return v; + return int.parse(v); + }, defaultValue: 6, allowedValues: (p0) => p0 > 0, ); + final otpAuthCounterValidator = ObjectValidator( transformer: (v) { if (v is int) return v; diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart index eb87c522..b64b49d1 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart @@ -145,19 +145,82 @@ class TokenContainerNotifier extends _$TokenContainerNotifier with ResultHandler ////////////////////////// PUBLIC METHODS ////////////////////////// ///////////////////////////////////////////////////////////////// */ - Future> syncTokens({ + Future> sync({ required TokenState tokenState, required bool isManually, - - /// If not provided, all containers will be synced List? containersToSync, - }) async => - _syncTokens( - tokenState: tokenState, - containersToSync: containersToSync ?? state.asData?.value.containerList.whereType().toList() ?? [], - apiCall: _containerApi.sync, - isManually: isManually, + }) async { + containersToSync ??= state.asData?.value.containerList.whereType().toList() ?? []; + Logger.info('Syncing ${containersToSync.length} tokens'); + if (containersToSync.isEmpty) { + final containerList = (await future).containerList; + containersToSync = containerList.whereType().where((e) => e.syncState != SyncState.syncing).toList(); + } else { + final current = []; + for (final container in containersToSync) { + current.add((await future).currentOf(container)!); + } + containersToSync = current.whereType().where((e) => e.syncState != SyncState.syncing).toList(); + } + final syncFutures = >[]; + + List syncedTokens = []; + List deletedTokens = []; + + containersToSync = await updateContainerList(containersToSync, (c) => c.copyWith(syncState: SyncState.syncing)); + + final failedContainers = {}; + + for (var finalizedContainer in containersToSync) { + syncFutures.add( + Future(() async { + final syncResult = await _containerApi.sync( + finalizedContainer, + tokenState, + ); + if (syncResult == null) { + await updateContainer(finalizedContainer, (TokenContainerFinalized c) => c.copyWith(syncState: SyncState.failed)); + return null; + } + await updateContainer(finalizedContainer, (TokenContainerFinalized c) => c.copyWith(syncState: SyncState.completed)); + return syncResult; + }).catchError((error, stackTrace) async { + await updateContainer(finalizedContainer, (TokenContainerFinalized c) => c.copyWith(syncState: SyncState.failed)); + if (error is PiServerResultError) { + failedContainers.addAll({error.code: finalizedContainer}); + } + if (!isManually) return null; + Logger.debug('Failed to sync container ${error.runtimeType}', error: error, stackTrace: stackTrace); + showStatusMessage( + message: AppLocalizations.of(await globalContext)!.failedToSyncContainer(finalizedContainer.serial), + subMessage: error is PiServerResultError ? error.message : error.toString(), + ); + return null; + }), ); + } + + Map newPoliciesMap = {}; + + await Future.wait(syncFutures).then((containerUpdates) { + for (var containerUpdate in containerUpdates) { + if (containerUpdate == null) continue; + syncedTokens.addAll(containerUpdate.updatedTokens); + deletedTokens.addAll(containerUpdate.deleteTokenSerials); + newPoliciesMap[containerUpdate.containerSerial] = containerUpdate.newPolicies; + } + }).onError((error, stackTrace) { + Logger.error('Failed to sync container', error: error, stackTrace: stackTrace); + }); + + // Do not remove tokens that are synced in any other container + deletedTokens.removeWhere((serial) => syncedTokens.any((token) => token.serial == serial)); + + await ref.read(tokenProvider.notifier).addOrReplaceTokens(syncedTokens); + await ref.read(tokenProvider.notifier).removeTokensBySerials(deletedTokens); + await updateContainerList((await future).containerList, (c) => newPoliciesMap[c.serial] == null ? c : c.copyWith(policies: newPoliciesMap[c.serial]!)); + return failedContainers; + } Future rolloverTokens({ required TokenState tokenState, @@ -168,12 +231,12 @@ class TokenContainerNotifier extends _$TokenContainerNotifier with ResultHandler if (uri == null) throw ArgumentError('Invalid rollover uri'); final result = (await TokenContainerProcessor().processUri(uri, fromInit: false))?.firstOrNull; if (result == null) throw StateError('Failed to process rollover uri'); - final success = await handleProcessorResult(result, {}, doReplace: true); + final success = await handleProcessorResult(result, {TokenContainerProcessor.ARG_DO_REPLACE: true}); return success; } Future getTransferQrData(TokenContainerFinalized container) async { - final currentContainer = (await future).currentOf(container); + final currentContainer = (await future).currentOf(container); if (currentContainer == null) throw StateError('Container was removed'); final qrCode = await _containerApi.getTransferQrData(currentContainer); return qrCode; @@ -226,7 +289,7 @@ class TokenContainerNotifier extends _$TokenContainerNotifier with ResultHandler return super.update(cb, onError: onError); } - Future updateContainer(T container, T Function(T) updater) async { + Future updateContainer(TokenContainer container, R Function(T) updater) async { await _stateMutex.acquire(); final oldState = await future; final currentContainer = oldState.currentOf(container); @@ -235,6 +298,7 @@ class TokenContainerNotifier extends _$TokenContainerNotifier with ResultHandler _stateMutex.release(); return null; } + Logger.info('Updating container ${currentContainer.serial}'); final updated = updater(currentContainer); final newState = await _saveContainerToRepo(updated); await update((_) => newState); @@ -316,16 +380,21 @@ class TokenContainerNotifier extends _$TokenContainerNotifier with ResultHandler /// Returns true if the processor result was handled successfully @override - Future handleProcessorResult(ProcessorResult result, Map args, {bool? doReplace}) async { - final failedContainer = await handleProcessorResults([result], args, doReplace: doReplace); + Future handleProcessorResult(ProcessorResult result, Map args) async { + final failedContainer = await handleProcessorResults([result], args); return failedContainer?.isEmpty ?? false; } /// Returns a list of containers that failed to add @override - Future?> handleProcessorResults(List results, Map args, {bool? doReplace}) async { + Future?> handleProcessorResults(List results, Map args) async { Logger.info('Handling processor results'); final newContainers = results.getData().whereType().toList(); + final validatedArgs = TokenContainerProcessor.validateArgs(args); + final doReplace = validatedArgs[TokenContainerProcessor.ARG_DO_REPLACE]; + bool? addDeviceInfos = validatedArgs[TokenContainerProcessor.ARG_ADD_DEVICE_INFOS]; + final initSync = validatedArgs[TokenContainerProcessor.ARG_INIT_SYNC] ?? true; + if (newContainers.isEmpty) return null; final currentState = await future; final stateContainers = currentState.containerList; @@ -346,7 +415,7 @@ class TokenContainerNotifier extends _$TokenContainerNotifier with ResultHandler await deleteContainerList(replaceContainers); newContainerList.addAll(replaceContainers); } - final addDeviceInfos = (await SendDeviceInfosDialog.showDialog()) == true; + addDeviceInfos ??= (await SendDeviceInfosDialog.showDialog()) == true; newContainerList = newContainerList.map((e) => e.copyWith(addDeviceInfos: addDeviceInfos)).toList(); if (newContainerList.isNotEmpty) _showAddContainerProgressDialog(newContainerList); @@ -365,7 +434,7 @@ class TokenContainerNotifier extends _$TokenContainerNotifier with ResultHandler finalizeFutures.add(finalize(container, isManually: true)); } await Future.wait(finalizeFutures); - await syncTokens(tokenState: ref.read(tokenProvider), containersToSync: [], isManually: true); + if (initSync) await sync(tokenState: ref.read(tokenProvider), containersToSync: [], isManually: true); return failedToAdd; } @@ -393,26 +462,26 @@ class TokenContainerNotifier extends _$TokenContainerNotifier with ResultHandler ); } } on LocalizedArgumentError catch (e) { - final applocalizations = AppLocalizations.of(await globalContext)!; if (isManually) { + final applocalizations = AppLocalizations.of(await globalContext)!; ref.read(statusMessageProvider.notifier).state = ( container.finalizationState.asFailed.rolloutMsgLocalized(applocalizations), e.localizedMessage(applocalizations), ); } - await updateContainer(container, (c) => c.copyWith(finalizationState: c.finalizationState.asFailed)); + await updateContainer(container, (TokenContainerFinalized c) => c.copyWith(finalizationState: c.finalizationState.asFailed)); } on PiErrorResponse catch (e) { - final applocalizations = AppLocalizations.of(await globalContext)!; if (isManually) { + final applocalizations = AppLocalizations.of(await globalContext)!; ref.read(statusMessageProvider.notifier).state = ( container.finalizationState.asFailed.rolloutMsgLocalized(applocalizations), e.piServerResultError.message, ); } - await updateContainer(container, (c) => c.copyWith(finalizationState: c.finalizationState.asFailed)); + await updateContainer(container, (TokenContainerFinalized c) => c.copyWith(finalizationState: c.finalizationState.asFailed)); } on ResponseError catch (e) { - final applocalizations = AppLocalizations.of(await globalContext)!; if (isManually) { + final applocalizations = AppLocalizations.of(await globalContext)!; ref.read(statusMessageProvider.notifier).state = ( container.finalizationState.asFailed.rolloutMsgLocalized(applocalizations), e.toString(), @@ -430,83 +499,6 @@ class TokenContainerNotifier extends _$TokenContainerNotifier with ResultHandler ////////////////// PRIVATE HELPER METHODS FINALIZATION ///////////////////// ///////////////////////////////////////////////////////////////////////// */ - Future> _syncTokens({ - required TokenState tokenState, - required List containersToSync, - required Future Function(TokenContainerFinalized container, TokenState tokenState) apiCall, - required bool isManually, - }) async { - Logger.info('Syncing ${containersToSync.length} tokens'); - if (containersToSync.isEmpty) { - final containerList = (await future).containerList; - containersToSync = containerList.whereType().where((e) => e.syncState != SyncState.syncing).toList(); - } else { - final current = []; - for (final container in containersToSync) { - current.add((await future).currentOf(container)!); - } - containersToSync = current.whereType().where((e) => e.syncState != SyncState.syncing).toList(); - } - final syncFutures = >[]; - - List syncedTokens = []; - List deletedTokens = []; - - containersToSync = await updateContainerList(containersToSync, (c) => c.copyWith(syncState: SyncState.syncing)); - - final failedContainers = {}; - - for (var finalizedContainer in containersToSync) { - syncFutures.add( - Future(() async { - final syncResult = await apiCall( - finalizedContainer, - tokenState, - ); - if (syncResult == null) { - await updateContainer(finalizedContainer, (c) => c.copyWith(syncState: SyncState.failed)); - return null; - } - await updateContainer(finalizedContainer, (c) => c.copyWith(syncState: SyncState.completed)); - return syncResult; - }).catchError((error, stackTrace) async { - await updateContainer(finalizedContainer, (c) => c.copyWith(syncState: SyncState.failed)); - if (error is PiServerResultError) { - failedContainers.addAll({error.code: finalizedContainer}); - } - if (!isManually) return null; - Logger.debug('Failed to sync container ${error.runtimeType}', error: error, stackTrace: stackTrace); - showStatusMessage( - message: AppLocalizations.of(await globalContext)!.failedToSyncContainer(finalizedContainer.serial), - subMessage: error is PiServerResultError ? error.message : error.toString(), - ); - return null; - }), - ); - } - - Map newPoliciesMap = {}; - - await Future.wait(syncFutures).then((containerUpdates) { - for (var containerUpdate in containerUpdates) { - if (containerUpdate == null) continue; - syncedTokens.addAll(containerUpdate.updatedTokens); - deletedTokens.addAll(containerUpdate.deleteTokenSerials); - newPoliciesMap[containerUpdate.containerSerial] = containerUpdate.newPolicies; - } - }).onError((error, stackTrace) { - Logger.error('Failed to sync container', error: error, stackTrace: stackTrace); - }); - - // Do not remove tokens that are synced in any other container - deletedTokens.removeWhere((serial) => syncedTokens.any((token) => token.serial == serial)); - - await ref.read(tokenProvider.notifier).addOrReplaceTokens(syncedTokens); - await ref.read(tokenProvider.notifier).removeTokensBySerials(deletedTokens); - await updateContainerList((await future).containerList, (c) => newPoliciesMap[c.serial] == null ? c : c.copyWith(policies: newPoliciesMap[c.serial]!)); - return failedContainers; - } - Future _curentOf(TokenContainer container) async { final current = (await future).currentOf(container); if (current == null) throw StateError('Container was removed'); @@ -525,10 +517,12 @@ class TokenContainerNotifier extends _$TokenContainerNotifier with ResultHandler // generatingKeyPairFailed, // generatingKeyPairCompleted, TokenContainerUnfinalized? container = tokenContainer; - container = await updateContainer(container, (c) => c.copyWith(finalizationState: RolloutState.generatingKeyPair)); + container = await updateContainer( + container, (c) => c.copyWith(finalizationState: RolloutState.generatingKeyPair)); if (container == null) throw StateError('Container was removed'); final keyPair = eccUtils.generateKeyPair(container.ecKeyAlgorithm); - container = await updateContainer(container, (c) => c.withClientKeyPair(keyPair) as TokenContainerUnfinalized); + container = await updateContainer( + container, (c) => c.withClientKeyPair(keyPair) as TokenContainerUnfinalized); if (container == null) throw StateError('Container was removed'); return container; } @@ -548,17 +542,17 @@ class TokenContainerNotifier extends _$TokenContainerNotifier with ResultHandler TokenContainerUnfinalized? container = tokenContainer; - final Response? response; - container = await updateContainer(container, (c) => c.copyWith(finalizationState: RolloutState.sendingPublicKey)); + final Response response; + container = await updateContainer(container, (TokenContainerUnfinalized c) => c.copyWith(finalizationState: RolloutState.sendingPublicKey)); if (container == null) throw StateError('Container was removed'); response = (await _containerApi.finalizeContainer(container, eccUtils)); if (response.statusCode != 200) { - container = await updateContainer(container, (c) => c.copyWith(finalizationState: RolloutState.sendingPublicKeyFailed)); - if (container == null) throw StateError('Container was removed'); - return response; + container = await updateContainer(container, (TokenContainerUnfinalized c) => c.copyWith(finalizationState: RolloutState.sendingPublicKeyFailed)); + throw ResponseError(response); } + container = await updateContainer(container, (TokenContainerUnfinalized c) => c.copyWith(finalizationState: RolloutState.sendingPublicKeyCompleted)); return response; } @@ -573,38 +567,39 @@ class TokenContainerNotifier extends _$TokenContainerNotifier with ResultHandler piResponse = response.asPiServerResponse(); } catch (e) { Logger.error('Failed to parse response', error: e); - container = await updateContainer(container, (c) => c.copyWith(finalizationState: RolloutState.parsingResponseFailed)); - if (container == null) throw StateError('Container was removed'); + container = await updateContainer(container, (TokenContainerUnfinalized c) => c.copyWith(finalizationState: RolloutState.parsingResponseFailed)); rethrow; } if (piResponse == null || piResponse.isError) { Logger.debug('Status code: ${response.statusCode}'); Logger.debug('Response body: ${response.body}'); - container = await updateContainer(container, (c) => c.copyWith(finalizationState: RolloutState.sendingPublicKeyFailed)); - if (container == null) throw StateError('Container was removed'); + container = await updateContainer(container, (TokenContainerUnfinalized c) => c.copyWith(finalizationState: RolloutState.sendingPublicKeyFailed)); final error = piResponse?.asError; if (error != null) throw error; throw ResponseError(response); } - container = await updateContainer(container, (c) => c.copyWith(finalizationState: RolloutState.sendingPublicKeyCompleted)); + container = await updateContainer(container, (TokenContainerUnfinalized c) => c.copyWith(finalizationState: RolloutState.sendingPublicKeyCompleted)); if (container == null) throw StateError('Container was removed'); - container = await updateContainer(container, (c) => c.copyWith(finalizationState: RolloutState.parsingResponse)); + container = await updateContainer(container, (TokenContainerUnfinalized c) => c.copyWith(finalizationState: RolloutState.parsingResponse)); if (container == null) throw StateError('Container was removed'); - ContainerFinalizationResponse resultValue = piResponse.asSuccess!.resultValue; + ContainerFinalizationResponse finalizationResponse = piResponse.asSuccess!.resultValue; try { - resultValue = piResponse.asSuccess!.resultValue; + finalizationResponse = piResponse.asSuccess!.resultValue; } catch (e) { Logger.error('Failed to parse response', error: e); - container = await updateContainer(container, (c) => c.copyWith(finalizationState: RolloutState.parsingResponseFailed)); - if (container == null) throw StateError('Container was removed'); + container = await updateContainer(container, (TokenContainerUnfinalized c) => c.copyWith(finalizationState: RolloutState.parsingResponseFailed)); rethrow; } - container = await updateContainer(container, (c) => c.copyWith(policies: resultValue.policies).finalize(publicServerKey: resultValue.publicServerKey)!); - if (container == null) throw StateError('Container was removed or finalization failed'); - return container as TokenContainerFinalized; + // final signature = finalizationResponse.signature; + final finalizedContainer = await updateContainer( + container, + (TokenContainerUnfinalized c) => c.copyWith(policies: finalizationResponse.policies).finalize(publicServerKey: finalizationResponse.publicServerKey)!, + ); + if (finalizedContainer == null) throw StateError('Container was removed'); + return finalizedContainer; } } diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.g.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.g.dart index f4788877..28325a40 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.g.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.g.dart @@ -7,7 +7,7 @@ part of 'token_container_notifier.dart'; // ************************************************************************** String _$tokenContainerNotifierHash() => - r'0a2058e6972b4a790dada0d9cacaebfeace4e246'; + r'a54590476e980e31528f4b833b50bb5786168964'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart index 3c0e3758..b1d27453 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart @@ -159,7 +159,7 @@ class TokenNotifier extends _$TokenNotifier with ResultHandler { /// Adds a list of tokens and returns the tokens that could not be added or replaced. Future> _addOrReplaceTokens(List tokens) async { - tokens = _removeDuplicates(tokens); + tokens = _filterDuplicates([...tokens, ...state.tokens]); if (tokens.isEmpty) return []; Logger.debug('Adding ${tokens.length} tokens.', verbose: true); await _repoMutex.acquire(); @@ -790,7 +790,7 @@ class TokenNotifier extends _$TokenNotifier with ResultHandler { ///////////////////////////// Helper Methods /////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// */ - List _removeDuplicates(List tokens) { + List _filterDuplicates(List tokens) { final uniqueTokens = []; for (var token in tokens) { if (!uniqueTokens.any((uniqureToken) => uniqureToken.isSameTokenAs(token))) { diff --git a/lib/views/container_view/container_widgets/container_actions/details_container_action_dialog.dart b/lib/views/container_view/container_widgets/container_actions/details_container_action_dialog.dart index d404a35f..785d2142 100644 --- a/lib/views/container_view/container_widgets/container_actions/details_container_action_dialog.dart +++ b/lib/views/container_view/container_widgets/container_actions/details_container_action_dialog.dart @@ -93,7 +93,9 @@ class _DetailsContainerDialogState extends ConsumerState TextButton( onPressed: Uri.tryParse(controller.text) != null ? () { - ref.read(tokenContainerProvider.notifier).updateContainer(widget.container, (c) => c.copyWith(serverUrl: Uri.parse(controller.text))); + ref + .read(tokenContainerProvider.notifier) + .updateContainer(widget.container, (TokenContainer c) => c.copyWith(serverUrl: Uri.parse(controller.text))); Navigator.of(context).pop(); } : null, diff --git a/lib/views/container_view/container_widgets/container_actions/transfer_dialogs/transfer_delete_dontainer_dialog.dart b/lib/views/container_view/container_widgets/container_actions/transfer_dialogs/transfer_delete_dontainer_dialog.dart index beaa0c82..d89b1d7f 100644 --- a/lib/views/container_view/container_widgets/container_actions/transfer_dialogs/transfer_delete_dontainer_dialog.dart +++ b/lib/views/container_view/container_widgets/container_actions/transfer_dialogs/transfer_delete_dontainer_dialog.dart @@ -49,7 +49,7 @@ class _TransferDeleteContainerDialogState extends ConsumerState(this.container); return CooldownButton( styleType: CooldownButtonStyleType.iconButton, childWhenCooldown: CircularProgressIndicator.adaptive(), @@ -163,7 +163,7 @@ class SyncContainerButton extends ConsumerWidget { onPressed: container != null ? () async { final tokenState = ref.read(tokenProvider); - await ref.read(tokenContainerProvider.notifier).syncTokens(tokenState: tokenState, containersToSync: [container], isManually: true); + await ref.read(tokenContainerProvider.notifier).sync(tokenState: tokenState, containersToSync: [container], isManually: true); } : null, child: const Icon(Icons.sync, size: 40), @@ -221,7 +221,7 @@ class RolloverContainerTokensButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final container = ref.watch(tokenContainerProvider).asData?.value.currentOf(this.container); + final container = ref.watch(tokenContainerProvider).asData?.value.currentOf(this.container); return CooldownButton( styleType: CooldownButtonStyleType.iconButton, childWhenCooldown: CircularProgressIndicator.adaptive(), diff --git a/lib/views/splash_screen/splash_screen.dart b/lib/views/splash_screen/splash_screen.dart index e3b3679e..7237f383 100644 --- a/lib/views/splash_screen/splash_screen.dart +++ b/lib/views/splash_screen/splash_screen.dart @@ -74,13 +74,13 @@ class _SplashScreenState extends ConsumerState { if (!mounted) return []; final tokenState = ref.read(tokenProvider); - ref.read(tokenContainerProvider.notifier).syncTokens(tokenState: tokenState, isManually: false); + ref.read(tokenContainerProvider.notifier).sync(tokenState: tokenState, isManually: false); _navigate(); return []; }).then((values) async { if (!mounted) return; final tokenState = ref.read(tokenProvider); - ref.read(tokenContainerProvider.notifier).syncTokens(tokenState: tokenState, isManually: false); + ref.read(tokenContainerProvider.notifier).sync(tokenState: tokenState, isManually: false); return _navigate(); }); }); diff --git a/lib/widgets/default_refresh_indicator.dart b/lib/widgets/default_refresh_indicator.dart index 0c25ae89..8cfd7c3f 100644 --- a/lib/widgets/default_refresh_indicator.dart +++ b/lib/widgets/default_refresh_indicator.dart @@ -39,7 +39,7 @@ class _DefaultRefreshIndicatorState extends ConsumerState isRefreshing = false); }, diff --git a/test/tests_app_wrapper.dart b/test/tests_app_wrapper.dart index df3508af..ea1d4fa9 100644 --- a/test/tests_app_wrapper.dart +++ b/test/tests_app_wrapper.dart @@ -30,7 +30,6 @@ import 'package:privacyidea_authenticator/utils/rsa_utils.dart'; MockSpec(), MockSpec(), ]) -@GenerateMocks([]) class TestsAppWrapper extends StatelessWidget { final Widget child; final List overrides; diff --git a/test/tests_app_wrapper.mocks.dart b/test/tests_app_wrapper.mocks.dart index dd0ddef3..90fb6250 100644 --- a/test/tests_app_wrapper.mocks.dart +++ b/test/tests_app_wrapper.mocks.dart @@ -574,19 +574,6 @@ class MockTokenContainerRepository extends _i1.Mock )), ) as _i13.Future<_i6.TokenContainerState>); - @override - _i13.Future> loadContainerList() => - (super.noSuchMethod( - Invocation.method( - #loadContainerList, - [], - ), - returnValue: _i13.Future>.value( - <_i21.TokenContainer>[]), - returnValueForMissingStub: _i13.Future>.value( - <_i21.TokenContainer>[]), - ) as _i13.Future>); - @override _i13.Future<_i6.TokenContainerState> saveContainerList( List<_i21.TokenContainer>? containerList) => diff --git a/test/unit_test/state_notifiers/token_container_notifier_test.dart b/test/unit_test/state_notifiers/token_container_notifier_test.dart index a1b79067..e17add85 100644 --- a/test/unit_test/state_notifiers/token_container_notifier_test.dart +++ b/test/unit_test/state_notifiers/token_container_notifier_test.dart @@ -1,14 +1,28 @@ +import 'dart:convert'; + +import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart'; import 'package:mockito/mockito.dart'; +import 'package:privacyidea_authenticator/api/interfaces/container_api.dart'; +import 'package:privacyidea_authenticator/model/container_policies.dart'; import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; import 'package:privacyidea_authenticator/model/enums/ec_key_algorithm.dart'; +import 'package:privacyidea_authenticator/model/enums/rollout_state.dart'; +import 'package:privacyidea_authenticator/model/enums/sync_state.dart'; +import 'package:privacyidea_authenticator/model/riverpod_states/settings_state.dart'; import 'package:privacyidea_authenticator/model/riverpod_states/token_container_state.dart'; +import 'package:privacyidea_authenticator/model/riverpod_states/token_state.dart'; import 'package:privacyidea_authenticator/model/token_container.dart'; +import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; import 'package:privacyidea_authenticator/processors/scheme_processors/token_container_processor.dart'; import 'package:privacyidea_authenticator/utils/ecc_utils.dart'; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/settings_notifier.dart'; import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import '../../tests_app_wrapper.mocks.dart'; @@ -17,7 +31,7 @@ void main() { _testTokenContainerNotifier(); } -TokenContainerState _getBaseState() => TokenContainerState( +TokenContainerState _buildUnfinalizedContainerState() => TokenContainerState( containerList: [ TokenContainerUnfinalized( issuer: 'issuer', @@ -32,70 +46,87 @@ TokenContainerState _getBaseState() => TokenContainerState( ], ); +TokenContainerState _buildFinalizedContainerState() => TokenContainerState( + containerList: [ + TokenContainerFinalized( + serverName: 'privacyIDEA', + issuer: 'privacyIDEA', + nonce: 'dbd2ab5aa9b539484fc3b78cd4bb08375d3eb30e', + timestamp: DateTime.parse("2024-11-14 09:30:18.288530Z"), + serverUrl: Uri.parse("http://example.com"), + serial: "CONTAINER01", + ecKeyAlgorithm: EcKeyAlgorithm.secp384r1, + hashAlgorithm: Algorithms.SHA256, + sslVerify: false, + publicServerKey: 'publicServerKey', + publicClientKey: 'publicClientKey', + privateClientKey: 'privateClientKey', + finalizationState: RolloutState.completed, + syncState: SyncState.notStarted, + passphraseQuestion: null, + policies: ContainerPolicies( + rolloverAllowed: false, + initialTokenTransfer: false, + tokensDeletable: false, + unregisterAllowed: false, + ), + ), + ], + ); + void _testTokenContainerNotifier() { group('Token Container Notifier Test', () { test('load state from repo on creation', () async { final container = ProviderContainer(); - var repoState = _getBaseState(); - final mockRepo = MockTokenContainerRepository(); + var containerRepoState = _buildUnfinalizedContainerState(); + final mockContainerRepo = _setupMockContainerRepo(() => containerRepoState, (state) => containerRepoState = state); final mockContainerApi = MockTokenContainerApi(); when(mockContainerApi.finalizeContainer(any, any)).thenAnswer((_) async => Response('{}', 404)); - when(mockRepo.loadContainerState()).thenAnswer((_) => Future.value(repoState)); - when(mockRepo.saveContainer(any)).thenAnswer((invocation) { + when(mockContainerRepo.loadContainerState()).thenAnswer((_) => Future.value(containerRepoState)); + when(mockContainerRepo.saveContainer(any)).thenAnswer((invocation) { final container = invocation.positionalArguments[0] as TokenContainer; - final i = repoState.containerList.indexWhere((element) => element.serial == container.serial); + final i = containerRepoState.containerList.indexWhere((element) => element.serial == container.serial); final List newList; if (i == -1) { - newList = List.from(repoState.containerList)..add(container); + newList = List.from(containerRepoState.containerList)..add(container); } else { - newList = List.from(repoState.containerList)..[i] = container; + newList = List.from(containerRepoState.containerList)..[i] = container; } - repoState = TokenContainerState(containerList: newList); - return Future.value(repoState); + containerRepoState = TokenContainerState(containerList: newList); + return Future.value(containerRepoState); }); final tokenContainerProvider = tokenContainerNotifierProviderOf( - repo: mockRepo, + repo: mockContainerRepo, containerApi: mockContainerApi, eccUtils: EccUtils(), ); final state = await container.read(tokenContainerProvider.future); - verify(mockRepo.loadContainerState()).called(1); - expect(state, repoState); + verify(mockContainerRepo.loadContainerState()).called(1); + expect(state, containerRepoState); }); - // container.read(tokenContainerProvider.notifier).addContainer; - // container.read(tokenContainerProvider.notifier).addContainerList; - // container.read(tokenContainerProvider.notifier).updateContainer; - // container.read(tokenContainerProvider.notifier).updateContainerList; - // container.read(tokenContainerProvider.notifier).deleteContainer; - // container.read(tokenContainerProvider.notifier).deleteContainerList; - // container.read(tokenContainerProvider.notifier).handleProcessorResult; - // container.read(tokenContainerProvider.notifier).handleProcessorResults; - // container.read(tokenContainerProvider.notifier).finalizeContainer; - // container.read(tokenContainerProvider.notifier).syncTokens; - // container.read(tokenContainerProvider.notifier).getTransferQrData; test('addContainer', () async { // prepare final container = ProviderContainer(); - var repoState = _getBaseState(); - final mockRepo = MockTokenContainerRepository(); + var containerRepoState = _buildUnfinalizedContainerState(); + final mockContainerRepo = _setupMockContainerRepo(() => containerRepoState, (state) => containerRepoState = state); final mockContainerApi = MockTokenContainerApi(); when(mockContainerApi.finalizeContainer(any, any)).thenAnswer((_) async => Response('{}', 404)); - when(mockRepo.loadContainerState()).thenAnswer((_) => Future.value(repoState)); - when(mockRepo.saveContainer(any)).thenAnswer((invocation) { + when(mockContainerRepo.loadContainerState()).thenAnswer((_) => Future.value(containerRepoState)); + when(mockContainerRepo.saveContainer(any)).thenAnswer((invocation) { final container = invocation.positionalArguments[0] as TokenContainer; - final i = repoState.containerList.indexWhere((element) => element.serial == container.serial); + final i = containerRepoState.containerList.indexWhere((element) => element.serial == container.serial); final List newList; if (i == -1) { - newList = List.from(repoState.containerList)..add(container); + newList = List.from(containerRepoState.containerList)..add(container); } else { - newList = List.from(repoState.containerList)..[i] = container; + newList = List.from(containerRepoState.containerList)..[i] = container; } - repoState = TokenContainerState(containerList: newList); - return Future.value(repoState); + containerRepoState = TokenContainerState(containerList: newList); + return Future.value(containerRepoState); }); final tokenContainerProvider = tokenContainerNotifierProviderOf( - repo: mockRepo, + repo: mockContainerRepo, containerApi: mockContainerApi, eccUtils: EccUtils(), ); @@ -116,39 +147,39 @@ void _testTokenContainerNotifier() { // assert final state = await container.read(tokenContainerProvider.future); - verify(mockRepo.loadContainerState()).called(1); - verify(mockRepo.saveContainer(any)).called(greaterThanOrEqualTo(2)); + verify(mockContainerRepo.loadContainerState()).called(1); + verify(mockContainerRepo.saveContainer(any)).called(greaterThanOrEqualTo(2)); expect(state.containerList.length, equals(2)); expect(state.containerList.where((e) => e.nonce == 'nonce').length, equals(1)); expect(state.containerList.where((e) => e.nonce == 'nonce2').length, equals(1)); - expect(state, repoState); + expect(state, containerRepoState); }); test('addContainerList', () async { // prepare final container = ProviderContainer(); - var repoState = _getBaseState(); - final mockRepo = MockTokenContainerRepository(); + var containerRepoState = _buildUnfinalizedContainerState(); + final mockContainerRepo = _setupMockContainerRepo(() => containerRepoState, (state) => containerRepoState = state); final mockContainerApi = MockTokenContainerApi(); when(mockContainerApi.finalizeContainer(any, any)).thenAnswer((_) async => Response('{}', 404)); - when(mockRepo.loadContainerState()).thenAnswer((_) => Future.value(repoState)); - when(mockRepo.saveContainer(any)).thenAnswer((invocation) { + when(mockContainerRepo.loadContainerState()).thenAnswer((_) => Future.value(containerRepoState)); + when(mockContainerRepo.saveContainer(any)).thenAnswer((invocation) { final container = invocation.positionalArguments[0] as TokenContainer; - final i = repoState.containerList.indexWhere((element) => element.serial == container.serial); + final i = containerRepoState.containerList.indexWhere((element) => element.serial == container.serial); final List newList; if (i == -1) { - newList = List.from(repoState.containerList)..add(container); + newList = List.from(containerRepoState.containerList)..add(container); } else { - newList = List.from(repoState.containerList)..[i] = container; + newList = List.from(containerRepoState.containerList)..[i] = container; } - repoState = TokenContainerState(containerList: newList); - return Future.value(repoState); + containerRepoState = TokenContainerState(containerList: newList); + return Future.value(containerRepoState); }); - when(mockRepo.saveContainerState(any)).thenAnswer((invocation) { - repoState = invocation.positionalArguments[0] as TokenContainerState; - return Future.value(repoState); + when(mockContainerRepo.saveContainerState(any)).thenAnswer((invocation) { + containerRepoState = invocation.positionalArguments[0] as TokenContainerState; + return Future.value(containerRepoState); }); final tokenContainerProvider = tokenContainerNotifierProviderOf( - repo: mockRepo, + repo: mockContainerRepo, containerApi: mockContainerApi, eccUtils: EccUtils(), ); @@ -179,36 +210,36 @@ void _testTokenContainerNotifier() { final state = await container.read(tokenContainerProvider.future); // assert - verify(mockRepo.loadContainerState()).called(1); - verify(mockRepo.saveContainerState(any)).called(1); + verify(mockContainerRepo.loadContainerState()).called(1); + verify(mockContainerRepo.saveContainerState(any)).called(1); expect(state.containerList.length, equals(3)); expect(state.containerList.where((e) => e.nonce == 'nonce').length, equals(1)); expect(state.containerList.where((e) => e.nonce == 'nonce2').length, equals(1)); expect(state.containerList.where((e) => e.nonce == 'nonce3').length, equals(1)); - expect(state, repoState); + expect(state, containerRepoState); }); test('updateContainer', () async { // prepare final container = ProviderContainer(); - var repoState = _getBaseState(); - final mockRepo = MockTokenContainerRepository(); + var containerRepoState = _buildUnfinalizedContainerState(); + final mockContainerRepo = _setupMockContainerRepo(() => containerRepoState, (state) => containerRepoState = state); final mockContainerApi = MockTokenContainerApi(); when(mockContainerApi.finalizeContainer(any, any)).thenAnswer((_) async => Response('{}', 404)); - when(mockRepo.loadContainerState()).thenAnswer((_) => Future.value(repoState)); - when(mockRepo.saveContainer(any)).thenAnswer((invocation) { + when(mockContainerRepo.loadContainerState()).thenAnswer((_) => Future.value(containerRepoState)); + when(mockContainerRepo.saveContainer(any)).thenAnswer((invocation) { final container = invocation.positionalArguments[0] as TokenContainer; - final i = repoState.containerList.indexWhere((element) => element.serial == container.serial); + final i = containerRepoState.containerList.indexWhere((element) => element.serial == container.serial); final List newList; if (i == -1) { - newList = List.from(repoState.containerList)..add(container); + newList = List.from(containerRepoState.containerList)..add(container); } else { - newList = List.from(repoState.containerList)..[i] = container; + newList = List.from(containerRepoState.containerList)..[i] = container; } - repoState = TokenContainerState(containerList: newList); - return Future.value(repoState); + containerRepoState = TokenContainerState(containerList: newList); + return Future.value(containerRepoState); }); final tokenContainerProvider = tokenContainerNotifierProviderOf( - repo: mockRepo, + repo: mockContainerRepo, containerApi: mockContainerApi, eccUtils: EccUtils(), ); @@ -216,25 +247,25 @@ void _testTokenContainerNotifier() { // act await container.read(tokenContainerProvider.notifier).updateContainer( - repoState.containerList.first, - (c) => c.copyWith(issuer: 'issuer2'), + containerRepoState.containerList.first, + (TokenContainer c) => c.copyWith(issuer: 'issuer2'), ); // assert final state = await container.read(tokenContainerProvider.future); - verify(mockRepo.loadContainerState()).called(1); - verify(mockRepo.saveContainer(any)).called(2); + verify(mockContainerRepo.loadContainerState()).called(1); + verify(mockContainerRepo.saveContainer(any)).called(2); expect(state.containerList.length, equals(1)); expect(state.containerList.first.issuer, equals('issuer2')); - expect(state, repoState); + expect(state, containerRepoState); }); test('updateContainerList', () async { // prepare final container = ProviderContainer(); - var repoState = _getBaseState(); - repoState = repoState.copyWith( + var containerRepoState = _buildUnfinalizedContainerState(); + containerRepoState = containerRepoState.copyWith( containerList: [ - repoState.containerList.first, + containerRepoState.containerList.first, TokenContainerUnfinalized( issuer: 'issuer2', nonce: 'nonce2', @@ -247,107 +278,107 @@ void _testTokenContainerNotifier() { ), ], ); - final mockRepo = MockTokenContainerRepository(); + final mockContainerRepo = _setupMockContainerRepo(() => containerRepoState, (state) => containerRepoState = state); final mockContainerApi = MockTokenContainerApi(); when(mockContainerApi.finalizeContainer(any, any)).thenAnswer((_) async => Response('{}', 404)); - when(mockRepo.loadContainerState()).thenAnswer((_) => Future.value(repoState)); - when(mockRepo.saveContainer(any)).thenAnswer((invocation) { + when(mockContainerRepo.loadContainerState()).thenAnswer((_) => Future.value(containerRepoState)); + when(mockContainerRepo.saveContainer(any)).thenAnswer((invocation) { final container = invocation.positionalArguments[0] as TokenContainer; - final i = repoState.containerList.indexWhere((element) => element.serial == container.serial); + final i = containerRepoState.containerList.indexWhere((element) => element.serial == container.serial); final List newList; if (i == -1) { - newList = List.from(repoState.containerList)..add(container); + newList = List.from(containerRepoState.containerList)..add(container); } else { - newList = List.from(repoState.containerList)..[i] = container; + newList = List.from(containerRepoState.containerList)..[i] = container; } - repoState = TokenContainerState(containerList: newList); - return Future.value(repoState); + containerRepoState = TokenContainerState(containerList: newList); + return Future.value(containerRepoState); }); - when(mockRepo.saveContainerList(any)).thenAnswer((invocation) { + when(mockContainerRepo.saveContainerList(any)).thenAnswer((invocation) { final containers = invocation.positionalArguments[0] as List; - final newList = List.from(repoState.containerList); + final newList = List.from(containerRepoState.containerList); for (final container in containers) { final i = newList.indexWhere((element) => element.serial == container.serial); newList[i] = container; } - repoState = TokenContainerState(containerList: newList); - return Future.value(repoState); + containerRepoState = TokenContainerState(containerList: newList); + return Future.value(containerRepoState); }); final tokenContainerProvider = tokenContainerNotifierProviderOf( - repo: mockRepo, + repo: mockContainerRepo, containerApi: mockContainerApi, eccUtils: EccUtils(), ); await container.read(tokenContainerProvider.future); // act await container.read(tokenContainerProvider.notifier).updateContainerList( - repoState.containerList, + containerRepoState.containerList, (c) => c.copyWith(issuer: 'issuer3'), ); // assert final state = await container.read(tokenContainerProvider.future); - verify(mockRepo.loadContainerState()).called(1); + verify(mockContainerRepo.loadContainerState()).called(1); expect(state.containerList.length, equals(2)); expect(state.containerList.where((e) => e.issuer == 'issuer').length, equals(0)); expect(state.containerList.where((e) => e.issuer == 'issuer2').length, equals(0)); expect(state.containerList.where((e) => e.issuer == 'issuer3').length, equals(2)); - expect(state, repoState); + expect(state, containerRepoState); }); test('deleteContainer', () async { // prepare TestWidgetsFlutterBinding.ensureInitialized(); final container = ProviderContainer(); - var repoState = _getBaseState(); - final mockRepo = MockTokenContainerRepository(); + var containerRepoState = _buildUnfinalizedContainerState(); + final mockContainerRepo = _setupMockContainerRepo(() => containerRepoState, (state) => containerRepoState = state); final mockContainerApi = MockTokenContainerApi(); when(mockContainerApi.finalizeContainer(any, any)).thenAnswer((_) async => Response('{}', 404)); - when(mockRepo.loadContainerState()).thenAnswer((_) => Future.value(repoState)); - when(mockRepo.saveContainer(any)).thenAnswer((invocation) { + when(mockContainerRepo.loadContainerState()).thenAnswer((_) => Future.value(containerRepoState)); + when(mockContainerRepo.saveContainer(any)).thenAnswer((invocation) { final container = invocation.positionalArguments[0] as TokenContainer; - final i = repoState.containerList.indexWhere((element) => element.serial == container.serial); + final i = containerRepoState.containerList.indexWhere((element) => element.serial == container.serial); final List newList; if (i == -1) { - newList = List.from(repoState.containerList)..add(container); + newList = List.from(containerRepoState.containerList)..add(container); } else { - newList = List.from(repoState.containerList)..[i] = container; + newList = List.from(containerRepoState.containerList)..[i] = container; } - repoState = TokenContainerState(containerList: newList); - return Future.value(repoState); + containerRepoState = TokenContainerState(containerList: newList); + return Future.value(containerRepoState); }); - when(mockRepo.deleteContainer(any)).thenAnswer((invocation) { + when(mockContainerRepo.deleteContainer(any)).thenAnswer((invocation) { final serial = invocation.positionalArguments[0] as String; - final i = repoState.containerList.indexWhere((element) => element.serial == serial); + final i = containerRepoState.containerList.indexWhere((element) => element.serial == serial); if (i == -1) { - return Future.value(repoState); + return Future.value(containerRepoState); } - final newList = List.from(repoState.containerList)..removeAt(i); - repoState = TokenContainerState(containerList: newList); - return Future.value(repoState); + final newList = List.from(containerRepoState.containerList)..removeAt(i); + containerRepoState = TokenContainerState(containerList: newList); + return Future.value(containerRepoState); }); final tokenContainerProvider = tokenContainerNotifierProviderOf( - repo: mockRepo, + repo: mockContainerRepo, containerApi: mockContainerApi, eccUtils: EccUtils(), ); await container.read(tokenContainerProvider.future); // act - await container.read(tokenContainerProvider.notifier).deleteContainer(repoState.containerList.first); + await container.read(tokenContainerProvider.notifier).deleteContainer(containerRepoState.containerList.first); // assert final state = await container.read(tokenContainerProvider.future); - verify(mockRepo.loadContainerState()).called(1); + verify(mockContainerRepo.loadContainerState()).called(1); expect(state.containerList.length, equals(0)); - expect(state, repoState); + expect(state, containerRepoState); }); test('deleteContainerList', () async { // prepare TestWidgetsFlutterBinding.ensureInitialized(); final container = ProviderContainer(); - var repoState = _getBaseState(); - repoState = repoState.copyWith( + var containerRepoState = _buildUnfinalizedContainerState(); + containerRepoState = containerRepoState.copyWith( containerList: [ - repoState.containerList.first, + containerRepoState.containerList.first, TokenContainerUnfinalized( issuer: 'issuer2', nonce: 'nonce2', @@ -370,75 +401,100 @@ void _testTokenContainerNotifier() { ), ], ); - final mockRepo = MockTokenContainerRepository(); + final mockContainerRepo = _setupMockContainerRepo(() => containerRepoState, (state) => containerRepoState = state); final mockContainerApi = MockTokenContainerApi(); when(mockContainerApi.finalizeContainer(any, any)).thenAnswer((_) async => Response('{}', 404)); - when(mockRepo.loadContainerState()).thenAnswer((_) => Future.value(repoState)); - when(mockRepo.saveContainerState(any)).thenAnswer((invocation) { - repoState = invocation.positionalArguments[0] as TokenContainerState; - return Future.value(repoState); + when(mockContainerRepo.loadContainerState()).thenAnswer((_) => Future.value(containerRepoState)); + when(mockContainerRepo.saveContainerState(any)).thenAnswer((invocation) { + containerRepoState = invocation.positionalArguments[0] as TokenContainerState; + return Future.value(containerRepoState); }); - when(mockRepo.saveContainer(any)).thenAnswer((invocation) { + when(mockContainerRepo.saveContainer(any)).thenAnswer((invocation) { final container = invocation.positionalArguments[0] as TokenContainer; - final i = repoState.containerList.indexWhere((element) => element.serial == container.serial); + final i = containerRepoState.containerList.indexWhere((element) => element.serial == container.serial); final List newList; if (i == -1) { - newList = List.from(repoState.containerList)..add(container); + newList = List.from(containerRepoState.containerList)..add(container); } else { - newList = List.from(repoState.containerList)..[i] = container; + newList = List.from(containerRepoState.containerList)..[i] = container; } - repoState = TokenContainerState(containerList: newList); - return Future.value(repoState); + containerRepoState = TokenContainerState(containerList: newList); + return Future.value(containerRepoState); }); final tokenContainerProvider = tokenContainerNotifierProviderOf( - repo: mockRepo, + repo: mockContainerRepo, containerApi: mockContainerApi, eccUtils: EccUtils(), ); await container.read(tokenContainerProvider.future); // act serial serial3 - await container.read(tokenContainerProvider.notifier).deleteContainerList([repoState.containerList[0], repoState.containerList[2]]); + await container.read(tokenContainerProvider.notifier).deleteContainerList([containerRepoState.containerList[0], containerRepoState.containerList[2]]); // assert final state = await container.read(tokenContainerProvider.future); - verify(mockRepo.loadContainerState()).called(1); + verify(mockContainerRepo.loadContainerState()).called(1); expect(state.containerList.length, equals(1)); expect(state.containerList.where((e) => e.serial == 'serial').length, equals(0)); expect(state.containerList.where((e) => e.serial == 'serial2').length, equals(1)); expect(state.containerList.where((e) => e.serial == 'serial3').length, equals(0)); - expect(state, repoState); + expect(state, containerRepoState); }); test('handleProcessorResult', () async { // prepare TestWidgetsFlutterBinding.ensureInitialized(); - final container = ProviderContainer(); - var repoState = _getBaseState(); - final mockRepo = MockTokenContainerRepository(); + var containerRepoState = TokenContainerState(containerList: []); + final mockContainerRepo = MockTokenContainerRepository(); final mockContainerApi = MockTokenContainerApi(); - when(mockContainerApi.finalizeContainer(any, any)).thenAnswer((_) async => Response('{}', 404)); - when(mockRepo.loadContainerState()).thenAnswer((_) => Future.value(repoState)); - when(mockRepo.saveContainerState(any)).thenAnswer((invocation) { - repoState = invocation.positionalArguments[0] as TokenContainerState; - return Future.value(repoState); + when(mockContainerApi.finalizeContainer(any, any)).thenAnswer( + (_) async { + final json = { + "id": 1, + "jsonrpc": "2.0", + "result": { + "status": true, + "value": { + "policies": { + "client_container_unregister": true, + "client_token_deletable": true, + "container_client_rollover": true, + "container_initial_token_transfer": true + }, + "public_server_key": "-----BEGIN PUBLIC KEY-----\n" + "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEaik84am1122p15/8Z6FYW+q5DYe22RV0\n" + "6jimjkz/5J6vDkafcP1oq5fRgIEXTTU6uKarvSrOxcV8nNuW6o80L55iT1ZmZJ+q\n" + "tx/ncmaloOEFY4dMh1XNs0TayAxKrCNg\n" + "-----END PUBLIC KEY-----", + } + }, + "time": 1731941387.8622696, + "version": "privacyIDEA 3.10", + "versionnumber": "3.10", + "signature": + "rsa_sha256_pss:5d71b5a47b9330cdb26f090b8197fdf76df34919636cc54acf476185e1282beba9a8a800dbcfd834e0068a7d3f653e2193c9b0d2c80b44801d4f85d18c0a7af554d1d9659600a5570106a595687c5131b20a4793e0992dd11c138990c47e959aa391845010f4c85dbeb35aded5a57e85d20f544d79e35b06c3231323e50c2699af6758aeeb6aafb3c9e507c7c2c0d23230e5ec09b7c26b535cd43f51368b3edc1cf4148125f8b92263c7a52eaa0def49db1a6347edc12aa9151ede67a5ab114a72b860ebdd7d9c8b48851e981fa8582a4865389bc14eea26143d36b891278be097bf841bf75697cdb16ce1bba15dcb9ffe9d4717b6f8e8780040fe078f55f7d0" + }; + return Response( + jsonEncode(json), + 200, + ); + }, + ); + when(mockContainerRepo.loadContainerState()).thenAnswer((_) => Future.value(containerRepoState)); + when(mockContainerRepo.saveContainerState(any)).thenAnswer((invocation) { + containerRepoState = invocation.positionalArguments[0] as TokenContainerState; + return Future.value(containerRepoState); }); - when(mockRepo.saveContainer(any)).thenAnswer((invocation) { + when(mockContainerRepo.saveContainer(any)).thenAnswer((invocation) { final container = invocation.positionalArguments[0] as TokenContainer; - final i = repoState.containerList.indexWhere((element) => element.serial == container.serial); + final i = containerRepoState.containerList.indexWhere((element) => element.serial == container.serial); final List newList; if (i == -1) { - newList = List.from(repoState.containerList)..add(container); + newList = List.from(containerRepoState.containerList)..add(container); } else { - newList = List.from(repoState.containerList)..[i] = container; + newList = List.from(containerRepoState.containerList)..[i] = container; } - repoState = TokenContainerState(containerList: newList); - return Future.value(repoState); + containerRepoState = TokenContainerState(containerList: newList); + return Future.value(containerRepoState); }); - final tokenContainerProvider = tokenContainerNotifierProviderOf( - repo: mockRepo, - containerApi: mockContainerApi, - eccUtils: EccUtils(), - ); - await container.read(tokenContainerProvider.future); final Uri uri = Uri.parse( 'pia://container/SMPH00067A2F?' @@ -453,153 +509,420 @@ void _testTokenContainerNotifier() { 'ssl_verify=True&' 'passphrase=', ); + + final mockTokenContainerProvider = TokenContainerNotifier( + repoOverride: mockContainerRepo, + containerApiOverride: mockContainerApi, + eccUtilsOverride: EccUtils(), + ); + final mockTokenRepo = MockTokenRepository(); + when(mockTokenRepo.loadTokens()).thenAnswer((_) => Future.value([])); + when(mockTokenRepo.saveOrReplaceTokens(any)).thenAnswer((_) => Future.value([])); + final mockTokenNotifier = TokenNotifier( + repoOverride: mockTokenRepo, + ); + final providerContainer = ProviderContainer( + overrides: [ + tokenContainerProvider.overrideWith(() => mockTokenContainerProvider), + tokenProvider.overrideWith(() => mockTokenNotifier), + ], + ); + // act + await providerContainer.read(tokenContainerProvider.future); final processorResults = await TokenContainerProcessor().processUri(uri); expect(processorResults, isNotNull); expect(processorResults!.length, 1); final result = processorResults.first; - await container.read(tokenContainerProvider.notifier).handleProcessorResult(result, {}); - - // assert - final state = await container.read(tokenContainerProvider.future); - verify(mockRepo.loadContainerState()).called(1); - expect(state, repoState); - }); - test('handleProcessorResults', () async { - // prepare - final container = ProviderContainer(); - var repoState = _getBaseState(); - final mockRepo = MockTokenContainerRepository(); - final mockContainerApi = MockTokenContainerApi(); - when(mockContainerApi.finalizeContainer(any, any)).thenAnswer((_) async => Response('{}', 404)); - when(mockRepo.loadContainerState()).thenAnswer((_) => Future.value(repoState)); - when(mockRepo.saveContainer(any)).thenAnswer((invocation) { - final container = invocation.positionalArguments[0] as TokenContainer; - final i = repoState.containerList.indexWhere((element) => element.serial == container.serial); - final List newList; - if (i == -1) { - newList = List.from(repoState.containerList)..add(container); - } else { - newList = List.from(repoState.containerList)..[i] = container; - } - repoState = TokenContainerState(containerList: newList); - return Future.value(repoState); + await providerContainer.read(tokenContainerProvider.notifier).handleProcessorResult(result, { + TokenContainerProcessor.ARG_DO_REPLACE: true, + TokenContainerProcessor.ARG_ADD_DEVICE_INFOS: true, + TokenContainerProcessor.ARG_INIT_SYNC: false, }); - final tokenContainerProvider = tokenContainerNotifierProviderOf( - repo: mockRepo, - containerApi: mockContainerApi, - eccUtils: EccUtils(), - ); - await container.read(tokenContainerProvider.future); - // act - // TODO: implement test // assert - final state = await container.read(tokenContainerProvider.future); - verify(mockRepo.loadContainerState()).called(1); - expect(state, repoState); + final state = await providerContainer.read(tokenContainerProvider.future); + verify(mockContainerRepo.loadContainerState()).called(1); + expect(state, containerRepoState); + final stateContainer = state.containerList.first as TokenContainerFinalized; + final expectedContainer = TokenContainerFinalized( + issuer: "privacyIDEA", + nonce: "dbd2ab5aa9b539484fc3b78cd4bb08375d3eb30e", + timestamp: DateTime.parse("2024-11-14 09:30:18.288530Z"), + serverUrl: Uri.parse("http://192.168.2.118:5000/"), + serial: "SMPH00067A2F", + ecKeyAlgorithm: EcKeyAlgorithm.secp384r1, + hashAlgorithm: Algorithms.SHA256, + finalizationState: RolloutState.completed, + syncState: SyncState.notStarted, + passphraseQuestion: "", + sslVerify: true, + privateClientKey: "random", + publicServerKey: "-----BEGIN PUBLIC KEY-----\n" + "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEaik84am1122p15/8Z6FYW+q5DYe22RV0\n" + "6jimjkz/5J6vDkafcP1oq5fRgIEXTTU6uKarvSrOxcV8nNuW6o80L55iT1ZmZJ+q\n" + "tx/ncmaloOEFY4dMh1XNs0TayAxKrCNg\n" + "-----END PUBLIC KEY-----", + publicClientKey: "random", + ); + expect(stateContainer.issuer, expectedContainer.issuer); + expect(stateContainer.nonce, expectedContainer.nonce); + expect(stateContainer.timestamp, expectedContainer.timestamp); + expect(stateContainer.serverUrl, expectedContainer.serverUrl); + expect(stateContainer.serial, expectedContainer.serial); + expect(stateContainer.ecKeyAlgorithm, expectedContainer.ecKeyAlgorithm); + expect(stateContainer.hashAlgorithm, expectedContainer.hashAlgorithm); + expect(stateContainer.finalizationState, expectedContainer.finalizationState); + expect(stateContainer.syncState, expectedContainer.syncState); + expect(stateContainer.passphraseQuestion, expectedContainer.passphraseQuestion); + expect(stateContainer.sslVerify, expectedContainer.sslVerify); + expect(stateContainer.privateClientKey, isNotEmpty); + expect(stateContainer.publicServerKey, expectedContainer.publicServerKey); + expect(stateContainer.publicClientKey, isNotEmpty); }); test('finalizeContainer', () async { // prepare - final container = ProviderContainer(); - var repoState = _getBaseState(); - final mockRepo = MockTokenContainerRepository(); + TestWidgetsFlutterBinding.ensureInitialized(); + var containerRepoState = _buildUnfinalizedContainerState(); + final mockContainerRepo = MockTokenContainerRepository(); final mockContainerApi = MockTokenContainerApi(); - when(mockContainerApi.finalizeContainer(any, any)).thenAnswer((_) async => Response('{}', 404)); - when(mockRepo.loadContainerState()).thenAnswer((_) => Future.value(repoState)); - when(mockRepo.saveContainer(any)).thenAnswer((invocation) { + when(mockContainerApi.finalizeContainer(any, any)).thenAnswer( + (_) async { + final json = { + "id": 1, + "jsonrpc": "2.0", + "result": { + "status": true, + "value": { + "policies": { + "client_container_unregister": true, + "client_token_deletable": true, + "container_client_rollover": true, + "container_initial_token_transfer": true + }, + "public_server_key": "-----BEGIN PUBLIC KEY-----\n" + "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEaik84am1122p15/8Z6FYW+q5DYe22RV0\n" + "6jimjkz/5J6vDkafcP1oq5fRgIEXTTU6uKarvSrOxcV8nNuW6o80L55iT1ZmZJ+q\n" + "tx/ncmaloOEFY4dMh1XNs0TayAxKrCNg\n" + "-----END PUBLIC KEY-----", + } + }, + "time": 1731941387.8622696, + "version": "privacyIDEA 3.10", + "versionnumber": "3.10", + "signature": + "rsa_sha256_pss:5d71b5a47b9330cdb26f090b8197fdf76df34919636cc54acf476185e1282beba9a8a800dbcfd834e0068a7d3f653e2193c9b0d2c80b44801d4f85d18c0a7af554d1d9659600a5570106a595687c5131b20a4793e0992dd11c138990c47e959aa391845010f4c85dbeb35aded5a57e85d20f544d79e35b06c3231323e50c2699af6758aeeb6aafb3c9e507c7c2c0d23230e5ec09b7c26b535cd43f51368b3edc1cf4148125f8b92263c7a52eaa0def49db1a6347edc12aa9151ede67a5ab114a72b860ebdd7d9c8b48851e981fa8582a4865389bc14eea26143d36b891278be097bf841bf75697cdb16ce1bba15dcb9ffe9d4717b6f8e8780040fe078f55f7d0" + }; + return Response( + jsonEncode(json), + 200, + ); + }, + ); + when(mockContainerRepo.loadContainerState()).thenAnswer((_) => Future.value(containerRepoState)); + when(mockContainerRepo.saveContainerState(any)).thenAnswer((invocation) { + containerRepoState = invocation.positionalArguments[0] as TokenContainerState; + return Future.value(containerRepoState); + }); + when(mockContainerRepo.saveContainer(any)).thenAnswer((invocation) { final container = invocation.positionalArguments[0] as TokenContainer; - final i = repoState.containerList.indexWhere((element) => element.serial == container.serial); + final i = containerRepoState.containerList.indexWhere((element) => element.serial == container.serial); final List newList; if (i == -1) { - newList = List.from(repoState.containerList)..add(container); + newList = List.from(containerRepoState.containerList)..add(container); } else { - newList = List.from(repoState.containerList)..[i] = container; + newList = List.from(containerRepoState.containerList)..[i] = container; } - repoState = TokenContainerState(containerList: newList); - return Future.value(repoState); + containerRepoState = TokenContainerState(containerList: newList); + return Future.value(containerRepoState); }); - final tokenContainerProvider = tokenContainerNotifierProviderOf( - repo: mockRepo, - containerApi: mockContainerApi, - eccUtils: EccUtils(), + + final mockTokenContainerProvider = TokenContainerNotifier( + repoOverride: mockContainerRepo, + containerApiOverride: mockContainerApi, + eccUtilsOverride: EccUtils(), ); - await container.read(tokenContainerProvider.future); + final mockTokenRepo = MockTokenRepository(); + when(mockTokenRepo.loadTokens()).thenAnswer((_) => Future.value([])); + when(mockTokenRepo.saveOrReplaceTokens(any)).thenAnswer((_) => Future.value([])); + final mockTokenNotifier = TokenNotifier( + repoOverride: mockTokenRepo, + ); + final providerContainer = ProviderContainer( + overrides: [ + tokenContainerProvider.overrideWith(() => mockTokenContainerProvider), + tokenProvider.overrideWith(() => mockTokenNotifier), + ], + ); + final container = containerRepoState.containerList.first as TokenContainerUnfinalized; // act - // TODO: implement test + + await providerContainer.read(tokenContainerProvider.notifier).finalize(containerRepoState.containerList.first, isManually: false); // assert - final state = await container.read(tokenContainerProvider.future); - verify(mockRepo.loadContainerState()).called(1); - expect(state, repoState); + final state = await providerContainer.read(tokenContainerProvider.future); + verify(mockContainerRepo.loadContainerState()).called(1); + expect(state, containerRepoState); + final stateContainer = state.containerList.first as TokenContainerFinalized; + final expectedContainer = TokenContainerFinalized( + issuer: "issuer", + nonce: "nonce", + timestamp: container.timestamp, + serverUrl: Uri.parse("https://example.com"), + serial: "serial", + ecKeyAlgorithm: EcKeyAlgorithm.secp521r1, + hashAlgorithm: Algorithms.SHA512, + finalizationState: RolloutState.completed, + syncState: SyncState.notStarted, + passphraseQuestion: null, + sslVerify: true, + privateClientKey: "random", + publicServerKey: "-----BEGIN PUBLIC KEY-----\n" + "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEaik84am1122p15/8Z6FYW+q5DYe22RV0\n" + "6jimjkz/5J6vDkafcP1oq5fRgIEXTTU6uKarvSrOxcV8nNuW6o80L55iT1ZmZJ+q\n" + "tx/ncmaloOEFY4dMh1XNs0TayAxKrCNg\n" + "-----END PUBLIC KEY-----", + publicClientKey: "random", + ); + verify(mockContainerApi.finalizeContainer(any, any)).called(1); + expect(stateContainer.issuer, expectedContainer.issuer); + expect(stateContainer.nonce, expectedContainer.nonce); + expect(stateContainer.timestamp, expectedContainer.timestamp); + expect(stateContainer.serverUrl, expectedContainer.serverUrl); + expect(stateContainer.serial, expectedContainer.serial); + expect(stateContainer.ecKeyAlgorithm, expectedContainer.ecKeyAlgorithm); + expect(stateContainer.hashAlgorithm, expectedContainer.hashAlgorithm); + expect(stateContainer.finalizationState, expectedContainer.finalizationState); + expect(stateContainer.syncState, expectedContainer.syncState); + expect(stateContainer.passphraseQuestion, expectedContainer.passphraseQuestion); + expect(stateContainer.sslVerify, expectedContainer.sslVerify); + expect(stateContainer.privateClientKey, isNotEmpty); + expect(stateContainer.publicServerKey, expectedContainer.publicServerKey); + expect(stateContainer.publicClientKey, isNotEmpty); }); - test('syncTokens', () async { + test('sync', () async { // prepare - final container = ProviderContainer(); - var repoState = _getBaseState(); - final mockRepo = MockTokenContainerRepository(); + TestWidgetsFlutterBinding.ensureInitialized(); + var containerRepoState = _buildFinalizedContainerState(); + final containerToSync = containerRepoState.containerList.first as TokenContainerFinalized; final mockContainerApi = MockTokenContainerApi(); - when(mockContainerApi.finalizeContainer(any, any)).thenAnswer((_) async => Response('{}', 404)); - when(mockRepo.loadContainerState()).thenAnswer((_) => Future.value(repoState)); - when(mockRepo.saveContainer(any)).thenAnswer((invocation) { - final container = invocation.positionalArguments[0] as TokenContainer; - final i = repoState.containerList.indexWhere((element) => element.serial == container.serial); - final List newList; - if (i == -1) { - newList = List.from(repoState.containerList)..add(container); - } else { - newList = List.from(repoState.containerList)..[i] = container; + final updatedTokens = [ + HOTPToken( + id: 'ID01', + serial: "HOTPTOKEN01", + containerSerial: "CONTAINER01", + algorithm: Algorithms.SHA256, + digits: 6, + secret: "SECRET01", + counter: 8, + ), + TOTPToken( + id: "ID03", + serial: "TOTPTOKEN01", + period: 30, + algorithm: Algorithms.SHA256, + digits: 8, + secret: "SECRET03", + ), + ]; + when(mockContainerApi.sync(any, any)).thenAnswer( + (v) async => ContainerSyncUpdates( + containerSerial: 'CONTAINER01', + updatedTokens: updatedTokens, + deleteTokenSerials: ["HOTPTOKEN02"], + newPolicies: ContainerPolicies( + rolloverAllowed: true, + initialTokenTransfer: true, + tokensDeletable: true, + unregisterAllowed: true, + ), + ), + ); + + final mockContainerRepo = _setupMockContainerRepo(() => containerRepoState, (state) => containerRepoState = state); + + final mockTokenContainerProvider = TokenContainerNotifier( + repoOverride: mockContainerRepo, + containerApiOverride: mockContainerApi, + eccUtilsOverride: EccUtils(), + ); + // prepare - token notifier + var repoTokens = { + 'ID01': HOTPToken( + id: 'ID01', + serial: "HOTPTOKEN01", + containerSerial: "CONTAINER01", + algorithm: Algorithms.SHA256, + digits: 8, + secret: "SECRET01", + counter: 10, + ), + "ID02": HOTPToken( + id: "ID02", + serial: "HOTPTOKEN02", + containerSerial: null, + algorithm: Algorithms.SHA256, + digits: 6, + secret: "SECRET02", + counter: 12, + ), + "ID04": TOTPToken( + id: "ID04", + serial: "TOTPTOKEN02", + period: 30, + algorithm: Algorithms.SHA512, + digits: 6, + secret: "SECRET04", + ), + }; + final mockTokenRepo = MockTokenRepository(); + when(mockTokenRepo.loadTokens()).thenAnswer((_) => Future.value(repoTokens.values.toList())); + when(mockTokenRepo.saveOrReplaceTokens(any)).thenAnswer((invocation) { + final tokens = invocation.positionalArguments[0] as List; + for (final token in tokens) { + repoTokens[token.id] = token; } - repoState = TokenContainerState(containerList: newList); - return Future.value(repoState); + return Future.value([]); }); - final tokenContainerProvider = tokenContainerNotifierProviderOf( - repo: mockRepo, - containerApi: mockContainerApi, - eccUtils: EccUtils(), + + final mockTokenNotifier = TokenNotifier( + repoOverride: mockTokenRepo, ); - await container.read(tokenContainerProvider.future); + + // prepare - settings notifier + final MockSettingsRepository mockSettingsRepo = MockSettingsRepository(); + when(mockSettingsRepo.loadSettings()).thenAnswer((_) => Future.value(SettingsState())); + when(mockSettingsRepo.saveSettings(any)).thenAnswer((invocation) => Future.value(invocation.positionalArguments[0])); + final SettingsNotifier settingsNotifier = SettingsNotifier(repoOverride: mockSettingsRepo); + + // prepare - provider container + final providerContainer = ProviderContainer( + overrides: [ + tokenContainerProvider.overrideWith(() => mockTokenContainerProvider), + tokenProvider.overrideWith(() => mockTokenNotifier), + settingsProvider.overrideWith(() => settingsNotifier), + ], + ); + // act - // TODO: implement test + var tokenState = providerContainer.read(tokenProvider); + await providerContainer.read(tokenContainerProvider.notifier).sync(tokenState: tokenState, isManually: false); // assert - final state = await container.read(tokenContainerProvider.future); - verify(mockRepo.loadContainerState()).called(1); - expect(state, repoState); + final expectedStateUnordered = TokenState(tokens: [...updatedTokens, repoTokens["ID04"]!]); + final containerState = await providerContainer.read(tokenContainerProvider.future); + await Future.delayed(const Duration(milliseconds: 1000)); // wait for the sync to finish + tokenState = providerContainer.read(tokenProvider); + verify(mockContainerRepo.loadContainerState()).called(1); + expect(containerState, containerRepoState); + final stateContainer = containerState.containerList.first as TokenContainerFinalized; + final expectedContainer = containerToSync.copyWith( + policies: ContainerPolicies( + rolloverAllowed: true, + initialTokenTransfer: true, + tokensDeletable: true, + unregisterAllowed: true, + ), + ); + verify(mockContainerApi.sync(any, any)).called(1); + expect(stateContainer.policies, expectedContainer.policies); + expect(stateContainer.syncState, SyncState.completed); + expect(tokenState.tokens.length, 3); + expect(tokenState.tokens, unorderedEquals(expectedStateUnordered.tokens)); }); test('getTransferQrData', () async { // prepare - final container = ProviderContainer(); - var repoState = _getBaseState(); - final mockRepo = MockTokenContainerRepository(); + final providerContainer = ProviderContainer(); + var containerRepoState = _buildFinalizedContainerState(); + final qrDataContainer = containerRepoState.containerList.first as TokenContainerFinalized; + final mockContainerRepo = _setupMockContainerRepo(() => containerRepoState, (state) => containerRepoState = state); final mockContainerApi = MockTokenContainerApi(); - when(mockContainerApi.finalizeContainer(any, any)).thenAnswer((_) async => Response('{}', 404)); - when(mockRepo.loadContainerState()).thenAnswer((_) => Future.value(repoState)); - when(mockRepo.saveContainer(any)).thenAnswer((invocation) { - final container = invocation.positionalArguments[0] as TokenContainer; - final i = repoState.containerList.indexWhere((element) => element.serial == container.serial); - final List newList; - if (i == -1) { - newList = List.from(repoState.containerList)..add(container); - } else { - newList = List.from(repoState.containerList)..[i] = container; - } - repoState = TokenContainerState(containerList: newList); - return Future.value(repoState); - }); + when(mockContainerApi.getTransferQrData(any)).thenAnswer((_) async => 'Some Random Data to be transferred'); final tokenContainerProvider = tokenContainerNotifierProviderOf( - repo: mockRepo, + repo: mockContainerRepo, containerApi: mockContainerApi, eccUtils: EccUtils(), ); - await container.read(tokenContainerProvider.future); + await providerContainer.read(tokenContainerProvider.future); + // act // TODO: implement test + final qrData = await providerContainer.read(tokenContainerProvider.notifier).getTransferQrData(qrDataContainer); // assert - final state = await container.read(tokenContainerProvider.future); - verify(mockRepo.loadContainerState()).called(1); - expect(state, repoState); + verify(mockContainerApi.getTransferQrData(any)).called(1); + expect(qrData, 'Some Random Data to be transferred'); }); }); } + +MockTokenContainerRepository _setupMockContainerRepo( + TokenContainerState Function() stateGetter, + void Function(TokenContainerState) stateSetter, +) { + final mockContainerRepo = MockTokenContainerRepository(); + when(mockContainerRepo.loadContainerState()).thenAnswer((_) => Future.value(stateGetter())); + when(mockContainerRepo.loadContainer(any)).thenAnswer((invocation) { + final serial = invocation.positionalArguments[0] as String; + if (stateGetter().containerList.isEmpty) return Future.value(null); + return Future.value(stateGetter().containerList.firstWhereOrNull((element) => element.serial == serial)); + }); + when(mockContainerRepo.saveContainer(any)).thenAnswer((invocation) { + final container = invocation.positionalArguments[0] as TokenContainer; + final i = stateGetter().containerList.indexWhere((element) => element.serial == container.serial); + final List newList; + if (i == -1) { + newList = List.from(stateGetter().containerList)..add(container); + } else { + newList = List.from(stateGetter().containerList)..[i] = container; + } + stateSetter(TokenContainerState(containerList: newList)); + return Future.value(stateGetter()); + }); + when(mockContainerRepo.saveContainerState(any)).thenAnswer((invocation) { + stateSetter(invocation.positionalArguments[0] as TokenContainerState); + return Future.value(stateGetter()); + }); + when(mockContainerRepo.saveContainerList(any)).thenAnswer((invocation) { + final containers = invocation.positionalArguments[0] as List; + final newList = List.from(stateGetter().containerList); + for (final container in containers) { + final i = newList.indexWhere((element) => element.serial == container.serial); + if (i == -1) { + newList.add(container); + } else { + newList[i] = container; + } + } + stateSetter(TokenContainerState(containerList: newList)); + return Future.value(stateGetter()); + }); + when(mockContainerRepo.deleteContainer(any)).thenAnswer((invocation) { + final serial = invocation.positionalArguments[0] as String; + final i = stateGetter().containerList.indexWhere((element) => element.serial == serial); + if (i == -1) { + return Future.value(stateGetter()); + } + final newList = List.from(stateGetter().containerList)..removeAt(i); + stateSetter(TokenContainerState(containerList: newList)); + return Future.value(stateGetter()); + }); + when(mockContainerRepo.deleteAllContainer()).thenAnswer((_) { + stateSetter(TokenContainerState(containerList: [])); + return Future.value(stateGetter()); + }); + + when(mockContainerRepo.saveContainer(any)).thenAnswer((invocation) { + final container = invocation.positionalArguments[0] as TokenContainer; + final i = stateGetter().containerList.indexWhere((element) => element.serial == container.serial); + final List newList; + if (i == -1) { + newList = List.from(stateGetter().containerList)..add(container); + } else { + newList = List.from(stateGetter().containerList)..[i] = container; + } + stateSetter(TokenContainerState(containerList: newList)); + return Future.value(stateGetter()); + }); + return mockContainerRepo; +}