From e01b79e8a211e499625df8fb1b5fe4c76465016c Mon Sep 17 00:00:00 2001 From: Srie Teja Date: Mon, 22 Jan 2024 13:30:08 +0530 Subject: [PATCH 01/16] feat: introduce utils required for registration process --- .../lib/src/util/api_call_status.dart | 1 + .../lib/src/util/onboarding_util.dart | 253 ++++++++++++++++++ .../lib/src/util/register_api_result.dart | 9 + .../lib/src/util/register_api_task.dart | 35 +++ .../lib/src/util/registrar_api_constants.dart | 22 ++ 5 files changed, 320 insertions(+) create mode 100644 packages/at_register/lib/src/util/api_call_status.dart create mode 100644 packages/at_register/lib/src/util/onboarding_util.dart create mode 100644 packages/at_register/lib/src/util/register_api_result.dart create mode 100644 packages/at_register/lib/src/util/register_api_task.dart create mode 100644 packages/at_register/lib/src/util/registrar_api_constants.dart diff --git a/packages/at_register/lib/src/util/api_call_status.dart b/packages/at_register/lib/src/util/api_call_status.dart new file mode 100644 index 00000000..56ad3637 --- /dev/null +++ b/packages/at_register/lib/src/util/api_call_status.dart @@ -0,0 +1 @@ +enum ApiCallStatus { success, failure, retry } diff --git a/packages/at_register/lib/src/util/onboarding_util.dart b/packages/at_register/lib/src/util/onboarding_util.dart new file mode 100644 index 00000000..b47b2ca4 --- /dev/null +++ b/packages/at_register/lib/src/util/onboarding_util.dart @@ -0,0 +1,253 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:at_client/at_client.dart' as at_client; +import 'registrar_api_constants.dart'; +import 'package:http/http.dart'; +import 'package:http/io_client.dart'; + +///class containing utilities to perform registration of a free atsign +class OnboardingUtil { + IOClient? _ioClient; + + void _createClient() { + HttpClient ioc = HttpClient(); + ioc.badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + _ioClient = IOClient(ioc); + } + + /// Returns a Future> containing free available atSigns of count provided as input. + Future> getFreeAtSigns( + {int amount = 1, + String authority = RegistrarApiConstants.apiHostProd}) async { + List atSigns = []; + Response response; + for (int i = 0; i < amount; i++) { + // get request at my.atsign.com/api/app/v3/get-free-atsign/ + response = + await getRequest(authority, RegistrarApiConstants.pathGetFreeAtSign); + if (response.statusCode == 200) { + String atSign = jsonDecode(response.body)['data']['atsign']; + atSigns.add(atSign); + } else { + throw at_client.AtClientException.message( + '${response.statusCode} ${response.reasonPhrase}'); + } + } + return atSigns; + } + + /// Registers the [atSign] provided in the input to the provided [email] + /// The `atSign` provided should be an unregistered and free atsign + /// Returns true if the request to send the OTP was successful. + /// Sends an OTP to the `email` provided. + /// Throws [AtException] if [atSign] is invalid + Future registerAtSign(String atSign, String email, + {oldEmail, String authority = RegistrarApiConstants.apiHostProd}) async { + Response response = + await postRequest(authority, RegistrarApiConstants.pathRegisterAtSign, { + 'atsign': atSign, + 'email': email, + 'oldEmail': oldEmail, + }); + if (response.statusCode == 200) { + Map jsonDecoded = jsonDecode(response.body); + bool sentSuccessfully = + jsonDecoded['message'].toLowerCase().contains('success'); + return sentSuccessfully; + } else { + throw at_client.AtClientException.message( + '${response.statusCode} ${response.reasonPhrase}'); + } + } + + /// Registers the [atSign] provided in the input to the provided [email] + /// The `atSign` provided should be an unregistered and free atsign + /// Validates the OTP against the atsign and registers it to the provided email if OTP is valid. + /// Returns the CRAM secret of the atsign which is registered. + /// + /// [confirmation] - Mandatory parameter for validateOTP API call. First request to be sent with confirmation as false, in this + /// case API will return cram key if the user is new otherwise will return list of already existing atsigns. + /// If the user already has existing atsigns user will have to select a listed atsign old/new and place a second call + /// to the same API endpoint with confirmation set to true with previously received OTP. The second follow-up call + /// is automated by this client using new atsign for user simplicity + /// + ///return value - Case 1("verified") - the API has registered the atsign to provided email and CRAM key present in HTTP_RESPONSE Body. + /// Case 2("follow-up"): User already has existing atsigns and new atsign registered successfully. To receive the CRAM key, follow-up by calling + /// the API with one of the existing listed atsigns, with confirmation set to true. + /// Case 3("retry"): Incorrect OTP send request again with correct OTP. + /// Throws [AtException] if [atSign] or [otp] is invalid + Future validateOtp(String atSign, String email, String otp, + {String confirmation = 'true', + String authority = RegistrarApiConstants.apiHostProd}) async { + Response response = + await postRequest(authority, RegistrarApiConstants.pathValidateOtp, { + 'atsign': atSign, + 'email': email, + 'otp': otp, + 'confirmation': confirmation, + }); + if (response.statusCode == 200) { + Map jsonDecoded = jsonDecode(response.body); + Map dataFromResponse = {}; + if (jsonDecoded.containsKey('data')) { + dataFromResponse.addAll(jsonDecoded['data']); + } + if ((jsonDecoded.containsKey('message') && + (jsonDecoded['message'] as String) + .toLowerCase() + .contains('verified')) && + jsonDecoded.containsKey('cramkey')) { + return jsonDecoded['cramkey']; + } else if (jsonDecoded.containsKey('data') && + dataFromResponse.containsKey('newAtsign')) { + return 'follow-up'; + } else if (jsonDecoded.containsKey('message') && + jsonDecoded['message'] == + 'The code you have entered is invalid or expired. Please try again?') { + return 'retry'; + } else if (jsonDecoded.containsKey('message') && + (jsonDecoded['message'] == + 'Oops! You already have the maximum number of free atSigns. Please select one of your existing atSigns.')) { + stdout.writeln( + '[Unable to proceed] This email address already has 10 free atSigns associated with it.\n' + 'To register a new atSign to this email address, please log into the dashboard \'my.atsign.com/login\'.\n' + 'Remove at least 1 atSign from your account and then try again.\n' + 'Alternatively, you can retry this process with a different email address.'); + exit(1); + } else { + throw at_client.AtClientException.message( + '${response.statusCode} ${jsonDecoded['message']}'); + } + } else { + throw at_client.AtClientException.message( + '${response.statusCode} ${response.reasonPhrase}'); + } + } + + /// Accepts a registered [atsign] as a parameter and sends a one-time verification code + /// to the email that the atsign is registered with + /// Throws an exception in the following cases: + /// 1) HTTP 400 BAD_REQUEST + /// 2) Invalid atsign + Future requestAuthenticationOtp(String atsign, + {String authority = RegistrarApiConstants.apiHostProd}) async { + Response response = await postRequest(authority, + RegistrarApiConstants.requestAuthenticationOtpPath, {'atsign': atsign}); + String apiResponseMessage = jsonDecode(response.body)['message']; + if (response.statusCode == 200) { + if (apiResponseMessage.contains('Sent Successfully')) { + stdout.writeln( + '[Information] Successfully sent verification code to your registered e-mail'); + return; + } + throw at_client.InternalServerError( + 'Unable to send verification code for authentication.\nCause: $apiResponseMessage'); + } + throw at_client.InvalidRequestException(apiResponseMessage); + } + + /// Returns the cram key for an atsign by fetching it from the registrar API + /// Accepts a registered [atsign], the verification code that was sent to + /// the registered email + /// Throws exception in the following cases: + /// 1) HTTP 400 BAD_REQUEST + Future getCramKey(String atsign, String verificationCode, + {String authority = RegistrarApiConstants.apiHostProd}) async { + Response response = await postRequest( + authority, + RegistrarApiConstants.getCramKeyWithOtpPath, + {'atsign': atsign, 'otp': verificationCode}); + Map jsonDecodedBody = jsonDecode(response.body); + if (response.statusCode == 200) { + if (jsonDecodedBody['message'] == 'Verified') { + String cram = jsonDecodedBody['cramkey']; + cram = cram.split(':')[1]; + stdout.writeln('[Information] CRAM Key fetched successfully'); + return cram; + } + throw at_client.InvalidDataException( + 'Invalid verification code. Please enter a valid verification code'); + } + throw at_client.InvalidDataException(jsonDecodedBody['message']); + } + + /// calls utility methods from [OnboardingUtil] that + /// 1) send verification code to the registered email + /// 2) fetch the CRAM key from registrar using the verification code + Future getCramUsingOtp(String atsign, String registrarUrl) async { + await requestAuthenticationOtp(atsign, authority: registrarUrl); + return await getCramKey(atsign, getVerificationCodeFromUser(), + authority: registrarUrl); + } + + /// generic GET request + Future getRequest(String authority, String path) async { + if (_ioClient == null) _createClient(); + Uri uri = Uri.https(authority, path); + Response response = await _ioClient!.get(uri, headers: { + 'Authorization': RegistrarApiConstants.authorization, + 'Content-Type': RegistrarApiConstants.contentType, + }); + return response; + } + + /// generic POST request + Future postRequest( + String authority, String path, Map data) async { + if (_ioClient == null) _createClient(); + + Uri uri = Uri.https(authority, path); + + String body = json.encode(data); + if (RegistrarApiConstants.isDebugMode) { + stdout.writeln('Sending request to url: $uri\nRequest Body: $body'); + } + Response response = await _ioClient!.post( + uri, + body: body, + headers: { + 'Authorization': RegistrarApiConstants.authorization, + 'Content-Type': RegistrarApiConstants.contentType, + }, + ); + if (RegistrarApiConstants.isDebugMode) { + print('Got Response: ${response.body}'); + } + return response; + } + + bool validateEmail(String email) { + return RegExp( + r"^[a-zA-Z0-9.a-zA-Z0-9!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") + .hasMatch(email); + } + + bool validateVerificationCode(String otp) { + if (otp.length == 4) { + return RegExp(r"^[a-zA-z0-9]").hasMatch(otp); + } + return false; + } + + /// Method to get verification code from user input + /// validates code locally and retries taking user input if invalid + /// Returns only when the user has provided a 4-length String only containing numbers and alphabets + String getVerificationCodeFromUser() { + String? otp; + stdout.writeln( + '[Action Required] Enter your verification code: (verification code is not case-sensitive)'); + otp = stdin.readLineSync()!.toUpperCase(); + while (!validateVerificationCode(otp!)) { + stderr.writeln( + '[Unable to proceed] The verification code you entered is invalid.\n' + 'Please check your email for a 4-character verification code.\n' + 'If you cannot see the code in your inbox, please check your spam/junk/promotions folders.\n' + '[Action Required] Enter your verification code:'); + otp = stdin.readLineSync()!.toUpperCase(); + } + return otp; + } +} diff --git a/packages/at_register/lib/src/util/register_api_result.dart b/packages/at_register/lib/src/util/register_api_result.dart new file mode 100644 index 00000000..c6e170f9 --- /dev/null +++ b/packages/at_register/lib/src/util/register_api_result.dart @@ -0,0 +1,9 @@ +import 'api_call_status.dart'; + +class RegisterApiResult { + dynamic data; + + late ApiCallStatus apiCallStatus; + + String? exceptionMessage; +} diff --git a/packages/at_register/lib/src/util/register_api_task.dart b/packages/at_register/lib/src/util/register_api_task.dart new file mode 100644 index 00000000..8643adc8 --- /dev/null +++ b/packages/at_register/lib/src/util/register_api_task.dart @@ -0,0 +1,35 @@ +import 'dart:collection'; + +import 'package:at_register/src/util/onboarding_util.dart'; +import 'package:at_register/src/util/register_api_result.dart'; + +/// Represents a task in an AtSign registration cycle +abstract class RegisterApiTask { + static final maximumRetries = 3; + + int retryCount = 1; + + late Map params; + + late OnboardingUtil registerUtil; + + RegisterApiResult result = RegisterApiResult(); + + ///Initializes the Task object with necessary parameters + ///[params] is a map that contains necessary data to complete atsign + /// registration process + void init(Map params, OnboardingUtil registerUtil) { + this.params = params; + result.data = HashMap(); + this.registerUtil = registerUtil; + } + + ///Implementing classes need to implement required logic in this method to + ///complete their sub-process in the AtSign registration process + Future run(); + + ///In case the task has returned a [RegisterApiResult] with status retry, this method checks and returns if the call can be retried + bool shouldRetry() { + return retryCount < maximumRetries; + } +} diff --git a/packages/at_register/lib/src/util/registrar_api_constants.dart b/packages/at_register/lib/src/util/registrar_api_constants.dart new file mode 100644 index 00000000..f6cb3ba2 --- /dev/null +++ b/packages/at_register/lib/src/util/registrar_api_constants.dart @@ -0,0 +1,22 @@ +class RegistrarApiConstants { + /// Authorities + static const String apiHostProd = 'my.atsign.com'; + static const String apiHostStaging = 'my.atsign.wtf'; + + /// API Paths + static const String pathGetFreeAtSign = '/api/app/v3/get-free-atsign'; + static const String pathRegisterAtSign = '/api/app/v3/register-person'; + static const String pathValidateOtp = '/api/app/v3/validate-person'; + static const String requestAuthenticationOtpPath = + '/api/app/v3/authenticate/atsign'; + static const String getCramKeyWithOtpPath = + '/api/app/v3/authenticate/atsign/activate'; + + /// API headers + static const String contentType = 'application/json'; + static const String authorization = '477b-876u-bcez-c42z-6a3d'; + + /// DebugMode: setting it to true will print more logs to aid understanding + /// the inner working of Register_cli + static const bool isDebugMode = false; +} From 16c135bebae6dc491d2d1236d124753c649e3556 Mon Sep 17 00:00:00 2001 From: Srie Teja Date: Tue, 30 Jan 2024 03:39:56 +0530 Subject: [PATCH 02/16] feat: consume at_register in at_onboarding_cli --- .../example/get_cram_key.dart | 2 +- .../onboard/at_onboarding_service_impl.dart | 2 +- .../lib/src/register_cli/register.dart | 7 +- .../lib/src/util/api_call_status.dart | 1 - .../src/util/at_onboarding_preference.dart | 2 +- .../lib/src/util/onboarding_util.dart | 253 ------------------ .../lib/src/util/register_api_result.dart | 9 - .../lib/src/util/register_api_task.dart | 35 --- .../lib/src/util/registrar_api_constants.dart | 22 -- packages/at_onboarding_cli/pubspec.yaml | 4 + 10 files changed, 8 insertions(+), 329 deletions(-) delete mode 100644 packages/at_onboarding_cli/lib/src/util/api_call_status.dart delete mode 100644 packages/at_onboarding_cli/lib/src/util/onboarding_util.dart delete mode 100644 packages/at_onboarding_cli/lib/src/util/register_api_result.dart delete mode 100644 packages/at_onboarding_cli/lib/src/util/register_api_task.dart delete mode 100644 packages/at_onboarding_cli/lib/src/util/registrar_api_constants.dart diff --git a/packages/at_onboarding_cli/example/get_cram_key.dart b/packages/at_onboarding_cli/example/get_cram_key.dart index 1af1b0f5..9d2842e7 100644 --- a/packages/at_onboarding_cli/example/get_cram_key.dart +++ b/packages/at_onboarding_cli/example/get_cram_key.dart @@ -1,5 +1,5 @@ import 'package:args/args.dart'; -import 'package:at_onboarding_cli/src/util/onboarding_util.dart'; +import 'package:at_register/at_register.dart'; import 'util/custom_arg_parser.dart'; diff --git a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart index 5284dc66..07703681 100644 --- a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart +++ b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart @@ -18,9 +18,9 @@ import 'package:encrypt/encrypt.dart'; import 'package:zxing2/qrcode.dart'; import 'package:image/image.dart'; import 'package:path/path.dart' as path; +import 'package:at_register/at_register.dart'; import '../util/home_directory_util.dart'; -import '../util/onboarding_util.dart'; ///class containing service that can onboard/activate/authenticate @signs class AtOnboardingServiceImpl implements AtOnboardingService { diff --git a/packages/at_onboarding_cli/lib/src/register_cli/register.dart b/packages/at_onboarding_cli/lib/src/register_cli/register.dart index eedc8eb5..2ee6fe6d 100644 --- a/packages/at_onboarding_cli/lib/src/register_cli/register.dart +++ b/packages/at_onboarding_cli/lib/src/register_cli/register.dart @@ -5,14 +5,9 @@ import 'package:args/args.dart'; import 'package:at_client/at_client.dart'; import 'package:at_onboarding_cli/src/activate_cli/activate_cli.dart' as activate_cli; -import 'package:at_onboarding_cli/src/util/api_call_status.dart'; import 'package:at_onboarding_cli/src/util/at_onboarding_exceptions.dart'; -import 'package:at_onboarding_cli/src/util/register_api_result.dart'; -import 'package:at_onboarding_cli/src/util/register_api_task.dart'; import 'package:at_utils/at_logger.dart'; - -import '../util/onboarding_util.dart'; -import '../util/registrar_api_constants.dart'; +import 'package:at_register/at_register.dart'; ///Class containing logic to register a free atsign to email provided ///through [args] by utilizing methods defined in [RegisterUtil] diff --git a/packages/at_onboarding_cli/lib/src/util/api_call_status.dart b/packages/at_onboarding_cli/lib/src/util/api_call_status.dart deleted file mode 100644 index 56ad3637..00000000 --- a/packages/at_onboarding_cli/lib/src/util/api_call_status.dart +++ /dev/null @@ -1 +0,0 @@ -enum ApiCallStatus { success, failure, retry } diff --git a/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart b/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart index b2f3ee1e..619dac78 100644 --- a/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart +++ b/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart @@ -2,7 +2,7 @@ import 'dart:core'; import 'package:at_chops/at_chops.dart'; import 'package:at_client/at_client.dart'; -import 'package:at_onboarding_cli/src/util/registrar_api_constants.dart'; +import 'package:at_register/at_register.dart'; class AtOnboardingPreference extends AtClientPreference { /// specify path of .atKeysFile containing encryption keys diff --git a/packages/at_onboarding_cli/lib/src/util/onboarding_util.dart b/packages/at_onboarding_cli/lib/src/util/onboarding_util.dart deleted file mode 100644 index 5ef2b4bc..00000000 --- a/packages/at_onboarding_cli/lib/src/util/onboarding_util.dart +++ /dev/null @@ -1,253 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:at_client/at_client.dart' as at_client; -import 'package:at_onboarding_cli/src/util/registrar_api_constants.dart'; -import 'package:http/http.dart'; -import 'package:http/io_client.dart'; - -///class containing utilities to perform registration of a free atsign -class OnboardingUtil { - IOClient? _ioClient; - - void _createClient() { - HttpClient ioc = HttpClient(); - ioc.badCertificateCallback = - (X509Certificate cert, String host, int port) => true; - _ioClient = IOClient(ioc); - } - - /// Returns a Future> containing free available atSigns of count provided as input. - Future> getFreeAtSigns( - {int amount = 1, - String authority = RegistrarApiConstants.apiHostProd}) async { - List atSigns = []; - Response response; - for (int i = 0; i < amount; i++) { - // get request at my.atsign.com/api/app/v3/get-free-atsign/ - response = - await getRequest(authority, RegistrarApiConstants.pathGetFreeAtSign); - if (response.statusCode == 200) { - String atSign = jsonDecode(response.body)['data']['atsign']; - atSigns.add(atSign); - } else { - throw at_client.AtClientException.message( - '${response.statusCode} ${response.reasonPhrase}'); - } - } - return atSigns; - } - - /// Registers the [atSign] provided in the input to the provided [email] - /// The `atSign` provided should be an unregistered and free atsign - /// Returns true if the request to send the OTP was successful. - /// Sends an OTP to the `email` provided. - /// Throws [AtException] if [atSign] is invalid - Future registerAtSign(String atSign, String email, - {oldEmail, String authority = RegistrarApiConstants.apiHostProd}) async { - Response response = - await postRequest(authority, RegistrarApiConstants.pathRegisterAtSign, { - 'atsign': atSign, - 'email': email, - 'oldEmail': oldEmail, - }); - if (response.statusCode == 200) { - Map jsonDecoded = jsonDecode(response.body); - bool sentSuccessfully = - jsonDecoded['message'].toLowerCase().contains('success'); - return sentSuccessfully; - } else { - throw at_client.AtClientException.message( - '${response.statusCode} ${response.reasonPhrase}'); - } - } - - /// Registers the [atSign] provided in the input to the provided [email] - /// The `atSign` provided should be an unregistered and free atsign - /// Validates the OTP against the atsign and registers it to the provided email if OTP is valid. - /// Returns the CRAM secret of the atsign which is registered. - /// - /// [confirmation] - Mandatory parameter for validateOTP API call. First request to be sent with confirmation as false, in this - /// case API will return cram key if the user is new otherwise will return list of already existing atsigns. - /// If the user already has existing atsigns user will have to select a listed atsign old/new and place a second call - /// to the same API endpoint with confirmation set to true with previously received OTP. The second follow-up call - /// is automated by this client using new atsign for user simplicity - /// - ///return value - Case 1("verified") - the API has registered the atsign to provided email and CRAM key present in HTTP_RESPONSE Body. - /// Case 2("follow-up"): User already has existing atsigns and new atsign registered successfully. To receive the CRAM key, follow-up by calling - /// the API with one of the existing listed atsigns, with confirmation set to true. - /// Case 3("retry"): Incorrect OTP send request again with correct OTP. - /// Throws [AtException] if [atSign] or [otp] is invalid - Future validateOtp(String atSign, String email, String otp, - {String confirmation = 'true', - String authority = RegistrarApiConstants.apiHostProd}) async { - Response response = - await postRequest(authority, RegistrarApiConstants.pathValidateOtp, { - 'atsign': atSign, - 'email': email, - 'otp': otp, - 'confirmation': confirmation, - }); - if (response.statusCode == 200) { - Map jsonDecoded = jsonDecode(response.body); - Map dataFromResponse = {}; - if (jsonDecoded.containsKey('data')) { - dataFromResponse.addAll(jsonDecoded['data']); - } - if ((jsonDecoded.containsKey('message') && - (jsonDecoded['message'] as String) - .toLowerCase() - .contains('verified')) && - jsonDecoded.containsKey('cramkey')) { - return jsonDecoded['cramkey']; - } else if (jsonDecoded.containsKey('data') && - dataFromResponse.containsKey('newAtsign')) { - return 'follow-up'; - } else if (jsonDecoded.containsKey('message') && - jsonDecoded['message'] == - 'The code you have entered is invalid or expired. Please try again?') { - return 'retry'; - } else if (jsonDecoded.containsKey('message') && - (jsonDecoded['message'] == - 'Oops! You already have the maximum number of free atSigns. Please select one of your existing atSigns.')) { - stdout.writeln( - '[Unable to proceed] This email address already has 10 free atSigns associated with it.\n' - 'To register a new atSign to this email address, please log into the dashboard \'my.atsign.com/login\'.\n' - 'Remove at least 1 atSign from your account and then try again.\n' - 'Alternatively, you can retry this process with a different email address.'); - exit(1); - } else { - throw at_client.AtClientException.message( - '${response.statusCode} ${jsonDecoded['message']}'); - } - } else { - throw at_client.AtClientException.message( - '${response.statusCode} ${response.reasonPhrase}'); - } - } - - /// Accepts a registered [atsign] as a parameter and sends a one-time verification code - /// to the email that the atsign is registered with - /// Throws an exception in the following cases: - /// 1) HTTP 400 BAD_REQUEST - /// 2) Invalid atsign - Future requestAuthenticationOtp(String atsign, - {String authority = RegistrarApiConstants.apiHostProd}) async { - Response response = await postRequest(authority, - RegistrarApiConstants.requestAuthenticationOtpPath, {'atsign': atsign}); - String apiResponseMessage = jsonDecode(response.body)['message']; - if (response.statusCode == 200) { - if (apiResponseMessage.contains('Sent Successfully')) { - stdout.writeln( - '[Information] Successfully sent verification code to your registered e-mail'); - return; - } - throw at_client.InternalServerError( - 'Unable to send verification code for authentication.\nCause: $apiResponseMessage'); - } - throw at_client.InvalidRequestException(apiResponseMessage); - } - - /// Returns the cram key for an atsign by fetching it from the registrar API - /// Accepts a registered [atsign], the verification code that was sent to - /// the registered email - /// Throws exception in the following cases: - /// 1) HTTP 400 BAD_REQUEST - Future getCramKey(String atsign, String verificationCode, - {String authority = RegistrarApiConstants.apiHostProd}) async { - Response response = await postRequest( - authority, - RegistrarApiConstants.getCramKeyWithOtpPath, - {'atsign': atsign, 'otp': verificationCode}); - Map jsonDecodedBody = jsonDecode(response.body); - if (response.statusCode == 200) { - if (jsonDecodedBody['message'] == 'Verified') { - String cram = jsonDecodedBody['cramkey']; - cram = cram.split(':')[1]; - stdout.writeln('[Information] CRAM Key fetched successfully'); - return cram; - } - throw at_client.InvalidDataException( - 'Invalid verification code. Please enter a valid verification code'); - } - throw at_client.InvalidDataException(jsonDecodedBody['message']); - } - - /// calls utility methods from [OnboardingUtil] that - /// 1) send verification code to the registered email - /// 2) fetch the CRAM key from registrar using the verification code - Future getCramUsingOtp(String atsign, String registrarUrl) async { - await requestAuthenticationOtp(atsign, authority: registrarUrl); - return await getCramKey(atsign, getVerificationCodeFromUser(), - authority: registrarUrl); - } - - /// generic GET request - Future getRequest(String authority, String path) async { - if (_ioClient == null) _createClient(); - Uri uri = Uri.https(authority, path); - Response response = await _ioClient!.get(uri, headers: { - 'Authorization': RegistrarApiConstants.authorization, - 'Content-Type': RegistrarApiConstants.contentType, - }); - return response; - } - - /// generic POST request - Future postRequest( - String authority, String path, Map data) async { - if (_ioClient == null) _createClient(); - - Uri uri = Uri.https(authority, path); - - String body = json.encode(data); - if (RegistrarApiConstants.isDebugMode) { - stdout.writeln('Sending request to url: $uri\nRequest Body: $body'); - } - Response response = await _ioClient!.post( - uri, - body: body, - headers: { - 'Authorization': RegistrarApiConstants.authorization, - 'Content-Type': RegistrarApiConstants.contentType, - }, - ); - if (RegistrarApiConstants.isDebugMode) { - print('Got Response: ${response.body}'); - } - return response; - } - - bool validateEmail(String email) { - return RegExp( - r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") - .hasMatch(email); - } - - bool validateVerificationCode(String otp) { - if (otp.length == 4) { - return RegExp(r"^[a-zA-z0-9]").hasMatch(otp); - } - return false; - } - - /// Method to get verification code from user input - /// validates code locally and retries taking user input if invalid - /// Returns only when the user has provided a 4-length String only containing numbers and alphabets - String getVerificationCodeFromUser() { - String? otp; - stdout.writeln( - '[Action Required] Enter your verification code: (verification code is not case-sensitive)'); - otp = stdin.readLineSync()!.toUpperCase(); - while (!validateVerificationCode(otp!)) { - stderr.writeln( - '[Unable to proceed] The verification code you entered is invalid.\n' - 'Please check your email for a 4-character verification code.\n' - 'If you cannot see the code in your inbox, please check your spam/junk/promotions folders.\n' - '[Action Required] Enter your verification code:'); - otp = stdin.readLineSync()!.toUpperCase(); - } - return otp; - } -} diff --git a/packages/at_onboarding_cli/lib/src/util/register_api_result.dart b/packages/at_onboarding_cli/lib/src/util/register_api_result.dart deleted file mode 100644 index c6e170f9..00000000 --- a/packages/at_onboarding_cli/lib/src/util/register_api_result.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'api_call_status.dart'; - -class RegisterApiResult { - dynamic data; - - late ApiCallStatus apiCallStatus; - - String? exceptionMessage; -} diff --git a/packages/at_onboarding_cli/lib/src/util/register_api_task.dart b/packages/at_onboarding_cli/lib/src/util/register_api_task.dart deleted file mode 100644 index d45b0e0d..00000000 --- a/packages/at_onboarding_cli/lib/src/util/register_api_task.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'dart:collection'; - -import 'package:at_onboarding_cli/src/util/onboarding_util.dart'; -import 'package:at_onboarding_cli/src/util/register_api_result.dart'; - -/// Represents a task in an AtSign registration cycle -abstract class RegisterApiTask { - static final maximumRetries = 3; - - int retryCount = 1; - - late Map params; - - late OnboardingUtil registerUtil; - - RegisterApiResult result = RegisterApiResult(); - - ///Initializes the Task object with necessary parameters - ///[params] is a map that contains necessary data to complete atsign - /// registration process - void init(Map params, OnboardingUtil registerUtil) { - this.params = params; - result.data = HashMap(); - this.registerUtil = registerUtil; - } - - ///Implementing classes need to implement required logic in this method to - ///complete their sub-process in the AtSign registration process - Future run(); - - ///In case the task has returned a [RegisterApiResult] with status retry, this method checks and returns if the call can be retried - bool shouldRetry() { - return retryCount < maximumRetries; - } -} diff --git a/packages/at_onboarding_cli/lib/src/util/registrar_api_constants.dart b/packages/at_onboarding_cli/lib/src/util/registrar_api_constants.dart deleted file mode 100644 index f6cb3ba2..00000000 --- a/packages/at_onboarding_cli/lib/src/util/registrar_api_constants.dart +++ /dev/null @@ -1,22 +0,0 @@ -class RegistrarApiConstants { - /// Authorities - static const String apiHostProd = 'my.atsign.com'; - static const String apiHostStaging = 'my.atsign.wtf'; - - /// API Paths - static const String pathGetFreeAtSign = '/api/app/v3/get-free-atsign'; - static const String pathRegisterAtSign = '/api/app/v3/register-person'; - static const String pathValidateOtp = '/api/app/v3/validate-person'; - static const String requestAuthenticationOtpPath = - '/api/app/v3/authenticate/atsign'; - static const String getCramKeyWithOtpPath = - '/api/app/v3/authenticate/atsign/activate'; - - /// API headers - static const String contentType = 'application/json'; - static const String authorization = '477b-876u-bcez-c42z-6a3d'; - - /// DebugMode: setting it to true will print more logs to aid understanding - /// the inner working of Register_cli - static const bool isDebugMode = false; -} diff --git a/packages/at_onboarding_cli/pubspec.yaml b/packages/at_onboarding_cli/pubspec.yaml index 62f8521a..22164af2 100644 --- a/packages/at_onboarding_cli/pubspec.yaml +++ b/packages/at_onboarding_cli/pubspec.yaml @@ -28,6 +28,10 @@ dependencies: at_server_status: ^1.0.4 at_utils: ^3.0.16 +dependency_overrides: + at_register: + path: /home/srie/Desktop/work/at_libraries/packages/at_register + dev_dependencies: lints: ^2.1.0 test: ^1.24.2 From 92692bb8a3710009f735692c372963b3c2d8e0e6 Mon Sep 17 00:00:00 2001 From: Srie Teja Date: Tue, 30 Jan 2024 03:52:16 +0530 Subject: [PATCH 03/16] feat: rename classes in at_register --- .../lib/src/register_cli/register.dart | 36 ++++++------ .../src/util/at_onboarding_preference.dart | 2 +- packages/at_register/lib/at_register.dart | 10 ++++ ...arding_util.dart => at_register_base.dart} | 56 +++++++++---------- ...r_api_result.dart => register_result.dart} | 2 +- ...ister_api_task.dart => register_task.dart} | 12 ++-- ...onstants.dart => registrar_constants.dart} | 2 +- 7 files changed, 65 insertions(+), 55 deletions(-) create mode 100644 packages/at_register/lib/at_register.dart rename packages/at_register/lib/src/{util/onboarding_util.dart => at_register_base.dart} (82%) rename packages/at_register/lib/src/util/{register_api_result.dart => register_result.dart} (81%) rename packages/at_register/lib/src/util/{register_api_task.dart => register_task.dart} (67%) rename packages/at_register/lib/src/util/{registrar_api_constants.dart => registrar_constants.dart} (96%) diff --git a/packages/at_onboarding_cli/lib/src/register_cli/register.dart b/packages/at_onboarding_cli/lib/src/register_cli/register.dart index 2ee6fe6d..f36a4a6b 100644 --- a/packages/at_onboarding_cli/lib/src/register_cli/register.dart +++ b/packages/at_onboarding_cli/lib/src/register_cli/register.dart @@ -49,9 +49,9 @@ class Register { //set the following parameter to RegisterApiConstants.apiHostStaging //to use the staging environment - params['authority'] = RegistrarApiConstants.apiHostProd; + params['authority'] = RegistrarConstants.apiHostProd; - //create stream of tasks each of type [RegisterApiTask] and then + //create stream of tasks each of type [RegisterTask] and then // call start on the stream of tasks await RegistrationFlow(params, registerUtil) .add(GetFreeAtsign()) @@ -63,27 +63,27 @@ class Register { } } -///class that handles multiple tasks of type [RegisterApiTask] +///class that handles multiple tasks of type [RegisterTask] ///Initialized with a params map that needs to be populated with - email and api host address -///[add] method can be used to add tasks[RegisterApiTask] to the [processFlow] +///[add] method can be used to add tasks[RegisterTask] to the [processFlow] ///[start] needs to be called after all required tasks are added to the [processFlow] class RegistrationFlow { - List processFlow = []; - RegisterApiResult result = RegisterApiResult(); + List processFlow = []; + RegisterResult result = RegisterResult(); late OnboardingUtil registerUtil; Map params; RegistrationFlow(this.params, this.registerUtil); - RegistrationFlow add(RegisterApiTask task) { + RegistrationFlow add(RegisterTask task) { processFlow.add(task); return this; } Future start() async { - for (RegisterApiTask task in processFlow) { + for (RegisterTask task in processFlow) { task.init(params, registerUtil); - if (RegistrarApiConstants.isDebugMode) { + if (RegistrarConstants.isDebugMode) { print('Current Task: $task [params=$params]\n'); } result = await task.run(); @@ -103,12 +103,12 @@ class RegistrationFlow { } } -///This is a [RegisterApiTask] that fetches a free atsign +///This is a [RegisterTask] that fetches a free atsign ///throws [AtException] with concerned message which was encountered in the ///HTTP GET/POST request -class GetFreeAtsign extends RegisterApiTask { +class GetFreeAtsign extends RegisterTask { @override - Future run() async { + Future run() async { stdout .writeln('[Information] Getting your randomly generated free atSign…'); try { @@ -127,13 +127,13 @@ class GetFreeAtsign extends RegisterApiTask { } } -///This is a [RegisterApiTask] that registers a free atsign fetched in +///This is a [RegisterTask] that registers a free atsign fetched in ///[GetFreeAtsign] to the email provided as args ///throws [AtException] with concerned message which was encountered in the ///HTTP GET/POST request -class RegisterAtsign extends RegisterApiTask { +class RegisterAtsign extends RegisterTask { @override - Future run() async { + Future run() async { stdout.writeln( '[Information] Sending verification code to: ${params['email']}'); try { @@ -153,11 +153,11 @@ class RegisterAtsign extends RegisterApiTask { } } -///This is a [RegisterApiTask] that validates the otp which was sent as a part +///This is a [RegisterTask] that validates the otp which was sent as a part ///of [RegisterAtsign] to email provided in args ///throws [AtException] with concerned message which was encountered in the ///HTTP GET/POST request -class ValidateOtp extends RegisterApiTask { +class ValidateOtp extends RegisterTask { @override void init(Map params, OnboardingUtil registerUtil) { params['confirmation'] = 'false'; @@ -167,7 +167,7 @@ class ValidateOtp extends RegisterApiTask { } @override - Future run() async { + Future run() async { if (params['otp'] == null) { params['otp'] = registerUtil.getVerificationCodeFromUser(); } diff --git a/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart b/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart index 619dac78..0292a3a5 100644 --- a/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart +++ b/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart @@ -28,7 +28,7 @@ class AtOnboardingPreference extends AtClientPreference { bool skipSync = false; /// the hostName of the registrar which will be used to activate the atsign - String registrarUrl = RegistrarApiConstants.apiHostProd; + String registrarUrl = RegistrarConstants.apiHostProd; String? appName; diff --git a/packages/at_register/lib/at_register.dart b/packages/at_register/lib/at_register.dart new file mode 100644 index 00000000..86c57e58 --- /dev/null +++ b/packages/at_register/lib/at_register.dart @@ -0,0 +1,10 @@ +/// Support for doing something awesome. +/// +/// More dartdocs go here. +library; + +export 'src/at_register_base.dart'; +export 'src/util/api_call_status.dart'; +export 'src/util/register_result.dart'; +export 'src/util/register_task.dart'; +export 'src/util/registrar_constants.dart'; diff --git a/packages/at_register/lib/src/util/onboarding_util.dart b/packages/at_register/lib/src/at_register_base.dart similarity index 82% rename from packages/at_register/lib/src/util/onboarding_util.dart rename to packages/at_register/lib/src/at_register_base.dart index b47b2ca4..fd3c604d 100644 --- a/packages/at_register/lib/src/util/onboarding_util.dart +++ b/packages/at_register/lib/src/at_register_base.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:at_client/at_client.dart' as at_client; -import 'registrar_api_constants.dart'; +import 'package:at_register/src/util/registrar_constants.dart'; import 'package:http/http.dart'; import 'package:http/io_client.dart'; @@ -21,13 +21,13 @@ class OnboardingUtil { /// Returns a Future> containing free available atSigns of count provided as input. Future> getFreeAtSigns( {int amount = 1, - String authority = RegistrarApiConstants.apiHostProd}) async { + String authority = RegistrarConstants.apiHostProd}) async { List atSigns = []; Response response; for (int i = 0; i < amount; i++) { // get request at my.atsign.com/api/app/v3/get-free-atsign/ response = - await getRequest(authority, RegistrarApiConstants.pathGetFreeAtSign); + await getRequest(authority, RegistrarConstants.pathGetFreeAtSign); if (response.statusCode == 200) { String atSign = jsonDecode(response.body)['data']['atsign']; atSigns.add(atSign); @@ -45,9 +45,9 @@ class OnboardingUtil { /// Sends an OTP to the `email` provided. /// Throws [AtException] if [atSign] is invalid Future registerAtSign(String atSign, String email, - {oldEmail, String authority = RegistrarApiConstants.apiHostProd}) async { + {oldEmail, String authority = RegistrarConstants.apiHostProd}) async { Response response = - await postRequest(authority, RegistrarApiConstants.pathRegisterAtSign, { + await postRequest(authority, RegistrarConstants.pathRegisterAtSign, { 'atsign': atSign, 'email': email, 'oldEmail': oldEmail, @@ -55,7 +55,7 @@ class OnboardingUtil { if (response.statusCode == 200) { Map jsonDecoded = jsonDecode(response.body); bool sentSuccessfully = - jsonDecoded['message'].toLowerCase().contains('success'); + jsonDecoded['message'].toLowerCase().contains('success'); return sentSuccessfully; } else { throw at_client.AtClientException.message( @@ -81,9 +81,9 @@ class OnboardingUtil { /// Throws [AtException] if [atSign] or [otp] is invalid Future validateOtp(String atSign, String email, String otp, {String confirmation = 'true', - String authority = RegistrarApiConstants.apiHostProd}) async { + String authority = RegistrarConstants.apiHostProd}) async { Response response = - await postRequest(authority, RegistrarApiConstants.pathValidateOtp, { + await postRequest(authority, RegistrarConstants.pathValidateOtp, { 'atsign': atSign, 'email': email, 'otp': otp, @@ -96,9 +96,9 @@ class OnboardingUtil { dataFromResponse.addAll(jsonDecoded['data']); } if ((jsonDecoded.containsKey('message') && - (jsonDecoded['message'] as String) - .toLowerCase() - .contains('verified')) && + (jsonDecoded['message'] as String) + .toLowerCase() + .contains('verified')) && jsonDecoded.containsKey('cramkey')) { return jsonDecoded['cramkey']; } else if (jsonDecoded.containsKey('data') && @@ -113,9 +113,9 @@ class OnboardingUtil { 'Oops! You already have the maximum number of free atSigns. Please select one of your existing atSigns.')) { stdout.writeln( '[Unable to proceed] This email address already has 10 free atSigns associated with it.\n' - 'To register a new atSign to this email address, please log into the dashboard \'my.atsign.com/login\'.\n' - 'Remove at least 1 atSign from your account and then try again.\n' - 'Alternatively, you can retry this process with a different email address.'); + 'To register a new atSign to this email address, please log into the dashboard \'my.atsign.com/login\'.\n' + 'Remove at least 1 atSign from your account and then try again.\n' + 'Alternatively, you can retry this process with a different email address.'); exit(1); } else { throw at_client.AtClientException.message( @@ -133,9 +133,9 @@ class OnboardingUtil { /// 1) HTTP 400 BAD_REQUEST /// 2) Invalid atsign Future requestAuthenticationOtp(String atsign, - {String authority = RegistrarApiConstants.apiHostProd}) async { + {String authority = RegistrarConstants.apiHostProd}) async { Response response = await postRequest(authority, - RegistrarApiConstants.requestAuthenticationOtpPath, {'atsign': atsign}); + RegistrarConstants.requestAuthenticationOtpPath, {'atsign': atsign}); String apiResponseMessage = jsonDecode(response.body)['message']; if (response.statusCode == 200) { if (apiResponseMessage.contains('Sent Successfully')) { @@ -155,10 +155,10 @@ class OnboardingUtil { /// Throws exception in the following cases: /// 1) HTTP 400 BAD_REQUEST Future getCramKey(String atsign, String verificationCode, - {String authority = RegistrarApiConstants.apiHostProd}) async { + {String authority = RegistrarConstants.apiHostProd}) async { Response response = await postRequest( authority, - RegistrarApiConstants.getCramKeyWithOtpPath, + RegistrarConstants.getCramKeyWithOtpPath, {'atsign': atsign, 'otp': verificationCode}); Map jsonDecodedBody = jsonDecode(response.body); if (response.statusCode == 200) { @@ -188,8 +188,8 @@ class OnboardingUtil { if (_ioClient == null) _createClient(); Uri uri = Uri.https(authority, path); Response response = await _ioClient!.get(uri, headers: { - 'Authorization': RegistrarApiConstants.authorization, - 'Content-Type': RegistrarApiConstants.contentType, + 'Authorization': RegistrarConstants.authorization, + 'Content-Type': RegistrarConstants.contentType, }); return response; } @@ -202,18 +202,18 @@ class OnboardingUtil { Uri uri = Uri.https(authority, path); String body = json.encode(data); - if (RegistrarApiConstants.isDebugMode) { + if (RegistrarConstants.isDebugMode) { stdout.writeln('Sending request to url: $uri\nRequest Body: $body'); } Response response = await _ioClient!.post( uri, body: body, headers: { - 'Authorization': RegistrarApiConstants.authorization, - 'Content-Type': RegistrarApiConstants.contentType, + 'Authorization': RegistrarConstants.authorization, + 'Content-Type': RegistrarConstants.contentType, }, ); - if (RegistrarApiConstants.isDebugMode) { + if (RegistrarConstants.isDebugMode) { print('Got Response: ${response.body}'); } return response; @@ -221,7 +221,7 @@ class OnboardingUtil { bool validateEmail(String email) { return RegExp( - r"^[a-zA-Z0-9.a-zA-Z0-9!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") + r"^[a-zA-Z0-9.a-zA-Z0-9!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") .hasMatch(email); } @@ -243,9 +243,9 @@ class OnboardingUtil { while (!validateVerificationCode(otp!)) { stderr.writeln( '[Unable to proceed] The verification code you entered is invalid.\n' - 'Please check your email for a 4-character verification code.\n' - 'If you cannot see the code in your inbox, please check your spam/junk/promotions folders.\n' - '[Action Required] Enter your verification code:'); + 'Please check your email for a 4-character verification code.\n' + 'If you cannot see the code in your inbox, please check your spam/junk/promotions folders.\n' + '[Action Required] Enter your verification code:'); otp = stdin.readLineSync()!.toUpperCase(); } return otp; diff --git a/packages/at_register/lib/src/util/register_api_result.dart b/packages/at_register/lib/src/util/register_result.dart similarity index 81% rename from packages/at_register/lib/src/util/register_api_result.dart rename to packages/at_register/lib/src/util/register_result.dart index c6e170f9..e5ea0981 100644 --- a/packages/at_register/lib/src/util/register_api_result.dart +++ b/packages/at_register/lib/src/util/register_result.dart @@ -1,6 +1,6 @@ import 'api_call_status.dart'; -class RegisterApiResult { +class RegisterResult { dynamic data; late ApiCallStatus apiCallStatus; diff --git a/packages/at_register/lib/src/util/register_api_task.dart b/packages/at_register/lib/src/util/register_task.dart similarity index 67% rename from packages/at_register/lib/src/util/register_api_task.dart rename to packages/at_register/lib/src/util/register_task.dart index 8643adc8..ec279b43 100644 --- a/packages/at_register/lib/src/util/register_api_task.dart +++ b/packages/at_register/lib/src/util/register_task.dart @@ -1,10 +1,10 @@ import 'dart:collection'; -import 'package:at_register/src/util/onboarding_util.dart'; -import 'package:at_register/src/util/register_api_result.dart'; +import 'package:at_register/at_register.dart'; +import 'package:at_register/src/util/register_result.dart'; /// Represents a task in an AtSign registration cycle -abstract class RegisterApiTask { +abstract class RegisterTask { static final maximumRetries = 3; int retryCount = 1; @@ -13,7 +13,7 @@ abstract class RegisterApiTask { late OnboardingUtil registerUtil; - RegisterApiResult result = RegisterApiResult(); + RegisterResult result = RegisterResult(); ///Initializes the Task object with necessary parameters ///[params] is a map that contains necessary data to complete atsign @@ -26,9 +26,9 @@ abstract class RegisterApiTask { ///Implementing classes need to implement required logic in this method to ///complete their sub-process in the AtSign registration process - Future run(); + Future run(); - ///In case the task has returned a [RegisterApiResult] with status retry, this method checks and returns if the call can be retried + ///In case the task has returned a [RegisterResult] with status retry, this method checks and returns if the call can be retried bool shouldRetry() { return retryCount < maximumRetries; } diff --git a/packages/at_register/lib/src/util/registrar_api_constants.dart b/packages/at_register/lib/src/util/registrar_constants.dart similarity index 96% rename from packages/at_register/lib/src/util/registrar_api_constants.dart rename to packages/at_register/lib/src/util/registrar_constants.dart index f6cb3ba2..e2897c1b 100644 --- a/packages/at_register/lib/src/util/registrar_api_constants.dart +++ b/packages/at_register/lib/src/util/registrar_constants.dart @@ -1,4 +1,4 @@ -class RegistrarApiConstants { +class RegistrarConstants { /// Authorities static const String apiHostProd = 'my.atsign.com'; static const String apiHostStaging = 'my.atsign.wtf'; From dacd2792e9724a67695143e04a2741708b3703c5 Mon Sep 17 00:00:00 2001 From: Srie Teja Date: Tue, 30 Jan 2024 03:39:56 +0530 Subject: [PATCH 04/16] feat: consume at_register in at_onboarding_cli --- .../example/get_cram_key.dart | 2 +- .../onboard/at_onboarding_service_impl.dart | 2 +- .../lib/src/register_cli/register.dart | 43 ++- .../lib/src/util/api_call_status.dart | 1 - .../src/util/at_onboarding_preference.dart | 4 +- .../lib/src/util/onboarding_util.dart | 253 ------------------ .../lib/src/util/register_api_result.dart | 9 - .../lib/src/util/register_api_task.dart | 35 --- packages/at_onboarding_cli/pubspec.yaml | 4 + packages/at_register/lib/at_register.dart | 10 + ...arding_util.dart => at_register_base.dart} | 56 ++-- ...r_api_result.dart => register_result.dart} | 2 +- ...ister_api_task.dart => register_task.dart} | 12 +- .../lib/src/util/registrar_api_constants.dart | 22 -- .../lib/src/util/registrar_constants.dart} | 2 +- 15 files changed, 73 insertions(+), 384 deletions(-) delete mode 100644 packages/at_onboarding_cli/lib/src/util/api_call_status.dart delete mode 100644 packages/at_onboarding_cli/lib/src/util/onboarding_util.dart delete mode 100644 packages/at_onboarding_cli/lib/src/util/register_api_result.dart delete mode 100644 packages/at_onboarding_cli/lib/src/util/register_api_task.dart create mode 100644 packages/at_register/lib/at_register.dart rename packages/at_register/lib/src/{util/onboarding_util.dart => at_register_base.dart} (82%) rename packages/at_register/lib/src/util/{register_api_result.dart => register_result.dart} (81%) rename packages/at_register/lib/src/util/{register_api_task.dart => register_task.dart} (67%) delete mode 100644 packages/at_register/lib/src/util/registrar_api_constants.dart rename packages/{at_onboarding_cli/lib/src/util/registrar_api_constants.dart => at_register/lib/src/util/registrar_constants.dart} (96%) diff --git a/packages/at_onboarding_cli/example/get_cram_key.dart b/packages/at_onboarding_cli/example/get_cram_key.dart index 1af1b0f5..9d2842e7 100644 --- a/packages/at_onboarding_cli/example/get_cram_key.dart +++ b/packages/at_onboarding_cli/example/get_cram_key.dart @@ -1,5 +1,5 @@ import 'package:args/args.dart'; -import 'package:at_onboarding_cli/src/util/onboarding_util.dart'; +import 'package:at_register/at_register.dart'; import 'util/custom_arg_parser.dart'; diff --git a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart index 5284dc66..07703681 100644 --- a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart +++ b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart @@ -18,9 +18,9 @@ import 'package:encrypt/encrypt.dart'; import 'package:zxing2/qrcode.dart'; import 'package:image/image.dart'; import 'package:path/path.dart' as path; +import 'package:at_register/at_register.dart'; import '../util/home_directory_util.dart'; -import '../util/onboarding_util.dart'; ///class containing service that can onboard/activate/authenticate @signs class AtOnboardingServiceImpl implements AtOnboardingService { diff --git a/packages/at_onboarding_cli/lib/src/register_cli/register.dart b/packages/at_onboarding_cli/lib/src/register_cli/register.dart index eedc8eb5..f36a4a6b 100644 --- a/packages/at_onboarding_cli/lib/src/register_cli/register.dart +++ b/packages/at_onboarding_cli/lib/src/register_cli/register.dart @@ -5,14 +5,9 @@ import 'package:args/args.dart'; import 'package:at_client/at_client.dart'; import 'package:at_onboarding_cli/src/activate_cli/activate_cli.dart' as activate_cli; -import 'package:at_onboarding_cli/src/util/api_call_status.dart'; import 'package:at_onboarding_cli/src/util/at_onboarding_exceptions.dart'; -import 'package:at_onboarding_cli/src/util/register_api_result.dart'; -import 'package:at_onboarding_cli/src/util/register_api_task.dart'; import 'package:at_utils/at_logger.dart'; - -import '../util/onboarding_util.dart'; -import '../util/registrar_api_constants.dart'; +import 'package:at_register/at_register.dart'; ///Class containing logic to register a free atsign to email provided ///through [args] by utilizing methods defined in [RegisterUtil] @@ -54,9 +49,9 @@ class Register { //set the following parameter to RegisterApiConstants.apiHostStaging //to use the staging environment - params['authority'] = RegistrarApiConstants.apiHostProd; + params['authority'] = RegistrarConstants.apiHostProd; - //create stream of tasks each of type [RegisterApiTask] and then + //create stream of tasks each of type [RegisterTask] and then // call start on the stream of tasks await RegistrationFlow(params, registerUtil) .add(GetFreeAtsign()) @@ -68,27 +63,27 @@ class Register { } } -///class that handles multiple tasks of type [RegisterApiTask] +///class that handles multiple tasks of type [RegisterTask] ///Initialized with a params map that needs to be populated with - email and api host address -///[add] method can be used to add tasks[RegisterApiTask] to the [processFlow] +///[add] method can be used to add tasks[RegisterTask] to the [processFlow] ///[start] needs to be called after all required tasks are added to the [processFlow] class RegistrationFlow { - List processFlow = []; - RegisterApiResult result = RegisterApiResult(); + List processFlow = []; + RegisterResult result = RegisterResult(); late OnboardingUtil registerUtil; Map params; RegistrationFlow(this.params, this.registerUtil); - RegistrationFlow add(RegisterApiTask task) { + RegistrationFlow add(RegisterTask task) { processFlow.add(task); return this; } Future start() async { - for (RegisterApiTask task in processFlow) { + for (RegisterTask task in processFlow) { task.init(params, registerUtil); - if (RegistrarApiConstants.isDebugMode) { + if (RegistrarConstants.isDebugMode) { print('Current Task: $task [params=$params]\n'); } result = await task.run(); @@ -108,12 +103,12 @@ class RegistrationFlow { } } -///This is a [RegisterApiTask] that fetches a free atsign +///This is a [RegisterTask] that fetches a free atsign ///throws [AtException] with concerned message which was encountered in the ///HTTP GET/POST request -class GetFreeAtsign extends RegisterApiTask { +class GetFreeAtsign extends RegisterTask { @override - Future run() async { + Future run() async { stdout .writeln('[Information] Getting your randomly generated free atSign…'); try { @@ -132,13 +127,13 @@ class GetFreeAtsign extends RegisterApiTask { } } -///This is a [RegisterApiTask] that registers a free atsign fetched in +///This is a [RegisterTask] that registers a free atsign fetched in ///[GetFreeAtsign] to the email provided as args ///throws [AtException] with concerned message which was encountered in the ///HTTP GET/POST request -class RegisterAtsign extends RegisterApiTask { +class RegisterAtsign extends RegisterTask { @override - Future run() async { + Future run() async { stdout.writeln( '[Information] Sending verification code to: ${params['email']}'); try { @@ -158,11 +153,11 @@ class RegisterAtsign extends RegisterApiTask { } } -///This is a [RegisterApiTask] that validates the otp which was sent as a part +///This is a [RegisterTask] that validates the otp which was sent as a part ///of [RegisterAtsign] to email provided in args ///throws [AtException] with concerned message which was encountered in the ///HTTP GET/POST request -class ValidateOtp extends RegisterApiTask { +class ValidateOtp extends RegisterTask { @override void init(Map params, OnboardingUtil registerUtil) { params['confirmation'] = 'false'; @@ -172,7 +167,7 @@ class ValidateOtp extends RegisterApiTask { } @override - Future run() async { + Future run() async { if (params['otp'] == null) { params['otp'] = registerUtil.getVerificationCodeFromUser(); } diff --git a/packages/at_onboarding_cli/lib/src/util/api_call_status.dart b/packages/at_onboarding_cli/lib/src/util/api_call_status.dart deleted file mode 100644 index 56ad3637..00000000 --- a/packages/at_onboarding_cli/lib/src/util/api_call_status.dart +++ /dev/null @@ -1 +0,0 @@ -enum ApiCallStatus { success, failure, retry } diff --git a/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart b/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart index b2f3ee1e..0292a3a5 100644 --- a/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart +++ b/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart @@ -2,7 +2,7 @@ import 'dart:core'; import 'package:at_chops/at_chops.dart'; import 'package:at_client/at_client.dart'; -import 'package:at_onboarding_cli/src/util/registrar_api_constants.dart'; +import 'package:at_register/at_register.dart'; class AtOnboardingPreference extends AtClientPreference { /// specify path of .atKeysFile containing encryption keys @@ -28,7 +28,7 @@ class AtOnboardingPreference extends AtClientPreference { bool skipSync = false; /// the hostName of the registrar which will be used to activate the atsign - String registrarUrl = RegistrarApiConstants.apiHostProd; + String registrarUrl = RegistrarConstants.apiHostProd; String? appName; diff --git a/packages/at_onboarding_cli/lib/src/util/onboarding_util.dart b/packages/at_onboarding_cli/lib/src/util/onboarding_util.dart deleted file mode 100644 index 5ef2b4bc..00000000 --- a/packages/at_onboarding_cli/lib/src/util/onboarding_util.dart +++ /dev/null @@ -1,253 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:at_client/at_client.dart' as at_client; -import 'package:at_onboarding_cli/src/util/registrar_api_constants.dart'; -import 'package:http/http.dart'; -import 'package:http/io_client.dart'; - -///class containing utilities to perform registration of a free atsign -class OnboardingUtil { - IOClient? _ioClient; - - void _createClient() { - HttpClient ioc = HttpClient(); - ioc.badCertificateCallback = - (X509Certificate cert, String host, int port) => true; - _ioClient = IOClient(ioc); - } - - /// Returns a Future> containing free available atSigns of count provided as input. - Future> getFreeAtSigns( - {int amount = 1, - String authority = RegistrarApiConstants.apiHostProd}) async { - List atSigns = []; - Response response; - for (int i = 0; i < amount; i++) { - // get request at my.atsign.com/api/app/v3/get-free-atsign/ - response = - await getRequest(authority, RegistrarApiConstants.pathGetFreeAtSign); - if (response.statusCode == 200) { - String atSign = jsonDecode(response.body)['data']['atsign']; - atSigns.add(atSign); - } else { - throw at_client.AtClientException.message( - '${response.statusCode} ${response.reasonPhrase}'); - } - } - return atSigns; - } - - /// Registers the [atSign] provided in the input to the provided [email] - /// The `atSign` provided should be an unregistered and free atsign - /// Returns true if the request to send the OTP was successful. - /// Sends an OTP to the `email` provided. - /// Throws [AtException] if [atSign] is invalid - Future registerAtSign(String atSign, String email, - {oldEmail, String authority = RegistrarApiConstants.apiHostProd}) async { - Response response = - await postRequest(authority, RegistrarApiConstants.pathRegisterAtSign, { - 'atsign': atSign, - 'email': email, - 'oldEmail': oldEmail, - }); - if (response.statusCode == 200) { - Map jsonDecoded = jsonDecode(response.body); - bool sentSuccessfully = - jsonDecoded['message'].toLowerCase().contains('success'); - return sentSuccessfully; - } else { - throw at_client.AtClientException.message( - '${response.statusCode} ${response.reasonPhrase}'); - } - } - - /// Registers the [atSign] provided in the input to the provided [email] - /// The `atSign` provided should be an unregistered and free atsign - /// Validates the OTP against the atsign and registers it to the provided email if OTP is valid. - /// Returns the CRAM secret of the atsign which is registered. - /// - /// [confirmation] - Mandatory parameter for validateOTP API call. First request to be sent with confirmation as false, in this - /// case API will return cram key if the user is new otherwise will return list of already existing atsigns. - /// If the user already has existing atsigns user will have to select a listed atsign old/new and place a second call - /// to the same API endpoint with confirmation set to true with previously received OTP. The second follow-up call - /// is automated by this client using new atsign for user simplicity - /// - ///return value - Case 1("verified") - the API has registered the atsign to provided email and CRAM key present in HTTP_RESPONSE Body. - /// Case 2("follow-up"): User already has existing atsigns and new atsign registered successfully. To receive the CRAM key, follow-up by calling - /// the API with one of the existing listed atsigns, with confirmation set to true. - /// Case 3("retry"): Incorrect OTP send request again with correct OTP. - /// Throws [AtException] if [atSign] or [otp] is invalid - Future validateOtp(String atSign, String email, String otp, - {String confirmation = 'true', - String authority = RegistrarApiConstants.apiHostProd}) async { - Response response = - await postRequest(authority, RegistrarApiConstants.pathValidateOtp, { - 'atsign': atSign, - 'email': email, - 'otp': otp, - 'confirmation': confirmation, - }); - if (response.statusCode == 200) { - Map jsonDecoded = jsonDecode(response.body); - Map dataFromResponse = {}; - if (jsonDecoded.containsKey('data')) { - dataFromResponse.addAll(jsonDecoded['data']); - } - if ((jsonDecoded.containsKey('message') && - (jsonDecoded['message'] as String) - .toLowerCase() - .contains('verified')) && - jsonDecoded.containsKey('cramkey')) { - return jsonDecoded['cramkey']; - } else if (jsonDecoded.containsKey('data') && - dataFromResponse.containsKey('newAtsign')) { - return 'follow-up'; - } else if (jsonDecoded.containsKey('message') && - jsonDecoded['message'] == - 'The code you have entered is invalid or expired. Please try again?') { - return 'retry'; - } else if (jsonDecoded.containsKey('message') && - (jsonDecoded['message'] == - 'Oops! You already have the maximum number of free atSigns. Please select one of your existing atSigns.')) { - stdout.writeln( - '[Unable to proceed] This email address already has 10 free atSigns associated with it.\n' - 'To register a new atSign to this email address, please log into the dashboard \'my.atsign.com/login\'.\n' - 'Remove at least 1 atSign from your account and then try again.\n' - 'Alternatively, you can retry this process with a different email address.'); - exit(1); - } else { - throw at_client.AtClientException.message( - '${response.statusCode} ${jsonDecoded['message']}'); - } - } else { - throw at_client.AtClientException.message( - '${response.statusCode} ${response.reasonPhrase}'); - } - } - - /// Accepts a registered [atsign] as a parameter and sends a one-time verification code - /// to the email that the atsign is registered with - /// Throws an exception in the following cases: - /// 1) HTTP 400 BAD_REQUEST - /// 2) Invalid atsign - Future requestAuthenticationOtp(String atsign, - {String authority = RegistrarApiConstants.apiHostProd}) async { - Response response = await postRequest(authority, - RegistrarApiConstants.requestAuthenticationOtpPath, {'atsign': atsign}); - String apiResponseMessage = jsonDecode(response.body)['message']; - if (response.statusCode == 200) { - if (apiResponseMessage.contains('Sent Successfully')) { - stdout.writeln( - '[Information] Successfully sent verification code to your registered e-mail'); - return; - } - throw at_client.InternalServerError( - 'Unable to send verification code for authentication.\nCause: $apiResponseMessage'); - } - throw at_client.InvalidRequestException(apiResponseMessage); - } - - /// Returns the cram key for an atsign by fetching it from the registrar API - /// Accepts a registered [atsign], the verification code that was sent to - /// the registered email - /// Throws exception in the following cases: - /// 1) HTTP 400 BAD_REQUEST - Future getCramKey(String atsign, String verificationCode, - {String authority = RegistrarApiConstants.apiHostProd}) async { - Response response = await postRequest( - authority, - RegistrarApiConstants.getCramKeyWithOtpPath, - {'atsign': atsign, 'otp': verificationCode}); - Map jsonDecodedBody = jsonDecode(response.body); - if (response.statusCode == 200) { - if (jsonDecodedBody['message'] == 'Verified') { - String cram = jsonDecodedBody['cramkey']; - cram = cram.split(':')[1]; - stdout.writeln('[Information] CRAM Key fetched successfully'); - return cram; - } - throw at_client.InvalidDataException( - 'Invalid verification code. Please enter a valid verification code'); - } - throw at_client.InvalidDataException(jsonDecodedBody['message']); - } - - /// calls utility methods from [OnboardingUtil] that - /// 1) send verification code to the registered email - /// 2) fetch the CRAM key from registrar using the verification code - Future getCramUsingOtp(String atsign, String registrarUrl) async { - await requestAuthenticationOtp(atsign, authority: registrarUrl); - return await getCramKey(atsign, getVerificationCodeFromUser(), - authority: registrarUrl); - } - - /// generic GET request - Future getRequest(String authority, String path) async { - if (_ioClient == null) _createClient(); - Uri uri = Uri.https(authority, path); - Response response = await _ioClient!.get(uri, headers: { - 'Authorization': RegistrarApiConstants.authorization, - 'Content-Type': RegistrarApiConstants.contentType, - }); - return response; - } - - /// generic POST request - Future postRequest( - String authority, String path, Map data) async { - if (_ioClient == null) _createClient(); - - Uri uri = Uri.https(authority, path); - - String body = json.encode(data); - if (RegistrarApiConstants.isDebugMode) { - stdout.writeln('Sending request to url: $uri\nRequest Body: $body'); - } - Response response = await _ioClient!.post( - uri, - body: body, - headers: { - 'Authorization': RegistrarApiConstants.authorization, - 'Content-Type': RegistrarApiConstants.contentType, - }, - ); - if (RegistrarApiConstants.isDebugMode) { - print('Got Response: ${response.body}'); - } - return response; - } - - bool validateEmail(String email) { - return RegExp( - r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") - .hasMatch(email); - } - - bool validateVerificationCode(String otp) { - if (otp.length == 4) { - return RegExp(r"^[a-zA-z0-9]").hasMatch(otp); - } - return false; - } - - /// Method to get verification code from user input - /// validates code locally and retries taking user input if invalid - /// Returns only when the user has provided a 4-length String only containing numbers and alphabets - String getVerificationCodeFromUser() { - String? otp; - stdout.writeln( - '[Action Required] Enter your verification code: (verification code is not case-sensitive)'); - otp = stdin.readLineSync()!.toUpperCase(); - while (!validateVerificationCode(otp!)) { - stderr.writeln( - '[Unable to proceed] The verification code you entered is invalid.\n' - 'Please check your email for a 4-character verification code.\n' - 'If you cannot see the code in your inbox, please check your spam/junk/promotions folders.\n' - '[Action Required] Enter your verification code:'); - otp = stdin.readLineSync()!.toUpperCase(); - } - return otp; - } -} diff --git a/packages/at_onboarding_cli/lib/src/util/register_api_result.dart b/packages/at_onboarding_cli/lib/src/util/register_api_result.dart deleted file mode 100644 index c6e170f9..00000000 --- a/packages/at_onboarding_cli/lib/src/util/register_api_result.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'api_call_status.dart'; - -class RegisterApiResult { - dynamic data; - - late ApiCallStatus apiCallStatus; - - String? exceptionMessage; -} diff --git a/packages/at_onboarding_cli/lib/src/util/register_api_task.dart b/packages/at_onboarding_cli/lib/src/util/register_api_task.dart deleted file mode 100644 index d45b0e0d..00000000 --- a/packages/at_onboarding_cli/lib/src/util/register_api_task.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'dart:collection'; - -import 'package:at_onboarding_cli/src/util/onboarding_util.dart'; -import 'package:at_onboarding_cli/src/util/register_api_result.dart'; - -/// Represents a task in an AtSign registration cycle -abstract class RegisterApiTask { - static final maximumRetries = 3; - - int retryCount = 1; - - late Map params; - - late OnboardingUtil registerUtil; - - RegisterApiResult result = RegisterApiResult(); - - ///Initializes the Task object with necessary parameters - ///[params] is a map that contains necessary data to complete atsign - /// registration process - void init(Map params, OnboardingUtil registerUtil) { - this.params = params; - result.data = HashMap(); - this.registerUtil = registerUtil; - } - - ///Implementing classes need to implement required logic in this method to - ///complete their sub-process in the AtSign registration process - Future run(); - - ///In case the task has returned a [RegisterApiResult] with status retry, this method checks and returns if the call can be retried - bool shouldRetry() { - return retryCount < maximumRetries; - } -} diff --git a/packages/at_onboarding_cli/pubspec.yaml b/packages/at_onboarding_cli/pubspec.yaml index 62f8521a..22164af2 100644 --- a/packages/at_onboarding_cli/pubspec.yaml +++ b/packages/at_onboarding_cli/pubspec.yaml @@ -28,6 +28,10 @@ dependencies: at_server_status: ^1.0.4 at_utils: ^3.0.16 +dependency_overrides: + at_register: + path: /home/srie/Desktop/work/at_libraries/packages/at_register + dev_dependencies: lints: ^2.1.0 test: ^1.24.2 diff --git a/packages/at_register/lib/at_register.dart b/packages/at_register/lib/at_register.dart new file mode 100644 index 00000000..86c57e58 --- /dev/null +++ b/packages/at_register/lib/at_register.dart @@ -0,0 +1,10 @@ +/// Support for doing something awesome. +/// +/// More dartdocs go here. +library; + +export 'src/at_register_base.dart'; +export 'src/util/api_call_status.dart'; +export 'src/util/register_result.dart'; +export 'src/util/register_task.dart'; +export 'src/util/registrar_constants.dart'; diff --git a/packages/at_register/lib/src/util/onboarding_util.dart b/packages/at_register/lib/src/at_register_base.dart similarity index 82% rename from packages/at_register/lib/src/util/onboarding_util.dart rename to packages/at_register/lib/src/at_register_base.dart index b47b2ca4..fd3c604d 100644 --- a/packages/at_register/lib/src/util/onboarding_util.dart +++ b/packages/at_register/lib/src/at_register_base.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:at_client/at_client.dart' as at_client; -import 'registrar_api_constants.dart'; +import 'package:at_register/src/util/registrar_constants.dart'; import 'package:http/http.dart'; import 'package:http/io_client.dart'; @@ -21,13 +21,13 @@ class OnboardingUtil { /// Returns a Future> containing free available atSigns of count provided as input. Future> getFreeAtSigns( {int amount = 1, - String authority = RegistrarApiConstants.apiHostProd}) async { + String authority = RegistrarConstants.apiHostProd}) async { List atSigns = []; Response response; for (int i = 0; i < amount; i++) { // get request at my.atsign.com/api/app/v3/get-free-atsign/ response = - await getRequest(authority, RegistrarApiConstants.pathGetFreeAtSign); + await getRequest(authority, RegistrarConstants.pathGetFreeAtSign); if (response.statusCode == 200) { String atSign = jsonDecode(response.body)['data']['atsign']; atSigns.add(atSign); @@ -45,9 +45,9 @@ class OnboardingUtil { /// Sends an OTP to the `email` provided. /// Throws [AtException] if [atSign] is invalid Future registerAtSign(String atSign, String email, - {oldEmail, String authority = RegistrarApiConstants.apiHostProd}) async { + {oldEmail, String authority = RegistrarConstants.apiHostProd}) async { Response response = - await postRequest(authority, RegistrarApiConstants.pathRegisterAtSign, { + await postRequest(authority, RegistrarConstants.pathRegisterAtSign, { 'atsign': atSign, 'email': email, 'oldEmail': oldEmail, @@ -55,7 +55,7 @@ class OnboardingUtil { if (response.statusCode == 200) { Map jsonDecoded = jsonDecode(response.body); bool sentSuccessfully = - jsonDecoded['message'].toLowerCase().contains('success'); + jsonDecoded['message'].toLowerCase().contains('success'); return sentSuccessfully; } else { throw at_client.AtClientException.message( @@ -81,9 +81,9 @@ class OnboardingUtil { /// Throws [AtException] if [atSign] or [otp] is invalid Future validateOtp(String atSign, String email, String otp, {String confirmation = 'true', - String authority = RegistrarApiConstants.apiHostProd}) async { + String authority = RegistrarConstants.apiHostProd}) async { Response response = - await postRequest(authority, RegistrarApiConstants.pathValidateOtp, { + await postRequest(authority, RegistrarConstants.pathValidateOtp, { 'atsign': atSign, 'email': email, 'otp': otp, @@ -96,9 +96,9 @@ class OnboardingUtil { dataFromResponse.addAll(jsonDecoded['data']); } if ((jsonDecoded.containsKey('message') && - (jsonDecoded['message'] as String) - .toLowerCase() - .contains('verified')) && + (jsonDecoded['message'] as String) + .toLowerCase() + .contains('verified')) && jsonDecoded.containsKey('cramkey')) { return jsonDecoded['cramkey']; } else if (jsonDecoded.containsKey('data') && @@ -113,9 +113,9 @@ class OnboardingUtil { 'Oops! You already have the maximum number of free atSigns. Please select one of your existing atSigns.')) { stdout.writeln( '[Unable to proceed] This email address already has 10 free atSigns associated with it.\n' - 'To register a new atSign to this email address, please log into the dashboard \'my.atsign.com/login\'.\n' - 'Remove at least 1 atSign from your account and then try again.\n' - 'Alternatively, you can retry this process with a different email address.'); + 'To register a new atSign to this email address, please log into the dashboard \'my.atsign.com/login\'.\n' + 'Remove at least 1 atSign from your account and then try again.\n' + 'Alternatively, you can retry this process with a different email address.'); exit(1); } else { throw at_client.AtClientException.message( @@ -133,9 +133,9 @@ class OnboardingUtil { /// 1) HTTP 400 BAD_REQUEST /// 2) Invalid atsign Future requestAuthenticationOtp(String atsign, - {String authority = RegistrarApiConstants.apiHostProd}) async { + {String authority = RegistrarConstants.apiHostProd}) async { Response response = await postRequest(authority, - RegistrarApiConstants.requestAuthenticationOtpPath, {'atsign': atsign}); + RegistrarConstants.requestAuthenticationOtpPath, {'atsign': atsign}); String apiResponseMessage = jsonDecode(response.body)['message']; if (response.statusCode == 200) { if (apiResponseMessage.contains('Sent Successfully')) { @@ -155,10 +155,10 @@ class OnboardingUtil { /// Throws exception in the following cases: /// 1) HTTP 400 BAD_REQUEST Future getCramKey(String atsign, String verificationCode, - {String authority = RegistrarApiConstants.apiHostProd}) async { + {String authority = RegistrarConstants.apiHostProd}) async { Response response = await postRequest( authority, - RegistrarApiConstants.getCramKeyWithOtpPath, + RegistrarConstants.getCramKeyWithOtpPath, {'atsign': atsign, 'otp': verificationCode}); Map jsonDecodedBody = jsonDecode(response.body); if (response.statusCode == 200) { @@ -188,8 +188,8 @@ class OnboardingUtil { if (_ioClient == null) _createClient(); Uri uri = Uri.https(authority, path); Response response = await _ioClient!.get(uri, headers: { - 'Authorization': RegistrarApiConstants.authorization, - 'Content-Type': RegistrarApiConstants.contentType, + 'Authorization': RegistrarConstants.authorization, + 'Content-Type': RegistrarConstants.contentType, }); return response; } @@ -202,18 +202,18 @@ class OnboardingUtil { Uri uri = Uri.https(authority, path); String body = json.encode(data); - if (RegistrarApiConstants.isDebugMode) { + if (RegistrarConstants.isDebugMode) { stdout.writeln('Sending request to url: $uri\nRequest Body: $body'); } Response response = await _ioClient!.post( uri, body: body, headers: { - 'Authorization': RegistrarApiConstants.authorization, - 'Content-Type': RegistrarApiConstants.contentType, + 'Authorization': RegistrarConstants.authorization, + 'Content-Type': RegistrarConstants.contentType, }, ); - if (RegistrarApiConstants.isDebugMode) { + if (RegistrarConstants.isDebugMode) { print('Got Response: ${response.body}'); } return response; @@ -221,7 +221,7 @@ class OnboardingUtil { bool validateEmail(String email) { return RegExp( - r"^[a-zA-Z0-9.a-zA-Z0-9!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") + r"^[a-zA-Z0-9.a-zA-Z0-9!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") .hasMatch(email); } @@ -243,9 +243,9 @@ class OnboardingUtil { while (!validateVerificationCode(otp!)) { stderr.writeln( '[Unable to proceed] The verification code you entered is invalid.\n' - 'Please check your email for a 4-character verification code.\n' - 'If you cannot see the code in your inbox, please check your spam/junk/promotions folders.\n' - '[Action Required] Enter your verification code:'); + 'Please check your email for a 4-character verification code.\n' + 'If you cannot see the code in your inbox, please check your spam/junk/promotions folders.\n' + '[Action Required] Enter your verification code:'); otp = stdin.readLineSync()!.toUpperCase(); } return otp; diff --git a/packages/at_register/lib/src/util/register_api_result.dart b/packages/at_register/lib/src/util/register_result.dart similarity index 81% rename from packages/at_register/lib/src/util/register_api_result.dart rename to packages/at_register/lib/src/util/register_result.dart index c6e170f9..e5ea0981 100644 --- a/packages/at_register/lib/src/util/register_api_result.dart +++ b/packages/at_register/lib/src/util/register_result.dart @@ -1,6 +1,6 @@ import 'api_call_status.dart'; -class RegisterApiResult { +class RegisterResult { dynamic data; late ApiCallStatus apiCallStatus; diff --git a/packages/at_register/lib/src/util/register_api_task.dart b/packages/at_register/lib/src/util/register_task.dart similarity index 67% rename from packages/at_register/lib/src/util/register_api_task.dart rename to packages/at_register/lib/src/util/register_task.dart index 8643adc8..ec279b43 100644 --- a/packages/at_register/lib/src/util/register_api_task.dart +++ b/packages/at_register/lib/src/util/register_task.dart @@ -1,10 +1,10 @@ import 'dart:collection'; -import 'package:at_register/src/util/onboarding_util.dart'; -import 'package:at_register/src/util/register_api_result.dart'; +import 'package:at_register/at_register.dart'; +import 'package:at_register/src/util/register_result.dart'; /// Represents a task in an AtSign registration cycle -abstract class RegisterApiTask { +abstract class RegisterTask { static final maximumRetries = 3; int retryCount = 1; @@ -13,7 +13,7 @@ abstract class RegisterApiTask { late OnboardingUtil registerUtil; - RegisterApiResult result = RegisterApiResult(); + RegisterResult result = RegisterResult(); ///Initializes the Task object with necessary parameters ///[params] is a map that contains necessary data to complete atsign @@ -26,9 +26,9 @@ abstract class RegisterApiTask { ///Implementing classes need to implement required logic in this method to ///complete their sub-process in the AtSign registration process - Future run(); + Future run(); - ///In case the task has returned a [RegisterApiResult] with status retry, this method checks and returns if the call can be retried + ///In case the task has returned a [RegisterResult] with status retry, this method checks and returns if the call can be retried bool shouldRetry() { return retryCount < maximumRetries; } diff --git a/packages/at_register/lib/src/util/registrar_api_constants.dart b/packages/at_register/lib/src/util/registrar_api_constants.dart deleted file mode 100644 index f6cb3ba2..00000000 --- a/packages/at_register/lib/src/util/registrar_api_constants.dart +++ /dev/null @@ -1,22 +0,0 @@ -class RegistrarApiConstants { - /// Authorities - static const String apiHostProd = 'my.atsign.com'; - static const String apiHostStaging = 'my.atsign.wtf'; - - /// API Paths - static const String pathGetFreeAtSign = '/api/app/v3/get-free-atsign'; - static const String pathRegisterAtSign = '/api/app/v3/register-person'; - static const String pathValidateOtp = '/api/app/v3/validate-person'; - static const String requestAuthenticationOtpPath = - '/api/app/v3/authenticate/atsign'; - static const String getCramKeyWithOtpPath = - '/api/app/v3/authenticate/atsign/activate'; - - /// API headers - static const String contentType = 'application/json'; - static const String authorization = '477b-876u-bcez-c42z-6a3d'; - - /// DebugMode: setting it to true will print more logs to aid understanding - /// the inner working of Register_cli - static const bool isDebugMode = false; -} diff --git a/packages/at_onboarding_cli/lib/src/util/registrar_api_constants.dart b/packages/at_register/lib/src/util/registrar_constants.dart similarity index 96% rename from packages/at_onboarding_cli/lib/src/util/registrar_api_constants.dart rename to packages/at_register/lib/src/util/registrar_constants.dart index f6cb3ba2..e2897c1b 100644 --- a/packages/at_onboarding_cli/lib/src/util/registrar_api_constants.dart +++ b/packages/at_register/lib/src/util/registrar_constants.dart @@ -1,4 +1,4 @@ -class RegistrarApiConstants { +class RegistrarConstants { /// Authorities static const String apiHostProd = 'my.atsign.com'; static const String apiHostStaging = 'my.atsign.wtf'; From f8c61983c5a68838fa96c9c1bc3a47a4c7656f64 Mon Sep 17 00:00:00 2001 From: Srie Teja Date: Tue, 6 Feb 2024 19:19:38 +0530 Subject: [PATCH 05/16] feat: major redesign --- .../at_onboarding_cli/bin/register_cli.dart | 2 +- .../example/get_cram_key.dart | 6 +- .../onboard/at_onboarding_service_impl.dart | 2 +- .../lib/src/register_cli/register.dart | 176 ++---------- packages/at_register/lib/at_register.dart | 17 +- .../src/api-interactions/get_free_atsign.dart | 27 ++ .../src/api-interactions/register_atsign.dart | 29 ++ .../api-interactions/registrar_api_calls.dart | 197 ++++++++++++++ .../api-interactions/registration_flow.dart | 43 +++ .../src/api-interactions/validate_otp.dart | 80 ++++++ .../at_register/lib/src/at_register_base.dart | 253 ------------------ .../{util => config}/registrar_constants.dart | 8 +- .../lib/src/util/api_call_status.dart | 2 + .../at_register/lib/src/util/api_util.dart | 87 ++++++ .../lib/src/util/at_register_exception.dart | 20 ++ .../lib/src/util/register_params.dart | 41 +++ .../lib/src/util/register_task.dart | 40 +-- ..._result.dart => register_task_result.dart} | 4 +- .../src/util/validate_otp_task_result.dart | 5 + 19 files changed, 598 insertions(+), 441 deletions(-) create mode 100644 packages/at_register/lib/src/api-interactions/get_free_atsign.dart create mode 100644 packages/at_register/lib/src/api-interactions/register_atsign.dart create mode 100644 packages/at_register/lib/src/api-interactions/registrar_api_calls.dart create mode 100644 packages/at_register/lib/src/api-interactions/registration_flow.dart create mode 100644 packages/at_register/lib/src/api-interactions/validate_otp.dart delete mode 100644 packages/at_register/lib/src/at_register_base.dart rename packages/at_register/lib/src/{util => config}/registrar_constants.dart (78%) create mode 100644 packages/at_register/lib/src/util/api_util.dart create mode 100644 packages/at_register/lib/src/util/at_register_exception.dart create mode 100644 packages/at_register/lib/src/util/register_params.dart rename packages/at_register/lib/src/util/{register_result.dart => register_task_result.dart} (80%) create mode 100644 packages/at_register/lib/src/util/validate_otp_task_result.dart diff --git a/packages/at_onboarding_cli/bin/register_cli.dart b/packages/at_onboarding_cli/bin/register_cli.dart index a6cca120..237a5517 100644 --- a/packages/at_onboarding_cli/bin/register_cli.dart +++ b/packages/at_onboarding_cli/bin/register_cli.dart @@ -3,4 +3,4 @@ import 'package:at_onboarding_cli/src/register_cli/register.dart' Future main(List args) async { await register_cli.main(args); -} +} \ No newline at end of file diff --git a/packages/at_onboarding_cli/example/get_cram_key.dart b/packages/at_onboarding_cli/example/get_cram_key.dart index 9d2842e7..c3cbfb80 100644 --- a/packages/at_onboarding_cli/example/get_cram_key.dart +++ b/packages/at_onboarding_cli/example/get_cram_key.dart @@ -7,12 +7,12 @@ Future main(args) async { final argResults = CustomArgParser(getArgParser()).parse(args); // this step sends an OTP to the registered email - await OnboardingUtil().requestAuthenticationOtp( + await RegisterApiCall().requestAuthenticationOtp( argResults['atsign']); // requires a registered atsign // the following step validates the email that was sent in the above step - String? verificationCode = OnboardingUtil().getVerificationCodeFromUser(); - String cramKey = await OnboardingUtil().getCramKey(argResults['atsign'], + String? verificationCode = ApiUtil.getVerificationCodeFromUser(); + String cramKey = await RegisterApiCall().getCramKey(argResults['atsign'], verificationCode); // verification code received on the registered email print('Your cram key is: $cramKey'); diff --git a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart index 07703681..a5c55cfa 100644 --- a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart +++ b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart @@ -97,7 +97,7 @@ class AtOnboardingServiceImpl implements AtOnboardingService { // get cram_secret from either from AtOnboardingPreference // or fetch from the registrar using verification code sent to email - atOnboardingPreference.cramSecret ??= await OnboardingUtil() + atOnboardingPreference.cramSecret ??= await RegistrarApiCalls() .getCramUsingOtp(_atSign, atOnboardingPreference.registrarUrl); if (atOnboardingPreference.cramSecret == null) { logger.info('Root Server address is ${atOnboardingPreference.rootDomain}:' diff --git a/packages/at_onboarding_cli/lib/src/register_cli/register.dart b/packages/at_onboarding_cli/lib/src/register_cli/register.dart index f36a4a6b..4a16189f 100644 --- a/packages/at_onboarding_cli/lib/src/register_cli/register.dart +++ b/packages/at_onboarding_cli/lib/src/register_cli/register.dart @@ -1,4 +1,3 @@ -import 'dart:collection'; import 'dart:io'; import 'package:args/args.dart'; @@ -14,8 +13,8 @@ import 'package:at_register/at_register.dart'; ///Requires List args containing the following arguments: email class Register { Future main(List args) async { - Map params = HashMap(); - OnboardingUtil registerUtil = OnboardingUtil(); + RegisterParams registerParams = RegisterParams(); + RegistrarApiCalls registerUtil = RegistrarApiCalls(); final argParser = ArgParser() ..addOption('email', @@ -39,170 +38,24 @@ class Register { exit(6); } - if (registerUtil.validateEmail(argResults['email'])) { - params['email'] = argResults['email']; + if (ApiUtil.validateEmail(argResults['email'])) { + registerParams.email = argResults['email']; } else { stderr.writeln( '[Unable to run Register CLI] You have entered an invalid email address. Check your email address and try again.'); exit(7); } - //set the following parameter to RegisterApiConstants.apiHostStaging - //to use the staging environment - params['authority'] = RegistrarConstants.apiHostProd; - - //create stream of tasks each of type [RegisterTask] and then - // call start on the stream of tasks - await RegistrationFlow(params, registerUtil) + // create a queue of tasks each of type [RegisterTask] and then + // call start on the RegistrationFlow object + await RegistrationFlow(registerParams, registerUtil) .add(GetFreeAtsign()) .add(RegisterAtsign()) .add(ValidateOtp()) .start(); - activate_cli.main(['-a', params['atsign']!, '-c', params['cramkey']!]); - } -} - -///class that handles multiple tasks of type [RegisterTask] -///Initialized with a params map that needs to be populated with - email and api host address -///[add] method can be used to add tasks[RegisterTask] to the [processFlow] -///[start] needs to be called after all required tasks are added to the [processFlow] -class RegistrationFlow { - List processFlow = []; - RegisterResult result = RegisterResult(); - late OnboardingUtil registerUtil; - Map params; - - RegistrationFlow(this.params, this.registerUtil); - - RegistrationFlow add(RegisterTask task) { - processFlow.add(task); - return this; - } - - Future start() async { - for (RegisterTask task in processFlow) { - task.init(params, registerUtil); - if (RegistrarConstants.isDebugMode) { - print('Current Task: $task [params=$params]\n'); - } - result = await task.run(); - if (result.apiCallStatus == ApiCallStatus.retry) { - while ( - task.shouldRetry() && result.apiCallStatus == ApiCallStatus.retry) { - result = await task.run(); - task.retryCount++; - } - } - if (result.apiCallStatus == ApiCallStatus.success) { - params.addAll(result.data); - } else { - throw AtOnboardingException(result.exceptionMessage); - } - } - } -} - -///This is a [RegisterTask] that fetches a free atsign -///throws [AtException] with concerned message which was encountered in the -///HTTP GET/POST request -class GetFreeAtsign extends RegisterTask { - @override - Future run() async { - stdout - .writeln('[Information] Getting your randomly generated free atSign…'); - try { - List atsignList = - await registerUtil.getFreeAtSigns(authority: params['authority']!); - result.data['atsign'] = atsignList[0]; - stdout.writeln('[Information] Your new atSign is **@${atsignList[0]}**'); - result.apiCallStatus = ApiCallStatus.success; - } on Exception catch (e) { - result.exceptionMessage = e.toString(); - result.apiCallStatus = - shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; - } - - return result; - } -} - -///This is a [RegisterTask] that registers a free atsign fetched in -///[GetFreeAtsign] to the email provided as args -///throws [AtException] with concerned message which was encountered in the -///HTTP GET/POST request -class RegisterAtsign extends RegisterTask { - @override - Future run() async { - stdout.writeln( - '[Information] Sending verification code to: ${params['email']}'); - try { - result.data['otpSent'] = (await registerUtil.registerAtSign( - params['atsign']!, params['email']!, - authority: params['authority']!)) - .toString(); - stdout.writeln( - '[Information] Verification code sent to: ${params['email']}'); - result.apiCallStatus = ApiCallStatus.success; - } on Exception catch (e) { - result.exceptionMessage = e.toString(); - result.apiCallStatus = - shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; - } - return result; - } -} - -///This is a [RegisterTask] that validates the otp which was sent as a part -///of [RegisterAtsign] to email provided in args -///throws [AtException] with concerned message which was encountered in the -///HTTP GET/POST request -class ValidateOtp extends RegisterTask { - @override - void init(Map params, OnboardingUtil registerUtil) { - params['confirmation'] = 'false'; - this.params = params; - this.registerUtil = registerUtil; - result.data = HashMap(); - } - - @override - Future run() async { - if (params['otp'] == null) { - params['otp'] = registerUtil.getVerificationCodeFromUser(); - } - stdout.writeln('[Information] Validating your verification code...'); - try { - String apiResponse = await registerUtil.validateOtp( - params['atsign']!, params['email']!, params['otp']!, - confirmation: params['confirmation']!, - authority: params['authority']!); - if (apiResponse == 'retry') { - stderr.writeln( - '[Unable to proceed] The verification code you entered is either invalid or expired.\n' - ' Check your verification code and try again.'); - params['otp'] = registerUtil.getVerificationCodeFromUser(); - result.apiCallStatus = ApiCallStatus.retry; - result.exceptionMessage = - 'Incorrect otp entered 3 times. Max retries reached.'; - } else if (apiResponse == 'follow-up') { - params.update('confirmation', (value) => 'true'); - result.data['otp'] = params['otp']; - result.apiCallStatus = ApiCallStatus.retry; - } else if (apiResponse.startsWith("@")) { - result.data['cramkey'] = apiResponse.split(":")[1]; - stdout.writeln( - '[Information] Your cram secret: ${result.data['cramkey']}'); - stdout.writeln('[Success] Your atSign **@${params['atsign']}** has been' - ' successfully registered to ${params['email']}'); - result.apiCallStatus = ApiCallStatus.success; - } - } on Exception catch (e) { - result.exceptionMessage = e.toString(); - result.apiCallStatus = - shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; - } - return result; + activate_cli + .main(['-a', registerParams.atsign!, '-c', registerParams.cram!]); } } @@ -211,6 +64,13 @@ Future main(List args) async { AtSignLogger.root_level = 'severe'; try { await register.main(args); + } on MaximumAtsignQuotaException { + stdout.writeln( + '[Unable to proceed] This email address already has 10 free atSigns associated with it.\n' + 'To register a new atSign to this email address, please log into the dashboard \'my.atsign.com/login\'.\n' + 'Remove at least 1 atSign from your account and then try again.\n' + 'Alternatively, you can retry this process with a different email address.'); + exit(0); } on FormatException catch (e) { if (e.toString().contains('Missing argument')) { stderr.writeln( @@ -232,11 +92,11 @@ Future main(List args) async { stderr.writeln('Cause: $e'); exit(3); } - } on AtOnboardingException catch (e) { + } on AtException catch (e) { stderr.writeln( '[Error] Failed getting an atsign. It looks like something went wrong on our side.\n' 'Please try again or contact support@atsign.com, quoting the text displayed below.'); - stderr.writeln('Cause: $e ExceptionType:${e.runtimeType}'); + stderr.writeln('Cause: ${e.message} ExceptionType:${e.runtimeType}'); exit(4); } on Exception catch (e) { if (e diff --git a/packages/at_register/lib/at_register.dart b/packages/at_register/lib/at_register.dart index 86c57e58..5fe930f4 100644 --- a/packages/at_register/lib/at_register.dart +++ b/packages/at_register/lib/at_register.dart @@ -1,10 +1,15 @@ -/// Support for doing something awesome. -/// -/// More dartdocs go here. library; -export 'src/at_register_base.dart'; +export 'src/api-interactions/registrar_api_calls.dart'; +export 'src/api-interactions/registration_flow.dart'; +export 'src/api-interactions/get_free_atsign.dart'; +export 'src/api-interactions/register_atsign.dart'; +export 'src/api-interactions/validate_otp.dart'; export 'src/util/api_call_status.dart'; -export 'src/util/register_result.dart'; +export 'src/util/register_task_result.dart'; +export 'src/util/validate_otp_task_result.dart'; export 'src/util/register_task.dart'; -export 'src/util/registrar_constants.dart'; +export 'src/config/registrar_constants.dart'; +export 'src/util/register_params.dart'; +export 'src/util/api_util.dart'; +export 'src/util/at_register_exception.dart'; diff --git a/packages/at_register/lib/src/api-interactions/get_free_atsign.dart b/packages/at_register/lib/src/api-interactions/get_free_atsign.dart new file mode 100644 index 00000000..b27c5517 --- /dev/null +++ b/packages/at_register/lib/src/api-interactions/get_free_atsign.dart @@ -0,0 +1,27 @@ +import 'dart:io'; + +import '../../at_register.dart'; + +///This is a [RegisterTask] that fetches a free atsign +///throws [AtException] with concerned message which was encountered in the +///HTTP GET/POST request +class GetFreeAtsign extends RegisterTask { + @override + Future run() async { + stdout + .writeln('[Information] Getting your randomly generated free atSign…'); + try { + List atsignList = await registrarApiCalls.getFreeAtSigns( + count: 8, authority: RegistrarConstants.authority); + result.data['atsign'] = atsignList[0]; + stdout.writeln('[Information] Your new atSign is **@${atsignList[0]}**'); + result.apiCallStatus = ApiCallStatus.success; + } on Exception catch (e) { + result.exceptionMessage = e.toString(); + result.apiCallStatus = + shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; + } + + return result; + } +} diff --git a/packages/at_register/lib/src/api-interactions/register_atsign.dart b/packages/at_register/lib/src/api-interactions/register_atsign.dart new file mode 100644 index 00000000..a1d609b7 --- /dev/null +++ b/packages/at_register/lib/src/api-interactions/register_atsign.dart @@ -0,0 +1,29 @@ +import 'dart:io'; + +import '../../at_register.dart'; + +///This is a [RegisterTask] that registers a free atsign fetched in +///[GetFreeAtsign] to the email provided as args +///throws [AtException] with concerned message which was encountered in the +///HTTP GET/POST request +class RegisterAtsign extends RegisterTask { + @override + Future run() async { + stdout.writeln( + '[Information] Sending verification code to: ${registerParams.email}'); + try { + result.data['otpSent'] = (await registrarApiCalls.registerAtSign( + registerParams.atsign!, registerParams.email!, + authority: RegistrarConstants.authority)) + .toString(); + stdout.writeln( + '[Information] Verification code sent to: ${registerParams.email}'); + result.apiCallStatus = ApiCallStatus.success; + } on Exception catch (e) { + result.exceptionMessage = e.toString(); + result.apiCallStatus = + shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; + } + return result; + } +} diff --git a/packages/at_register/lib/src/api-interactions/registrar_api_calls.dart b/packages/at_register/lib/src/api-interactions/registrar_api_calls.dart new file mode 100644 index 00000000..a70c31de --- /dev/null +++ b/packages/at_register/lib/src/api-interactions/registrar_api_calls.dart @@ -0,0 +1,197 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:at_commons/at_commons.dart'; +import 'package:http/http.dart' as http; + +import '../../at_register.dart'; + +///class containing utilities to perform registration of a free atsign +class RegistrarApiCalls { + /// Returns a Future> containing free available atSigns of count provided as input. + Future> getFreeAtSigns( + {int count = 1, + String authority = RegistrarConstants.apiHostProd}) async { + List atSigns = []; + + /// ToDo: discuss - what happens when getRequest does not generate a response + http.Response response; + for (int i = 0; i < count; i++) { + // get request at my.atsign.com/api/app/v3/get-free-atsign/ + response = await ApiUtil.getRequest( + authority, RegistrarConstants.pathGetFreeAtSign); + if (response.statusCode == 200) { + String atSign = jsonDecode(response.body)['data']['atsign']; + atSigns.add(atSign); + } else { + throw AtRegisterException( + '${response.statusCode} ${response.reasonPhrase}'); + } + } + return atSigns; + } + + /// Registers the [atSign] provided in the input to the provided [email] + /// + /// The `atSign` provided should be an unregistered and free atsign + /// + /// Returns true if the request to send the OTP was successful. + /// + /// Sends an OTP to the `email` provided. + /// + /// Throws [AtRegisterException] if [atSign] is invalid + Future registerAtSign(String atSign, String email, + {oldEmail, String authority = RegistrarConstants.apiHostProd}) async { + http.Response response = await ApiUtil.postRequest( + authority, RegistrarConstants.pathRegisterAtSign, { + 'atsign': atSign, + 'email': email, + 'oldEmail': oldEmail, + }); + if (response.statusCode == 200) { + Map jsonDecoded = jsonDecode(response.body); + bool sentSuccessfully = + jsonDecoded['message'].toLowerCase().contains('success'); + return sentSuccessfully; + } else { + throw AtRegisterException( + '${response.statusCode} ${response.reasonPhrase}'); + } + } + + /// Registers the [atSign] provided in the input to the provided [email] + /// The `atSign` provided should be an unregistered and free atsign + /// Validates the OTP against the atsign and registers it to the provided email if OTP is valid. + /// Returns the CRAM secret of the atsign which is registered. + /// + /// [confirmation] - Mandatory parameter for validateOTP API call. First request to be sent with confirmation as false, in this + /// case API will return cram key if the user is new otherwise will return list of already existing atsigns. + /// If the user already has existing atsigns user will have to select a listed atsign old/new and place a second call + /// to the same API endpoint with confirmation set to true with previously received OTP. The second follow-up call + /// is automated by this client using new atsign for user simplicity + /// + /// Returns: + /// + /// Case 1("verified") - the API has registered the atsign to provided email and CRAM key present in HTTP_RESPONSE Body. + /// + /// Case 2("follow-up"): User already has existing atsigns and new atsign registered successfully. To receive the CRAM key, follow-up by calling + /// the API with one of the existing listed atsigns, with confirmation set to true. + /// + /// Case 3("retry"): Incorrect OTP send request again with correct OTP. + /// + /// Throws [AtException] if [atSign] or [otp] is invalid + Future validateOtp(String atSign, String email, String otp, + {bool confirmation = true, + String authority = RegistrarConstants.apiHostProd}) async { + http.Response response = await ApiUtil.postRequest( + authority, RegistrarConstants.pathValidateOtp, { + 'atsign': atSign, + 'email': email, + 'otp': otp, + 'confirmation': confirmation.toString(), + }); + + ValidateOtpResult validateOtpResult = ValidateOtpResult(); + Map jsonDecodedResponse; + if (response.statusCode == 200) { + validateOtpResult.data = {}; + jsonDecodedResponse = jsonDecode(response.body); + if (jsonDecodedResponse.containsKey('data')) { + validateOtpResult.data.addAll(jsonDecodedResponse['data']); + } + _processApiResponse(jsonDecodedResponse, validateOtpResult); + } else { + throw AtRegisterException( + '${response.statusCode} ${response.reasonPhrase}'); + } + return validateOtpResult; + } + + /// processes API response for ValidateOtp call + void _processApiResponse(jsonDecodedResponse, result) { + if ((jsonDecodedResponse.containsKey('message') && + (jsonDecodedResponse['message'].toString().toLowerCase()) == + 'verified') && + jsonDecodedResponse.containsKey('cramkey')) { + result.taskStatus = ValidateOtpStatus.verified; + result.data['cramKey'] = jsonDecodedResponse['cramkey']; + } else if (jsonDecodedResponse.containsKey('data') && + result.data.containsKey('newAtsign')) { + result.taskStatus = ValidateOtpStatus.followUp; + } else if (jsonDecodedResponse.containsKey('message') && + + /// ToDo: discuss - compare entire message explicitly or keywords like (expired/invalid) + jsonDecodedResponse['message'] == + 'The code you have entered is invalid or expired. Please try again?') { + result.taskStatus = ValidateOtpStatus.retry; + result.exceptionMessage = jsonDecodedResponse['message']; + } else if (jsonDecodedResponse.containsKey('message') && + (jsonDecodedResponse['message'] == + 'Oops! You already have the maximum number of free atSigns. Please select one of your existing atSigns.')) { + throw MaximumAtsignQuotaException( + 'Maximum free atsign limit reached for current email'); + } else { + throw AtRegisterException('${jsonDecodedResponse['message']}'); + } + } + + /// Accepts a registered [atsign] as a parameter and sends a one-time verification code + /// to the email that the atsign is registered with + /// Throws an exception in the following cases: + /// 1) HTTP 400 BAD_REQUEST + /// 2) Invalid atsign + Future requestAuthenticationOtp(String atsign, + {String authority = RegistrarConstants.apiHostProd}) async { + http.Response response = await ApiUtil.postRequest(authority, + RegistrarConstants.requestAuthenticationOtpPath, {'atsign': atsign}); + String apiResponseMessage = jsonDecode(response.body)['message']; + if (response.statusCode == 200) { + if (apiResponseMessage.contains('Sent Successfully')) { + stdout.writeln( + '[Information] Successfully sent verification code to your registered e-mail'); + return; + } + throw AtRegisterException( + 'Unable to send verification code for authentication.\nCause: $apiResponseMessage'); + } + throw AtRegisterException(apiResponseMessage); + } + + /// Returns the cram key for an atsign by fetching it from the registrar API + /// + /// Accepts a registered [atsign], the verification code that was sent to + /// the registered email + /// + /// Throws exception in the following cases: 1) HTTP 400 BAD_REQUEST + Future getCramKey(String atsign, String verificationCode, + {String authority = RegistrarConstants.apiHostProd}) async { + http.Response response = await ApiUtil.postRequest( + authority, + RegistrarConstants.getCramKeyWithOtpPath, + {'atsign': atsign, 'otp': verificationCode}); + Map jsonDecodedBody = jsonDecode(response.body); + if (response.statusCode == 200) { + if (jsonDecodedBody['message'] == 'Verified') { + String cram = jsonDecodedBody['cramkey']; + cram = cram.split(':')[1]; + stdout.writeln('[Information] CRAM Key fetched successfully'); + return cram; + } + throw InvalidDataException( + 'Invalid verification code. Please enter a valid verification code'); + } + throw InvalidDataException(jsonDecodedBody['message']); + } + + /// calls utility methods from [RegistrarApiCalls] that + /// + /// 1) send verification code to the registered email + /// + /// 2) fetch the CRAM key from registrar using the verification code + Future getCramUsingOtp(String atsign, String registrarUrl) async { + await requestAuthenticationOtp(atsign, authority: registrarUrl); + return await getCramKey(atsign, ApiUtil.getVerificationCodeFromUser(), + authority: registrarUrl); + } +} diff --git a/packages/at_register/lib/src/api-interactions/registration_flow.dart b/packages/at_register/lib/src/api-interactions/registration_flow.dart new file mode 100644 index 00000000..5943895b --- /dev/null +++ b/packages/at_register/lib/src/api-interactions/registration_flow.dart @@ -0,0 +1,43 @@ +import 'dart:io'; + +import '../../at_register.dart'; + +/// class that handles multiple tasks of type [RegisterTask] +/// Initialized with a params map that needs to be populated with - email and api host address +/// [add] method can be used to add tasks[RegisterTask] to the [processQueue] +/// [start] needs to be called after all required tasks are added to the [processQueue] +class RegistrationFlow { + List processQueue = []; + RegisterTaskResult result = RegisterTaskResult(); + late RegistrarApiCalls registrarApiCall; + RegisterParams params; + + RegistrationFlow(this.params, this.registrarApiCall); + + RegistrationFlow add(RegisterTask task) { + processQueue.add(task); + return this; + } + + Future start() async { + for (RegisterTask task in processQueue) { + task.init(params, registrarApiCall); + if (RegistrarConstants.isDebugMode) { + stdout.writeln('\nCurrent Task: $task | Attempt: ${task.retryCount} [params=$params]'); + } + result = await task.run(); + if (result.apiCallStatus == ApiCallStatus.retry) { + while ( + task.shouldRetry() && result.apiCallStatus == ApiCallStatus.retry) { + result = await task.run(); + task.increaseRetryCount(); + } + } + if (result.apiCallStatus == ApiCallStatus.success) { + params.addFromJson(result.data); + } else { + throw AtRegisterException(result.exceptionMessage!); + } + } + } +} diff --git a/packages/at_register/lib/src/api-interactions/validate_otp.dart b/packages/at_register/lib/src/api-interactions/validate_otp.dart new file mode 100644 index 00000000..0803f492 --- /dev/null +++ b/packages/at_register/lib/src/api-interactions/validate_otp.dart @@ -0,0 +1,80 @@ +import 'dart:collection'; +import 'dart:io'; + +import 'package:at_commons/at_commons.dart'; +import 'package:at_utils/at_utils.dart'; + +import '../../at_register.dart'; + +///This is a [RegisterTask] that validates the otp which was sent as a part +///of [RegisterAtsign] to email provided in args +///throws [AtException] with concerned message which was encountered in the +///HTTP GET/POST request +class ValidateOtp extends RegisterTask { + @override + void init( + RegisterParams registerParams, RegistrarApiCalls registrarApiCalls) { + this.registerParams = registerParams; + this.registrarApiCalls = registrarApiCalls; + this.registerParams.confirmation = false; + result.data = HashMap(); + } + + @override + Future run() async { + registerParams.otp ??= ApiUtil.getVerificationCodeFromUser(); + stdout.writeln('[Information] Validating your verification code...'); + try { + registerParams.atsign = AtUtils.fixAtSign(registerParams.atsign!); + ValidateOtpResult validateOtpApiResult = + await registrarApiCalls.validateOtp(registerParams.atsign!, + registerParams.email!, registerParams.otp!, + confirmation: registerParams.confirmation, + authority: RegistrarConstants.authority); + if (validateOtpApiResult.taskStatus == ValidateOtpStatus.retry) { + /// ToDo: move this log to onboarding cli + stderr + .writeln('[Unable to proceed] Invalid or expired verification code.' + ' Check your verification code and try again.'); + registerParams.otp = ApiUtil.getVerificationCodeFromUser(); + result.apiCallStatus = + shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; + result.exceptionMessage = + 'Incorrect otp entered 3 times. Max retries reached.'; + } else if (validateOtpApiResult.taskStatus == + ValidateOtpStatus.followUp) { + registerParams.confirmation = true; + result.data['otp'] = registerParams.otp; + result.apiCallStatus = ApiCallStatus.retry; + } else if (validateOtpApiResult.taskStatus == + ValidateOtpStatus.verified) { + result.data[RegistrarConstants.cramKey] = + validateOtpApiResult.data[RegistrarConstants.cramKey].split(":")[1]; + + /// ToDo: move this log to onboarding cli + stdout.writeln( + '[Information] Your cram secret: ${result.data['cramkey']}'); + stdout.writeln( + '[Success] Your atSign **@${registerParams.atsign}** has been' + ' successfully registered to ${registerParams.email}'); + result.apiCallStatus = ApiCallStatus.success; + } else if (validateOtpApiResult.taskStatus == ValidateOtpStatus.failure) { + result.apiCallStatus = ApiCallStatus.failure; + result.exceptionMessage = validateOtpApiResult.exceptionMessage; + } + } on MaximumAtsignQuotaException { + rethrow; + } on ExhaustedVerificationCodeRetriesException { + rethrow; + } on AtException catch (e) { + result.exceptionMessage = e.message; + result.apiCallStatus = + shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; + } on Exception catch (e) { + result.exceptionMessage = e.toString(); + result.apiCallStatus = + shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; + } + return result; + } +} diff --git a/packages/at_register/lib/src/at_register_base.dart b/packages/at_register/lib/src/at_register_base.dart deleted file mode 100644 index fd3c604d..00000000 --- a/packages/at_register/lib/src/at_register_base.dart +++ /dev/null @@ -1,253 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:at_client/at_client.dart' as at_client; -import 'package:at_register/src/util/registrar_constants.dart'; -import 'package:http/http.dart'; -import 'package:http/io_client.dart'; - -///class containing utilities to perform registration of a free atsign -class OnboardingUtil { - IOClient? _ioClient; - - void _createClient() { - HttpClient ioc = HttpClient(); - ioc.badCertificateCallback = - (X509Certificate cert, String host, int port) => true; - _ioClient = IOClient(ioc); - } - - /// Returns a Future> containing free available atSigns of count provided as input. - Future> getFreeAtSigns( - {int amount = 1, - String authority = RegistrarConstants.apiHostProd}) async { - List atSigns = []; - Response response; - for (int i = 0; i < amount; i++) { - // get request at my.atsign.com/api/app/v3/get-free-atsign/ - response = - await getRequest(authority, RegistrarConstants.pathGetFreeAtSign); - if (response.statusCode == 200) { - String atSign = jsonDecode(response.body)['data']['atsign']; - atSigns.add(atSign); - } else { - throw at_client.AtClientException.message( - '${response.statusCode} ${response.reasonPhrase}'); - } - } - return atSigns; - } - - /// Registers the [atSign] provided in the input to the provided [email] - /// The `atSign` provided should be an unregistered and free atsign - /// Returns true if the request to send the OTP was successful. - /// Sends an OTP to the `email` provided. - /// Throws [AtException] if [atSign] is invalid - Future registerAtSign(String atSign, String email, - {oldEmail, String authority = RegistrarConstants.apiHostProd}) async { - Response response = - await postRequest(authority, RegistrarConstants.pathRegisterAtSign, { - 'atsign': atSign, - 'email': email, - 'oldEmail': oldEmail, - }); - if (response.statusCode == 200) { - Map jsonDecoded = jsonDecode(response.body); - bool sentSuccessfully = - jsonDecoded['message'].toLowerCase().contains('success'); - return sentSuccessfully; - } else { - throw at_client.AtClientException.message( - '${response.statusCode} ${response.reasonPhrase}'); - } - } - - /// Registers the [atSign] provided in the input to the provided [email] - /// The `atSign` provided should be an unregistered and free atsign - /// Validates the OTP against the atsign and registers it to the provided email if OTP is valid. - /// Returns the CRAM secret of the atsign which is registered. - /// - /// [confirmation] - Mandatory parameter for validateOTP API call. First request to be sent with confirmation as false, in this - /// case API will return cram key if the user is new otherwise will return list of already existing atsigns. - /// If the user already has existing atsigns user will have to select a listed atsign old/new and place a second call - /// to the same API endpoint with confirmation set to true with previously received OTP. The second follow-up call - /// is automated by this client using new atsign for user simplicity - /// - ///return value - Case 1("verified") - the API has registered the atsign to provided email and CRAM key present in HTTP_RESPONSE Body. - /// Case 2("follow-up"): User already has existing atsigns and new atsign registered successfully. To receive the CRAM key, follow-up by calling - /// the API with one of the existing listed atsigns, with confirmation set to true. - /// Case 3("retry"): Incorrect OTP send request again with correct OTP. - /// Throws [AtException] if [atSign] or [otp] is invalid - Future validateOtp(String atSign, String email, String otp, - {String confirmation = 'true', - String authority = RegistrarConstants.apiHostProd}) async { - Response response = - await postRequest(authority, RegistrarConstants.pathValidateOtp, { - 'atsign': atSign, - 'email': email, - 'otp': otp, - 'confirmation': confirmation, - }); - if (response.statusCode == 200) { - Map jsonDecoded = jsonDecode(response.body); - Map dataFromResponse = {}; - if (jsonDecoded.containsKey('data')) { - dataFromResponse.addAll(jsonDecoded['data']); - } - if ((jsonDecoded.containsKey('message') && - (jsonDecoded['message'] as String) - .toLowerCase() - .contains('verified')) && - jsonDecoded.containsKey('cramkey')) { - return jsonDecoded['cramkey']; - } else if (jsonDecoded.containsKey('data') && - dataFromResponse.containsKey('newAtsign')) { - return 'follow-up'; - } else if (jsonDecoded.containsKey('message') && - jsonDecoded['message'] == - 'The code you have entered is invalid or expired. Please try again?') { - return 'retry'; - } else if (jsonDecoded.containsKey('message') && - (jsonDecoded['message'] == - 'Oops! You already have the maximum number of free atSigns. Please select one of your existing atSigns.')) { - stdout.writeln( - '[Unable to proceed] This email address already has 10 free atSigns associated with it.\n' - 'To register a new atSign to this email address, please log into the dashboard \'my.atsign.com/login\'.\n' - 'Remove at least 1 atSign from your account and then try again.\n' - 'Alternatively, you can retry this process with a different email address.'); - exit(1); - } else { - throw at_client.AtClientException.message( - '${response.statusCode} ${jsonDecoded['message']}'); - } - } else { - throw at_client.AtClientException.message( - '${response.statusCode} ${response.reasonPhrase}'); - } - } - - /// Accepts a registered [atsign] as a parameter and sends a one-time verification code - /// to the email that the atsign is registered with - /// Throws an exception in the following cases: - /// 1) HTTP 400 BAD_REQUEST - /// 2) Invalid atsign - Future requestAuthenticationOtp(String atsign, - {String authority = RegistrarConstants.apiHostProd}) async { - Response response = await postRequest(authority, - RegistrarConstants.requestAuthenticationOtpPath, {'atsign': atsign}); - String apiResponseMessage = jsonDecode(response.body)['message']; - if (response.statusCode == 200) { - if (apiResponseMessage.contains('Sent Successfully')) { - stdout.writeln( - '[Information] Successfully sent verification code to your registered e-mail'); - return; - } - throw at_client.InternalServerError( - 'Unable to send verification code for authentication.\nCause: $apiResponseMessage'); - } - throw at_client.InvalidRequestException(apiResponseMessage); - } - - /// Returns the cram key for an atsign by fetching it from the registrar API - /// Accepts a registered [atsign], the verification code that was sent to - /// the registered email - /// Throws exception in the following cases: - /// 1) HTTP 400 BAD_REQUEST - Future getCramKey(String atsign, String verificationCode, - {String authority = RegistrarConstants.apiHostProd}) async { - Response response = await postRequest( - authority, - RegistrarConstants.getCramKeyWithOtpPath, - {'atsign': atsign, 'otp': verificationCode}); - Map jsonDecodedBody = jsonDecode(response.body); - if (response.statusCode == 200) { - if (jsonDecodedBody['message'] == 'Verified') { - String cram = jsonDecodedBody['cramkey']; - cram = cram.split(':')[1]; - stdout.writeln('[Information] CRAM Key fetched successfully'); - return cram; - } - throw at_client.InvalidDataException( - 'Invalid verification code. Please enter a valid verification code'); - } - throw at_client.InvalidDataException(jsonDecodedBody['message']); - } - - /// calls utility methods from [OnboardingUtil] that - /// 1) send verification code to the registered email - /// 2) fetch the CRAM key from registrar using the verification code - Future getCramUsingOtp(String atsign, String registrarUrl) async { - await requestAuthenticationOtp(atsign, authority: registrarUrl); - return await getCramKey(atsign, getVerificationCodeFromUser(), - authority: registrarUrl); - } - - /// generic GET request - Future getRequest(String authority, String path) async { - if (_ioClient == null) _createClient(); - Uri uri = Uri.https(authority, path); - Response response = await _ioClient!.get(uri, headers: { - 'Authorization': RegistrarConstants.authorization, - 'Content-Type': RegistrarConstants.contentType, - }); - return response; - } - - /// generic POST request - Future postRequest( - String authority, String path, Map data) async { - if (_ioClient == null) _createClient(); - - Uri uri = Uri.https(authority, path); - - String body = json.encode(data); - if (RegistrarConstants.isDebugMode) { - stdout.writeln('Sending request to url: $uri\nRequest Body: $body'); - } - Response response = await _ioClient!.post( - uri, - body: body, - headers: { - 'Authorization': RegistrarConstants.authorization, - 'Content-Type': RegistrarConstants.contentType, - }, - ); - if (RegistrarConstants.isDebugMode) { - print('Got Response: ${response.body}'); - } - return response; - } - - bool validateEmail(String email) { - return RegExp( - r"^[a-zA-Z0-9.a-zA-Z0-9!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") - .hasMatch(email); - } - - bool validateVerificationCode(String otp) { - if (otp.length == 4) { - return RegExp(r"^[a-zA-z0-9]").hasMatch(otp); - } - return false; - } - - /// Method to get verification code from user input - /// validates code locally and retries taking user input if invalid - /// Returns only when the user has provided a 4-length String only containing numbers and alphabets - String getVerificationCodeFromUser() { - String? otp; - stdout.writeln( - '[Action Required] Enter your verification code: (verification code is not case-sensitive)'); - otp = stdin.readLineSync()!.toUpperCase(); - while (!validateVerificationCode(otp!)) { - stderr.writeln( - '[Unable to proceed] The verification code you entered is invalid.\n' - 'Please check your email for a 4-character verification code.\n' - 'If you cannot see the code in your inbox, please check your spam/junk/promotions folders.\n' - '[Action Required] Enter your verification code:'); - otp = stdin.readLineSync()!.toUpperCase(); - } - return otp; - } -} diff --git a/packages/at_register/lib/src/util/registrar_constants.dart b/packages/at_register/lib/src/config/registrar_constants.dart similarity index 78% rename from packages/at_register/lib/src/util/registrar_constants.dart rename to packages/at_register/lib/src/config/registrar_constants.dart index e2897c1b..66461df7 100644 --- a/packages/at_register/lib/src/util/registrar_constants.dart +++ b/packages/at_register/lib/src/config/registrar_constants.dart @@ -3,6 +3,10 @@ class RegistrarConstants { static const String apiHostProd = 'my.atsign.com'; static const String apiHostStaging = 'my.atsign.wtf'; + /// Select [Prod/Dev] + /// Change to [apiHostStaging] to use AtRegister in a staging env + static const String authority = apiHostStaging; + /// API Paths static const String pathGetFreeAtSign = '/api/app/v3/get-free-atsign'; static const String pathRegisterAtSign = '/api/app/v3/register-person'; @@ -18,5 +22,7 @@ class RegistrarConstants { /// DebugMode: setting it to true will print more logs to aid understanding /// the inner working of Register_cli - static const bool isDebugMode = false; + static const bool isDebugMode = true; + + static const String cramKey = 'cramkey'; } diff --git a/packages/at_register/lib/src/util/api_call_status.dart b/packages/at_register/lib/src/util/api_call_status.dart index 56ad3637..dd2635ff 100644 --- a/packages/at_register/lib/src/util/api_call_status.dart +++ b/packages/at_register/lib/src/util/api_call_status.dart @@ -1 +1,3 @@ enum ApiCallStatus { success, failure, retry } + +enum ValidateOtpStatus{verified, followUp, retry, failure} diff --git a/packages/at_register/lib/src/util/api_util.dart b/packages/at_register/lib/src/util/api_util.dart new file mode 100644 index 00000000..b48f6642 --- /dev/null +++ b/packages/at_register/lib/src/util/api_util.dart @@ -0,0 +1,87 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart' as http; +import 'package:http/io_client.dart'; + +import '../../at_register.dart'; + +class ApiUtil { + static IOClient? _ioClient; + + static void _createClient() { + HttpClient ioc = HttpClient(); + ioc.badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + _ioClient = IOClient(ioc); + } + + /// generic GET request + static Future getRequest(String authority, String path) async { + if (_ioClient == null) _createClient(); + Uri uri = Uri.https(authority, path); + http.Response response = + (await _ioClient!.get(uri, headers: { + 'Authorization': RegistrarConstants.authorization, + 'Content-Type': RegistrarConstants.contentType, + })); + return response; + } + + /// generic POST request + static Future postRequest( + String authority, String path, Map data) async { + if (_ioClient == null) _createClient(); + + Uri uri = Uri.https(authority, path); + + String body = json.encode(data); + if (RegistrarConstants.isDebugMode) { + stdout.writeln('Sending request to url: $uri\nRequest Body: $body'); + } + http.Response response = await _ioClient!.post( + uri, + body: body, + headers: { + 'Authorization': RegistrarConstants.authorization, + 'Content-Type': RegistrarConstants.contentType, + }, + ); + if (RegistrarConstants.isDebugMode) { + print('Got Response: ${response.body}'); + } + return response; + } + + static bool validateEmail(String email) { + return RegExp( + r"^[a-zA-Z0-9.a-zA-Z0-9!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") + .hasMatch(email); + } + + static bool validateVerificationCode(String otp) { + if (otp.length == 4) { + return RegExp(r"^[a-zA-z0-9]").hasMatch(otp); + } + return false; + } + + /// Method to get verification code from user input + /// validates code locally and retries taking user input if invalid + /// Returns only when the user has provided a 4-length String only containing numbers and alphabets + static String getVerificationCodeFromUser() { + String? otp; + stdout.writeln( + '[Action Required] Enter your verification code: (verification code is not case-sensitive)'); + otp = stdin.readLineSync()!.toUpperCase(); + while (!validateVerificationCode(otp!)) { + stderr.writeln( + '[Unable to proceed] The verification code you entered is invalid.\n' + 'Please check your email for a 4-character verification code.\n' + 'If you cannot see the code in your inbox, please check your spam/junk/promotions folders.\n' + '[Action Required] Enter your verification code:'); + otp = stdin.readLineSync()!.toUpperCase(); + } + return otp; + } +} diff --git a/packages/at_register/lib/src/util/at_register_exception.dart b/packages/at_register/lib/src/util/at_register_exception.dart new file mode 100644 index 00000000..d7894520 --- /dev/null +++ b/packages/at_register/lib/src/util/at_register_exception.dart @@ -0,0 +1,20 @@ +import 'package:at_commons/at_commons.dart'; + +class AtRegisterException extends AtException { + AtRegisterException(super.message, {super.intent, super.exceptionScenario}); +} + +class MaximumAtsignQuotaException extends AtException { + MaximumAtsignQuotaException(super.message, + {super.intent, super.exceptionScenario}); +} + +class InvalidVerificationCodeException extends AtException { + InvalidVerificationCodeException(super.message, + {super.intent, super.exceptionScenario}); +} + +class ExhaustedVerificationCodeRetriesException extends AtException { + ExhaustedVerificationCodeRetriesException(super.message, + {super.intent, super.exceptionScenario}); +} diff --git a/packages/at_register/lib/src/util/register_params.dart b/packages/at_register/lib/src/util/register_params.dart new file mode 100644 index 00000000..9e215733 --- /dev/null +++ b/packages/at_register/lib/src/util/register_params.dart @@ -0,0 +1,41 @@ +import 'package:at_commons/at_commons.dart'; + +class RegisterParams { + String? atsign; + String? email; + String? oldEmail; + bool confirmation = true; + String? otp; + String? cram; + + + /// Populates the current instance of [RegisterParams] using the fields from the json + /// + /// Usage: + /// + /// ```RegisterParams params = RegisterParams();``` + /// + /// ```params.addFromJson(json);``` + addFromJson(Map json) { + if (json.containsKey('atsign')) { + atsign = json['atsign']; + } + if (json.containsKey('otp')) { + otp = json['otp']; + } + if (json.containsKey('email')) { + email = json['email']; + } + if (json.containsKey('oldEmail')) { + oldEmail = json['oldEmail']; + } + if (json.containsKey('cramkey')) { + cram = json['cramkey']; + } + } + + @override + String toString() { + return 'atsign: $atsign | email: $email | otp: ${otp.isNullOrEmpty ? 'null' : '****'} | oldEmail: $oldEmail | confirmation: $confirmation'; + } +} diff --git a/packages/at_register/lib/src/util/register_task.dart b/packages/at_register/lib/src/util/register_task.dart index ec279b43..0e5ed3e1 100644 --- a/packages/at_register/lib/src/util/register_task.dart +++ b/packages/at_register/lib/src/util/register_task.dart @@ -1,35 +1,43 @@ import 'dart:collection'; import 'package:at_register/at_register.dart'; -import 'package:at_register/src/util/register_result.dart'; /// Represents a task in an AtSign registration cycle abstract class RegisterTask { static final maximumRetries = 3; - int retryCount = 1; + int _retryCount = 1; - late Map params; + int get retryCount => _retryCount; - late OnboardingUtil registerUtil; + late RegisterParams registerParams; - RegisterResult result = RegisterResult(); + late RegistrarApiCalls registrarApiCalls; - ///Initializes the Task object with necessary parameters - ///[params] is a map that contains necessary data to complete atsign - /// registration process - void init(Map params, OnboardingUtil registerUtil) { - this.params = params; + RegisterTaskResult result = RegisterTaskResult(); + + /// Initializes the Task object with necessary parameters + /// [params] is a map that contains necessary data to complete atsign + /// registration process + void init(RegisterParams registerParams, RegistrarApiCalls registrarApiCalls) { + this.registerParams = registerParams; + this.registrarApiCalls = registrarApiCalls; result.data = HashMap(); - this.registerUtil = registerUtil; } - ///Implementing classes need to implement required logic in this method to - ///complete their sub-process in the AtSign registration process - Future run(); + /// Implementing classes need to implement required logic in this method to + /// complete their sub-process in the AtSign registration process + Future run(); - ///In case the task has returned a [RegisterResult] with status retry, this method checks and returns if the call can be retried + /// Increases retry count by 1 + /// + /// This method is to ensure that retryCount cannot be reduced + void increaseRetryCount(){ + _retryCount++; + } + /// In case the task has returned a [RegisterTaskResult] with status retry, + /// this method checks and returns if the task can be retried bool shouldRetry() { - return retryCount < maximumRetries; + return _retryCount < maximumRetries; } } diff --git a/packages/at_register/lib/src/util/register_result.dart b/packages/at_register/lib/src/util/register_task_result.dart similarity index 80% rename from packages/at_register/lib/src/util/register_result.dart rename to packages/at_register/lib/src/util/register_task_result.dart index e5ea0981..1e485406 100644 --- a/packages/at_register/lib/src/util/register_result.dart +++ b/packages/at_register/lib/src/util/register_task_result.dart @@ -1,9 +1,9 @@ import 'api_call_status.dart'; -class RegisterResult { +class RegisterTaskResult { dynamic data; late ApiCallStatus apiCallStatus; String? exceptionMessage; -} +} \ No newline at end of file diff --git a/packages/at_register/lib/src/util/validate_otp_task_result.dart b/packages/at_register/lib/src/util/validate_otp_task_result.dart new file mode 100644 index 00000000..d3cbfb2f --- /dev/null +++ b/packages/at_register/lib/src/util/validate_otp_task_result.dart @@ -0,0 +1,5 @@ +import '../../at_register.dart'; + +class ValidateOtpResult extends RegisterTaskResult { + ValidateOtpStatus? taskStatus; +} \ No newline at end of file From a2fcc03e57571a57ac76ec329689b5a88d0edca9 Mon Sep 17 00:00:00 2001 From: Srie Teja Date: Fri, 23 Feb 2024 01:25:21 +0530 Subject: [PATCH 06/16] reformat: another major refactor --- .../lib/src/register_cli/register.dart | 9 +- packages/at_onboarding_cli/pubspec.yaml | 2 + .../src/api-interactions/get_free_atsign.dart | 30 ++++--- .../src/api-interactions/register_atsign.dart | 23 ++--- .../api-interactions/registrar_api_calls.dart | 89 +++++++++---------- .../api-interactions/registration_flow.dart | 10 +-- .../src/api-interactions/validate_otp.dart | 20 ++--- .../at_register/lib/src/util/api_util.dart | 36 ++++++-- .../lib/src/util/register_params.dart | 6 +- .../lib/src/util/register_task.dart | 17 +++- .../lib/src/util/register_task_result.dart | 9 +- 11 files changed, 153 insertions(+), 98 deletions(-) diff --git a/packages/at_onboarding_cli/lib/src/register_cli/register.dart b/packages/at_onboarding_cli/lib/src/register_cli/register.dart index 4a16189f..4efad2c9 100644 --- a/packages/at_onboarding_cli/lib/src/register_cli/register.dart +++ b/packages/at_onboarding_cli/lib/src/register_cli/register.dart @@ -4,7 +4,6 @@ import 'package:args/args.dart'; import 'package:at_client/at_client.dart'; import 'package:at_onboarding_cli/src/activate_cli/activate_cli.dart' as activate_cli; -import 'package:at_onboarding_cli/src/util/at_onboarding_exceptions.dart'; import 'package:at_utils/at_logger.dart'; import 'package:at_register/at_register.dart'; @@ -34,11 +33,11 @@ class Register { if (!argResults.wasParsed('email')) { stderr.writeln( '[Unable to run Register CLI] Please enter your email address' - '\n[Usage] dart run register.dart -e email@email.com\n[Options]\n${argParser.usage}'); + '\n[Usage] dart run bin/register.dart -e email@email.com\n[Options]\n${argParser.usage}'); exit(6); } - if (ApiUtil.validateEmail(argResults['email'])) { + if (ApiUtil.enforceEmailRegex(argResults['email'])) { registerParams.email = argResults['email']; } else { stderr.writeln( @@ -61,7 +60,7 @@ class Register { Future main(List args) async { Register register = Register(); - AtSignLogger.root_level = 'severe'; + AtSignLogger.root_level = 'info'; try { await register.main(args); } on MaximumAtsignQuotaException { @@ -76,7 +75,7 @@ Future main(List args) async { stderr.writeln( '[Unable to run Register CLI] Please re-run with your email address'); stderr - .writeln('Usage: \'dart run register_cli.dart -e email@email.com\''); + .writeln('Usage: \'dart run bin/register_cli.dart -e email@email.com\''); exit(1); } else if (e.toString().contains('Could not find an option or flag')) { stderr diff --git a/packages/at_onboarding_cli/pubspec.yaml b/packages/at_onboarding_cli/pubspec.yaml index 22164af2..59a67300 100644 --- a/packages/at_onboarding_cli/pubspec.yaml +++ b/packages/at_onboarding_cli/pubspec.yaml @@ -31,6 +31,8 @@ dependencies: dependency_overrides: at_register: path: /home/srie/Desktop/work/at_libraries/packages/at_register + at_client: + path: /home/srie/Desktop/at_client_sdk/packages/at_client dev_dependencies: lints: ^2.1.0 diff --git a/packages/at_register/lib/src/api-interactions/get_free_atsign.dart b/packages/at_register/lib/src/api-interactions/get_free_atsign.dart index b27c5517..9eedd096 100644 --- a/packages/at_register/lib/src/api-interactions/get_free_atsign.dart +++ b/packages/at_register/lib/src/api-interactions/get_free_atsign.dart @@ -1,27 +1,37 @@ -import 'dart:io'; - import '../../at_register.dart'; -///This is a [RegisterTask] that fetches a free atsign +///This is a [RegisterTask] that fetches a list of free atsigns +/// ///throws [AtException] with concerned message which was encountered in the ///HTTP GET/POST request +/// +/// e.g. +/// +/// `GetFreeAtsign getFreeAtsignInstance = GetFreeAtsign();` +/// +/// `await getFreeAtsignInstance.init(RegisterParams(), RegistrarApiCalls());` +/// +/// `RegisterTaskResult result = await getFreeAtsignInstance.run();` +/// +/// atsign stored in result.data['atsign'] class GetFreeAtsign extends RegisterTask { + @override + String get name => 'GetFreeAtsignTask'; + @override Future run() async { - stdout - .writeln('[Information] Getting your randomly generated free atSign…'); + logger.info('Getting your randomly generated free atSign…'); try { - List atsignList = await registrarApiCalls.getFreeAtSigns( - count: 8, authority: RegistrarConstants.authority); - result.data['atsign'] = atsignList[0]; - stdout.writeln('[Information] Your new atSign is **@${atsignList[0]}**'); + String atsign = await registrarApiCalls.getFreeAtSigns( + authority: RegistrarConstants.authority); + logger.info('Fetched free atsign: $atsign'); + result.data['atsign'] = atsign; result.apiCallStatus = ApiCallStatus.success; } on Exception catch (e) { result.exceptionMessage = e.toString(); result.apiCallStatus = shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; } - return result; } } diff --git a/packages/at_register/lib/src/api-interactions/register_atsign.dart b/packages/at_register/lib/src/api-interactions/register_atsign.dart index a1d609b7..59525a38 100644 --- a/packages/at_register/lib/src/api-interactions/register_atsign.dart +++ b/packages/at_register/lib/src/api-interactions/register_atsign.dart @@ -1,23 +1,26 @@ -import 'dart:io'; - import '../../at_register.dart'; -///This is a [RegisterTask] that registers a free atsign fetched in -///[GetFreeAtsign] to the email provided as args -///throws [AtException] with concerned message which was encountered in the -///HTTP GET/POST request +/// User needs to select an atsign from the list fetched in [GetFreeAtsign]. +/// +/// Registers the selected atsign to the email provided through [RegisterParams] +/// +/// sets [RegisterTaskResult.apiCallStatus] if the HTTP GET/POST request gets any response other than STATUS_OK +/// +/// Note: Provide an atsign through [RegisterParams] if it is not ideal to read +/// user choice through [stdin] class RegisterAtsign extends RegisterTask { + @override + String get name => 'RegisterAtsignTask'; + @override Future run() async { - stdout.writeln( - '[Information] Sending verification code to: ${registerParams.email}'); + logger.info('Sending verification code to: ${registerParams.email}'); try { result.data['otpSent'] = (await registrarApiCalls.registerAtSign( registerParams.atsign!, registerParams.email!, authority: RegistrarConstants.authority)) .toString(); - stdout.writeln( - '[Information] Verification code sent to: ${registerParams.email}'); + logger.info('Verification code sent to: ${registerParams.email}'); result.apiCallStatus = ApiCallStatus.success; } on Exception catch (e) { result.exceptionMessage = e.toString(); diff --git a/packages/at_register/lib/src/api-interactions/registrar_api_calls.dart b/packages/at_register/lib/src/api-interactions/registrar_api_calls.dart index a70c31de..553ef205 100644 --- a/packages/at_register/lib/src/api-interactions/registrar_api_calls.dart +++ b/packages/at_register/lib/src/api-interactions/registrar_api_calls.dart @@ -1,46 +1,44 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; import 'package:at_commons/at_commons.dart'; +import 'package:at_utils/at_logger.dart'; import 'package:http/http.dart' as http; import '../../at_register.dart'; -///class containing utilities to perform registration of a free atsign +/// Contains methods that actually perform the RegistrarAPI calls +/// and handle/process the response class RegistrarApiCalls { - /// Returns a Future> containing free available atSigns of count provided as input. - Future> getFreeAtSigns( - {int count = 1, - String authority = RegistrarConstants.apiHostProd}) async { - List atSigns = []; + AtSignLogger logger = AtSignLogger('AtRegister'); - /// ToDo: discuss - what happens when getRequest does not generate a response + /// Returns a Future> containing free available atSigns + /// based on [count] provided as input. + Future getFreeAtSigns( + {String authority = RegistrarConstants.apiHostProd}) async { http.Response response; - for (int i = 0; i < count; i++) { - // get request at my.atsign.com/api/app/v3/get-free-atsign/ - response = await ApiUtil.getRequest( - authority, RegistrarConstants.pathGetFreeAtSign); - if (response.statusCode == 200) { - String atSign = jsonDecode(response.body)['data']['atsign']; - atSigns.add(atSign); - } else { - throw AtRegisterException( - '${response.statusCode} ${response.reasonPhrase}'); + response = await ApiUtil.getRequest( + authority, RegistrarConstants.pathGetFreeAtSign); + if (response.statusCode == 200) { + String? atsign = jsonDecode(response.body)['data']['atsign']; + if (atsign != null) { + return atsign; } } - return atSigns; + throw AtRegisterException( + 'Could not fetch atsign | ${response.statusCode}:${response.reasonPhrase}'); } - /// Registers the [atSign] provided in the input to the provided [email] + /// Sends verification code to the provided [email] for the [atSign] provided /// /// The `atSign` provided should be an unregistered and free atsign /// - /// Returns true if the request to send the OTP was successful. - /// - /// Sends an OTP to the `email` provided. + /// Returns true if verification code is successfully delivered. /// /// Throws [AtRegisterException] if [atSign] is invalid + /// + /// Note: atsign will not be considered registered at this stage. The verification + /// of verificationCode/otp will take place in [validateOtp] Future registerAtSign(String atSign, String email, {oldEmail, String authority = RegistrarConstants.apiHostProd}) async { http.Response response = await ApiUtil.postRequest( @@ -65,7 +63,9 @@ class RegistrarApiCalls { /// Validates the OTP against the atsign and registers it to the provided email if OTP is valid. /// Returns the CRAM secret of the atsign which is registered. /// - /// [confirmation] - Mandatory parameter for validateOTP API call. First request to be sent with confirmation as false, in this + /// ToDo: what would be the best place to Put the paragraph below + /// + /// [confirmation] - Mandatory parameter for validateOTP API call. First request to be sent with confirmation as FALSE, in this /// case API will return cram key if the user is new otherwise will return list of already existing atsigns. /// If the user already has existing atsigns user will have to select a listed atsign old/new and place a second call /// to the same API endpoint with confirmation set to true with previously received OTP. The second follow-up call @@ -100,7 +100,7 @@ class RegistrarApiCalls { if (jsonDecodedResponse.containsKey('data')) { validateOtpResult.data.addAll(jsonDecodedResponse['data']); } - _processApiResponse(jsonDecodedResponse, validateOtpResult); + _processValidateOtpApiResponse(jsonDecodedResponse, validateOtpResult); } else { throw AtRegisterException( '${response.statusCode} ${response.reasonPhrase}'); @@ -108,36 +108,35 @@ class RegistrarApiCalls { return validateOtpResult; } - /// processes API response for ValidateOtp call - void _processApiResponse(jsonDecodedResponse, result) { - if ((jsonDecodedResponse.containsKey('message') && - (jsonDecodedResponse['message'].toString().toLowerCase()) == - 'verified') && - jsonDecodedResponse.containsKey('cramkey')) { + /// processes API response for [validateOtp] call and populates [result] + void _processValidateOtpApiResponse(responseJson, result) { + if ((responseJson.containsKey('message') && + (responseJson['message'].toString().toLowerCase()) == 'verified') && + responseJson.containsKey('cramkey')) { result.taskStatus = ValidateOtpStatus.verified; - result.data['cramKey'] = jsonDecodedResponse['cramkey']; - } else if (jsonDecodedResponse.containsKey('data') && + result.data[RegistrarConstants.cramKey] = + responseJson[RegistrarConstants.cramKey]; + } else if (responseJson.containsKey('data') && result.data.containsKey('newAtsign')) { result.taskStatus = ValidateOtpStatus.followUp; - } else if (jsonDecodedResponse.containsKey('message') && - - /// ToDo: discuss - compare entire message explicitly or keywords like (expired/invalid) - jsonDecodedResponse['message'] == + } else if (responseJson.containsKey('message') && + responseJson['message'] == 'The code you have entered is invalid or expired. Please try again?') { result.taskStatus = ValidateOtpStatus.retry; - result.exceptionMessage = jsonDecodedResponse['message']; - } else if (jsonDecodedResponse.containsKey('message') && - (jsonDecodedResponse['message'] == + result.exceptionMessage = responseJson['message']; + } else if (responseJson.containsKey('message') && + (responseJson['message'] == 'Oops! You already have the maximum number of free atSigns. Please select one of your existing atSigns.')) { throw MaximumAtsignQuotaException( 'Maximum free atsign limit reached for current email'); } else { - throw AtRegisterException('${jsonDecodedResponse['message']}'); + throw AtRegisterException('${responseJson['message']}'); } } /// Accepts a registered [atsign] as a parameter and sends a one-time verification code /// to the email that the atsign is registered with + /// /// Throws an exception in the following cases: /// 1) HTTP 400 BAD_REQUEST /// 2) Invalid atsign @@ -148,12 +147,12 @@ class RegistrarApiCalls { String apiResponseMessage = jsonDecode(response.body)['message']; if (response.statusCode == 200) { if (apiResponseMessage.contains('Sent Successfully')) { - stdout.writeln( - '[Information] Successfully sent verification code to your registered e-mail'); + logger.info( + 'Successfully sent verification code to your registered e-mail'); return; } throw AtRegisterException( - 'Unable to send verification code for authentication.\nCause: $apiResponseMessage'); + 'Unable to send verification code for authentication. | Cause: $apiResponseMessage'); } throw AtRegisterException(apiResponseMessage); } @@ -175,7 +174,7 @@ class RegistrarApiCalls { if (jsonDecodedBody['message'] == 'Verified') { String cram = jsonDecodedBody['cramkey']; cram = cram.split(':')[1]; - stdout.writeln('[Information] CRAM Key fetched successfully'); + logger.info('CRAM Key fetched successfully'); return cram; } throw InvalidDataException( diff --git a/packages/at_register/lib/src/api-interactions/registration_flow.dart b/packages/at_register/lib/src/api-interactions/registration_flow.dart index 5943895b..c126dd0e 100644 --- a/packages/at_register/lib/src/api-interactions/registration_flow.dart +++ b/packages/at_register/lib/src/api-interactions/registration_flow.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import '../../at_register.dart'; /// class that handles multiple tasks of type [RegisterTask] @@ -22,15 +20,15 @@ class RegistrationFlow { Future start() async { for (RegisterTask task in processQueue) { task.init(params, registrarApiCall); + result = await task.run(); if (RegistrarConstants.isDebugMode) { - stdout.writeln('\nCurrent Task: $task | Attempt: ${task.retryCount} [params=$params]'); + task.logger.shout('Attempt: ${task.retryCount} | params[$params]'); + task.logger.shout('Result: $result'); } - result = await task.run(); if (result.apiCallStatus == ApiCallStatus.retry) { while ( task.shouldRetry() && result.apiCallStatus == ApiCallStatus.retry) { - result = await task.run(); - task.increaseRetryCount(); + result = await task.retry(); } } if (result.apiCallStatus == ApiCallStatus.success) { diff --git a/packages/at_register/lib/src/api-interactions/validate_otp.dart b/packages/at_register/lib/src/api-interactions/validate_otp.dart index 0803f492..2a4393b6 100644 --- a/packages/at_register/lib/src/api-interactions/validate_otp.dart +++ b/packages/at_register/lib/src/api-interactions/validate_otp.dart @@ -1,5 +1,4 @@ import 'dart:collection'; -import 'dart:io'; import 'package:at_commons/at_commons.dart'; import 'package:at_utils/at_utils.dart'; @@ -11,6 +10,9 @@ import '../../at_register.dart'; ///throws [AtException] with concerned message which was encountered in the ///HTTP GET/POST request class ValidateOtp extends RegisterTask { + @override + String get name => 'ValidateOtpTask'; + @override void init( RegisterParams registerParams, RegistrarApiCalls registrarApiCalls) { @@ -18,12 +20,13 @@ class ValidateOtp extends RegisterTask { this.registrarApiCalls = registrarApiCalls; this.registerParams.confirmation = false; result.data = HashMap(); + logger = AtSignLogger(name); } @override Future run() async { registerParams.otp ??= ApiUtil.getVerificationCodeFromUser(); - stdout.writeln('[Information] Validating your verification code...'); + logger.info('Validating your verification code...'); try { registerParams.atsign = AtUtils.fixAtSign(registerParams.atsign!); ValidateOtpResult validateOtpApiResult = @@ -32,10 +35,10 @@ class ValidateOtp extends RegisterTask { confirmation: registerParams.confirmation, authority: RegistrarConstants.authority); if (validateOtpApiResult.taskStatus == ValidateOtpStatus.retry) { - /// ToDo: move this log to onboarding cli - stderr - .writeln('[Unable to proceed] Invalid or expired verification code.' + logger.severe('Invalid or expired verification code.' ' Check your verification code and try again.'); + + /// ToDo: how can an overrided ApiUtil be injected here registerParams.otp = ApiUtil.getVerificationCodeFromUser(); result.apiCallStatus = shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; @@ -51,11 +54,8 @@ class ValidateOtp extends RegisterTask { result.data[RegistrarConstants.cramKey] = validateOtpApiResult.data[RegistrarConstants.cramKey].split(":")[1]; - /// ToDo: move this log to onboarding cli - stdout.writeln( - '[Information] Your cram secret: ${result.data['cramkey']}'); - stdout.writeln( - '[Success] Your atSign **@${registerParams.atsign}** has been' + logger.info('Your cram secret: ${result.data['cramkey']}'); + logger.shout('Your atSign **@${registerParams.atsign}** has been' ' successfully registered to ${registerParams.email}'); result.apiCallStatus = ApiCallStatus.success; } else if (validateOtpApiResult.taskStatus == ValidateOtpStatus.failure) { diff --git a/packages/at_register/lib/src/util/api_util.dart b/packages/at_register/lib/src/util/api_util.dart index b48f6642..6e68bc80 100644 --- a/packages/at_register/lib/src/util/api_util.dart +++ b/packages/at_register/lib/src/util/api_util.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:at_utils/at_logger.dart'; import 'package:http/http.dart' as http; import 'package:http/io_client.dart'; @@ -36,9 +37,6 @@ class ApiUtil { Uri uri = Uri.https(authority, path); String body = json.encode(data); - if (RegistrarConstants.isDebugMode) { - stdout.writeln('Sending request to url: $uri\nRequest Body: $body'); - } http.Response response = await _ioClient!.post( uri, body: body, @@ -48,23 +46,28 @@ class ApiUtil { }, ); if (RegistrarConstants.isDebugMode) { - print('Got Response: ${response.body}'); + AtSignLogger('AtRegister').shout('Sent request to url: $uri | Request Body: $body'); + AtSignLogger('AtRegister').shout('Got Response: ${response.body}'); } return response; } - static bool validateEmail(String email) { + static bool enforceEmailRegex(String email) { return RegExp( r"^[a-zA-Z0-9.a-zA-Z0-9!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") .hasMatch(email); } - static bool validateVerificationCode(String otp) { + static bool enforceOtpRegex(String otp) { if (otp.length == 4) { return RegExp(r"^[a-zA-z0-9]").hasMatch(otp); } return false; } + + static String formatExceptionMessage(String exception){ + return exception.replaceAll('Exception:', ''); + } /// Method to get verification code from user input /// validates code locally and retries taking user input if invalid @@ -74,7 +77,7 @@ class ApiUtil { stdout.writeln( '[Action Required] Enter your verification code: (verification code is not case-sensitive)'); otp = stdin.readLineSync()!.toUpperCase(); - while (!validateVerificationCode(otp!)) { + while (!enforceOtpRegex(otp!)) { stderr.writeln( '[Unable to proceed] The verification code you entered is invalid.\n' 'Please check your email for a 4-character verification code.\n' @@ -84,4 +87,23 @@ class ApiUtil { } return otp; } + + static String readUserAtsignChoice(List? atsigns) { + if (atsigns == null) { + throw AtRegisterException('Fetched atsigns list is null'); + } else if (atsigns.length == 1) { + return atsigns[0]; + } + stdout.writeln( + 'Please select one atsign from the list above. Input the number of the atsign you wish to select.'); + stdout.writeln( + 'For example, type \'2\'+\'Enter\' to select the second atsign (or) just hit \'Enter\' to select the first one'); + stdout.writeln('Valid range is 1 - ${atsigns.length + 1}'); + int? choice = int.tryParse(stdin.readLineSync()!); + if (choice == null) { + return atsigns[0]; + } else { + return atsigns[choice]; + } + } } diff --git a/packages/at_register/lib/src/util/register_params.dart b/packages/at_register/lib/src/util/register_params.dart index 9e215733..b1538437 100644 --- a/packages/at_register/lib/src/util/register_params.dart +++ b/packages/at_register/lib/src/util/register_params.dart @@ -1,4 +1,5 @@ import 'package:at_commons/at_commons.dart'; +import 'package:at_register/at_register.dart'; class RegisterParams { String? atsign; @@ -7,6 +8,7 @@ class RegisterParams { bool confirmation = true; String? otp; String? cram; + List? fetchedAtsignsList; /// Populates the current instance of [RegisterParams] using the fields from the json @@ -17,7 +19,7 @@ class RegisterParams { /// /// ```params.addFromJson(json);``` addFromJson(Map json) { - if (json.containsKey('atsign')) { + if (json.containsKey('atsign') && json['atsign'].runtimeType == String) { atsign = json['atsign']; } if (json.containsKey('otp')) { @@ -29,7 +31,7 @@ class RegisterParams { if (json.containsKey('oldEmail')) { oldEmail = json['oldEmail']; } - if (json.containsKey('cramkey')) { + if (json.containsKey(RegistrarConstants.cramKey)) { cram = json['cramkey']; } } diff --git a/packages/at_register/lib/src/util/register_task.dart b/packages/at_register/lib/src/util/register_task.dart index 0e5ed3e1..6ce67971 100644 --- a/packages/at_register/lib/src/util/register_task.dart +++ b/packages/at_register/lib/src/util/register_task.dart @@ -1,9 +1,12 @@ import 'dart:collection'; import 'package:at_register/at_register.dart'; +import 'package:at_utils/at_logger.dart'; /// Represents a task in an AtSign registration cycle abstract class RegisterTask { + late String name; + static final maximumRetries = 3; int _retryCount = 1; @@ -14,27 +17,37 @@ abstract class RegisterTask { late RegistrarApiCalls registrarApiCalls; + late AtSignLogger logger; + RegisterTaskResult result = RegisterTaskResult(); /// Initializes the Task object with necessary parameters /// [params] is a map that contains necessary data to complete atsign /// registration process - void init(RegisterParams registerParams, RegistrarApiCalls registrarApiCalls) { + void init( + RegisterParams registerParams, RegistrarApiCalls registrarApiCalls) { this.registerParams = registerParams; this.registrarApiCalls = registrarApiCalls; result.data = HashMap(); + logger = AtSignLogger(name); } /// Implementing classes need to implement required logic in this method to /// complete their sub-process in the AtSign registration process Future run(); + Future retry() async { + increaseRetryCount(); + return await run(); + } + /// Increases retry count by 1 /// /// This method is to ensure that retryCount cannot be reduced - void increaseRetryCount(){ + void increaseRetryCount() { _retryCount++; } + /// In case the task has returned a [RegisterTaskResult] with status retry, /// this method checks and returns if the task can be retried bool shouldRetry() { diff --git a/packages/at_register/lib/src/util/register_task_result.dart b/packages/at_register/lib/src/util/register_task_result.dart index 1e485406..591f14d8 100644 --- a/packages/at_register/lib/src/util/register_task_result.dart +++ b/packages/at_register/lib/src/util/register_task_result.dart @@ -6,4 +6,11 @@ class RegisterTaskResult { late ApiCallStatus apiCallStatus; String? exceptionMessage; -} \ No newline at end of file + + @override + String toString() { + return 'Data: $data | ' + 'ApiCallStatus: ${apiCallStatus.name} | ' + 'exception(if encountered): $exceptionMessage'; + } +} From db38d73a5f2ceaebde635d46027bdad5de3f4c11 Mon Sep 17 00:00:00 2001 From: Srie Teja Date: Mon, 4 Mar 2024 01:29:17 +0530 Subject: [PATCH 07/16] another major code push | removed code related to onboardong_cli --- .../src/register_cli}/registration_flow.dart | 3 +- .../test/at_register_test.dart | 316 +++++++++++++++++ ...calls.dart => registrar_api_accessor.dart} | 4 +- .../at_register_allow_retry_false_test.dart | 321 ++++++++++++++++++ .../at_register_allow_retry_true_test.dart | 0 5 files changed, 641 insertions(+), 3 deletions(-) rename packages/{at_register/lib/src/api-interactions => at_onboarding_cli/lib/src/register_cli}/registration_flow.dart (92%) create mode 100644 packages/at_onboarding_cli/test/at_register_test.dart rename packages/at_register/lib/src/api-interactions/{registrar_api_calls.dart => registrar_api_accessor.dart} (98%) create mode 100644 packages/at_register/test/at_register_allow_retry_false_test.dart create mode 100644 packages/at_register/test/at_register_allow_retry_true_test.dart diff --git a/packages/at_register/lib/src/api-interactions/registration_flow.dart b/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart similarity index 92% rename from packages/at_register/lib/src/api-interactions/registration_flow.dart rename to packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart index c126dd0e..b7b15295 100644 --- a/packages/at_register/lib/src/api-interactions/registration_flow.dart +++ b/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart @@ -1,6 +1,6 @@ import '../../at_register.dart'; -/// class that handles multiple tasks of type [RegisterTask] +/// Processes tasks of type [RegisterTask] /// Initialized with a params map that needs to be populated with - email and api host address /// [add] method can be used to add tasks[RegisterTask] to the [processQueue] /// [start] needs to be called after all required tasks are added to the [processQueue] @@ -17,6 +17,7 @@ class RegistrationFlow { return this; } + /// ToDo: should return Result ? or it's fine to populate the params Future start() async { for (RegisterTask task in processQueue) { task.init(params, registrarApiCall); diff --git a/packages/at_onboarding_cli/test/at_register_test.dart b/packages/at_onboarding_cli/test/at_register_test.dart new file mode 100644 index 00000000..207fad10 --- /dev/null +++ b/packages/at_onboarding_cli/test/at_register_test.dart @@ -0,0 +1,316 @@ +import 'package:at_register/at_register.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockRegistrarApiCall extends Mock implements RegistrarApiAccessor {} + +void main() { + RegistrarApiAccessor mockRegistrarApiCall = MockRegistrarApiCall(); + + group('A group of tests to validate GetFreeAtsign', () { + setUp(() => resetMocktailState()); + + test('validate behaviour of GetFreeAtsign', () async { + when(() => mockRegistrarApiCall.getFreeAtSigns()) + .thenAnswer((invocation) => Future.value('@alice')); + + RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; + await RegistrationFlow(params, mockRegistrarApiCall) + .add(GetFreeAtsign()) + .start(); + expect(params.atsign, '@alice'); + }); + + test('validate behaviour of GetFreeAtsign - encounters exception', + () async { + when(() => mockRegistrarApiCall.getFreeAtSigns()) + .thenThrow(Exception('random exception')); + + RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; + GetFreeAtsign getFreeAtsign = GetFreeAtsign(); + + try { + await RegistrationFlow(params, mockRegistrarApiCall) + .add(getFreeAtsign) + .start(); + } on Exception catch (e) { + expect(e.runtimeType, AtRegisterException); + expect(e.toString().contains('random exception'), true); + } + expect(getFreeAtsign.retryCount, RegisterTask.maximumRetries); + expect(getFreeAtsign.result.apiCallStatus, ApiCallStatus.failure); + expect(getFreeAtsign.shouldRetry(), false); + expect(getFreeAtsign.retryCount, 3); + }); + + // test('fetch multiple atsigns using GetFreeAtsign', () async { + // String email = 'first-group-3@testland.test'; + // when(() => mockRegistrarApiCall.getFreeAtSigns()) + // .thenAnswer((invocation) => Future.value(['@alice', '@bob', '@charlie'])); + // + // RegisterParams params = RegisterParams()..email = email; + // GetFreeAtsign getFreeAtsignInstance = GetFreeAtsign(count: 3); + // getFreeAtsignInstance.init(params, mockRegistrarApiCall); + // RegisterTaskResult result = await getFreeAtsignInstance.run(); + // + // expect(result.data['atsign'], ''); + // }); + }); + + group('Group of tests to validate behaviour of RegisterAtsign', () { + setUp(() => resetMocktailState()); + + test('validate RegisterAtsign behaviour in RegistrationFlow', () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + when(() => mockRegistrarApiCall.registerAtSign(atsign, email)) + .thenAnswer((_) => Future.value(true)); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + RegisterAtsign registerAtsignTask = RegisterAtsign(); + await RegistrationFlow(params, mockRegistrarApiCall) + .add(registerAtsignTask) + .start(); + expect(registerAtsignTask.retryCount, 1); // 1 is the default retry count + // this task does not generate any new params. This test validates how RegistrationFlow + // processes the task when otp has been sent to user's email + // successful execution of this test would indicate that the process did not + // encounter any errors/exceptions + }); + + test('RegisterAtsign params reading and updating - positive case', + () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + when(() => mockRegistrarApiCall.registerAtSign(atsign, email)) + .thenAnswer((_) => Future.value(true)); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + RegisterAtsign registerAtsignTask = RegisterAtsign(); + registerAtsignTask.init(params, mockRegistrarApiCall); + RegisterTaskResult result = await registerAtsignTask.run(); + expect(result.apiCallStatus, ApiCallStatus.success); + expect(result.data['otpSent'], 'true'); + }); + + test('RegisterAtsign params reading and updating - negative case', + () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + when(() => mockRegistrarApiCall.registerAtSign(atsign, email)) + .thenAnswer((_) => Future.value(false)); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + RegisterAtsign registerAtsignTask = RegisterAtsign(); + registerAtsignTask.init(params, mockRegistrarApiCall); + RegisterTaskResult result = await registerAtsignTask.run(); + expect(result.apiCallStatus, ApiCallStatus.success); + expect(result.data['otpSent'], 'false'); + expect(registerAtsignTask.shouldRetry(), true); + }); + + test('verify behaviour of RegisterAtsign processing an exception', + () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + when(() => mockRegistrarApiCall.registerAtSign(atsign, email)) + .thenThrow(Exception('another random exception')); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + RegisterAtsign registerAtsignTask = RegisterAtsign(); + registerAtsignTask.init(params, mockRegistrarApiCall); + RegisterTaskResult result = await registerAtsignTask.run(); + expect(result.apiCallStatus, ApiCallStatus.retry); + expect(result.exceptionMessage, 'Exception: another random exception'); + expect(registerAtsignTask.shouldRetry(), true); + }); + + test( + 'verify behaviour of RegistrationFlow processing exception in RegisterAtsign', + () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + when(() => mockRegistrarApiCall.registerAtSign(atsign, email)) + .thenThrow(Exception('another new random exception')); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + RegisterAtsign registerAtsignTask = RegisterAtsign(); + try { + await RegistrationFlow(params, mockRegistrarApiCall) + .add(registerAtsignTask) + .start(); + } on Exception catch (e) { + print(e.toString()); + assert(e.toString().contains('another new random exception')); + } + expect(registerAtsignTask.retryCount, 3); + expect(registerAtsignTask.shouldRetry(), false); + }); + }); + + group('A group of tests to verify ValidateOtp task behaviour', () { + setUp(() => resetMocktailState()); + + test( + 'validate positive behaviour of ValidateOtp task - received cram in first call', + () async { + String atsign = '@charlie'; + String email = 'third-group@email'; + String otp = 'Abcd'; + String cram = 'craaaaaaaaaaaam'; + ValidateOtpResult validateOtpResult = ValidateOtpResult(); + validateOtpResult.taskStatus = ValidateOtpStatus.verified; + validateOtpResult.apiCallStatus = ApiCallStatus.success; + validateOtpResult.data = {RegistrarConstants.cramKey: '$atsign:$cram'}; + when(() => mockRegistrarApiCall.validateOtp(atsign, email, otp, + confirmation: false)) + .thenAnswer((invocation) => Future.value(validateOtpResult)); + + var params = RegisterParams() + ..atsign = atsign + ..confirmation = false + ..email = email + ..otp = otp; + + await RegistrationFlow(params, mockRegistrarApiCall) + .add(ValidateOtp()) + .start(); + + expect(params.cram, cram); + }); + + test( + 'validate positive behaviour of ValidateOtp task - need to followUp with confirmation set to true', + () async { + String atsign = '@charlie123'; + String email = 'third-group@email'; + String otp = 'bcde'; + String cram = 'craaaaaaaaaaaam1234'; + + var mockApiRespData = { + 'atsign': ['@old-atsign'], + 'newAtsign': atsign + }; + ValidateOtpResult validateOtpResult = ValidateOtpResult() + ..taskStatus = ValidateOtpStatus.followUp + ..apiCallStatus = ApiCallStatus.success + ..data = {'data': mockApiRespData}; + when(() => mockRegistrarApiCall.validateOtp(atsign, email, otp, + confirmation: false)) + .thenAnswer((invocation) => Future.value(validateOtpResult)); + + ValidateOtpResult validateOtpResult2 = ValidateOtpResult() + ..taskStatus = ValidateOtpStatus.verified + ..apiCallStatus = ApiCallStatus.success + ..data = {RegistrarConstants.cramKey: '$atsign:$cram'}; + when(() => mockRegistrarApiCall.validateOtp(atsign, email, otp, + confirmation: true)) + .thenAnswer((invocation) => Future.value(validateOtpResult2)); + + var params = RegisterParams() + ..atsign = atsign + ..confirmation = false + ..email = email + ..otp = otp; + // confirmation needs to be false for first call ? + await RegistrationFlow(params, mockRegistrarApiCall) + .add(ValidateOtp()) + .start(); + + expect(params.cram, cram); + expect(params.confirmation, true); + expect(validateOtpResult2.taskStatus, ValidateOtpStatus.verified); + }); + + test('validate behaviour of ValidateOtp task - 3 otp retries exhausted', + () async { + String atsign = '@charlie-otp-retry'; + String email = 'third-group-test-3@email'; + String otp = 'bcaa'; + String cram = 'craaaaaaaaaaaam'; + ValidateOtpResult validateOtpResult = ValidateOtpResult(); + validateOtpResult.taskStatus = ValidateOtpStatus.retry; + validateOtpResult.apiCallStatus = ApiCallStatus.success; + validateOtpResult.data = {RegistrarConstants.cramKey: '$atsign:$cram'}; + when(() => mockRegistrarApiCall.validateOtp(atsign, email, otp, + confirmation: any(named: "confirmation"))) + .thenAnswer((invocation) => Future.value(validateOtpResult)); + + var params = RegisterParams() + ..atsign = atsign + ..confirmation = false + ..email = email + ..otp = otp; + var validateOtpTask = ValidateOtp(); + expect( + () async => await RegistrationFlow(params, mockRegistrarApiCall) + .add(validateOtpTask) + .start(), + throwsA(ExhaustedVerificationCodeRetriesException)); + + expect(validateOtpTask.retryCount, 3); + }); + }); + + group('test to validate all 3 API calls in sequence', () { + setUp(() => resetMocktailState()); + + test('verify all 3 API calls at once', () async { + String atsign = '@lewis'; + String email = 'lewis44@gmail.com'; + String cram = 'craaaaaaaaaaaaam'; + String otp = 'Agbr'; + // mock for get-free-atsign + when(() => mockRegistrarApiCall.getFreeAtSigns()) + .thenAnswer((invocation) => Future.value(atsign)); + // mock for register-atsign + when(() => mockRegistrarApiCall.registerAtSign(atsign, email)) + .thenAnswer((_) => Future.value(true)); + // rest of the mocks for validate-otp + var mockApiRespData = { + 'atsign': ['@old-atsign'], + 'newAtsign': atsign + }; + ValidateOtpResult validateOtpResult = ValidateOtpResult() + ..taskStatus = ValidateOtpStatus.followUp + ..apiCallStatus = ApiCallStatus.success + ..data = {'data': mockApiRespData}; + when(() => mockRegistrarApiCall.validateOtp(atsign, email, otp, + confirmation: false)) + .thenAnswer((invocation) => Future.value(validateOtpResult)); + + ValidateOtpResult validateOtpResult2 = ValidateOtpResult() + ..taskStatus = ValidateOtpStatus.verified + ..apiCallStatus = ApiCallStatus.success + ..data = {RegistrarConstants.cramKey: '$atsign:$cram'}; + when(() => mockRegistrarApiCall.validateOtp(atsign, email, otp, + confirmation: true)) + .thenAnswer((invocation) => Future.value(validateOtpResult2)); + + RegisterParams params = RegisterParams() + ..email = email + ..otp = otp + ..confirmation = false; + + await RegistrationFlow(params, mockRegistrarApiCall) + .add(GetFreeAtsign()) + .add(RegisterAtsign()) + .add(ValidateOtp()) + .start(); + + expect(params.atsign, atsign); + expect(params.cram, cram); + expect(params.confirmation, true); + }); + }); +} diff --git a/packages/at_register/lib/src/api-interactions/registrar_api_calls.dart b/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart similarity index 98% rename from packages/at_register/lib/src/api-interactions/registrar_api_calls.dart rename to packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart index 553ef205..5b112a04 100644 --- a/packages/at_register/lib/src/api-interactions/registrar_api_calls.dart +++ b/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart @@ -9,7 +9,7 @@ import '../../at_register.dart'; /// Contains methods that actually perform the RegistrarAPI calls /// and handle/process the response -class RegistrarApiCalls { +class RegistrarApiAccessor { AtSignLogger logger = AtSignLogger('AtRegister'); /// Returns a Future> containing free available atSigns @@ -183,7 +183,7 @@ class RegistrarApiCalls { throw InvalidDataException(jsonDecodedBody['message']); } - /// calls utility methods from [RegistrarApiCalls] that + /// calls utility methods from [RegistrarApiAccessor] that /// /// 1) send verification code to the registered email /// diff --git a/packages/at_register/test/at_register_allow_retry_false_test.dart b/packages/at_register/test/at_register_allow_retry_false_test.dart new file mode 100644 index 00000000..e7160bc9 --- /dev/null +++ b/packages/at_register/test/at_register_allow_retry_false_test.dart @@ -0,0 +1,321 @@ +import 'dart:collection'; + +import 'package:at_register/at_register.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockRegistrarApiAccessor extends Mock implements RegistrarApiAccessor {} + +void main() { + MockRegistrarApiAccessor mockRegistrarApiAccessor = + MockRegistrarApiAccessor(); + + group('A group of tests to validate GetFreeAtsign', () { + setUp(() => resetMocktailState()); + + test('validate behaviour of GetFreeAtsign', () async { + when(() => mockRegistrarApiAccessor.getFreeAtSigns()) + .thenAnswer((invocation) => Future.value('@alice')); + print(mockRegistrarApiAccessor.getFreeAtSigns()); + RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; + GetFreeAtsign getFreeAtsign = GetFreeAtsign(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + final result = await getFreeAtsign.run(); + expect(result.data[RegistrarConstants.atsignName], '@alice'); + }); + + test('validate behaviour of GetFreeAtsign - encounters exception', + () async { + when(() => mockRegistrarApiAccessor.getFreeAtSigns()) + .thenThrow(Exception('random exception')); + + RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; + GetFreeAtsign getFreeAtsign = GetFreeAtsign(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + bool exceptionFlag = false; + try { + await getFreeAtsign.run(); + } on Exception catch (e) { + expect(e.runtimeType, AtRegisterException); + expect(e.toString().contains('random exception'), true); + exceptionFlag = true; + } + expect(getFreeAtsign.shouldRetry(), true); + expect(exceptionFlag, true); + }); + }); + + group('Group of tests to validate behaviour of RegisterAtsign', () { + setUp(() => resetMocktailState()); + + test('validate RegisterAtsign behaviour in RegistrationFlow', () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + when(() => mockRegistrarApiAccessor.registerAtSign(atsign, email)) + .thenAnswer((_) => Future.value(true)); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + RegisterAtsign registerAtsignTask = RegisterAtsign(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + await registerAtsignTask.run(); + expect(registerAtsignTask.retryCount, 1); // 1 is the default retry count + // this task does not generate any new params. This test validates how RegistrationFlow + // processes the task when otp has been sent to user's email + // successful execution of this test would indicate that the process did not + // encounter any errors/exceptions + }); + + test('RegisterAtsign params reading and updating - positive case', + () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + when(() => mockRegistrarApiAccessor.registerAtSign(atsign, email)) + .thenAnswer((_) => Future.value(true)); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + RegisterAtsign registerAtsignTask = RegisterAtsign(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + RegisterTaskResult result = await registerAtsignTask.run(); + expect(result.apiCallStatus, ApiCallStatus.success); + expect(result.data['otpSent'], 'true'); + }); + + test('RegisterAtsign params reading and updating - negative case', + () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + when(() => mockRegistrarApiAccessor.registerAtSign(atsign, email)) + .thenAnswer((_) => Future.value(false)); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + RegisterAtsign registerAtsignTask = RegisterAtsign(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + RegisterTaskResult result = await registerAtsignTask.run(); + expect(result.apiCallStatus, ApiCallStatus.success); + expect(result.data['otpSent'], 'false'); + expect(registerAtsignTask.shouldRetry(), true); + }); + + test('verify behaviour of RegisterAtsign processing an exception', + () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + when(() => mockRegistrarApiAccessor.registerAtSign(atsign, email)) + .thenThrow(Exception('another random exception')); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + + RegisterAtsign registerAtsignTask = RegisterAtsign(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + bool exceptionFlag = false; + try { + await registerAtsignTask.run(); + } on Exception catch (e) { + expect(e.runtimeType, AtRegisterException); + expect(e.toString().contains('random exception'), true); + exceptionFlag = true; + } + expect(registerAtsignTask.shouldRetry(), true); + expect(exceptionFlag, true); + }); + + test( + 'verify behaviour of RegistrationFlow processing exception in RegisterAtsign', + () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + when(() => mockRegistrarApiAccessor.registerAtSign(atsign, email)) + .thenThrow(Exception('another new random exception')); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + RegisterAtsign registerAtsignTask = RegisterAtsign(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + bool exceptionFlag = false; + try { + await registerAtsignTask.run(); + } on Exception catch (e) { + print(e.toString()); + assert(e.toString().contains('another new random exception')); + exceptionFlag = true; + } + expect(registerAtsignTask.retryCount, 1); + expect(exceptionFlag, true); + }); + }); + + group('A group of tests to verify ValidateOtp task behaviour', () { + setUp(() => resetMocktailState()); + + test( + 'validate positive behaviour of ValidateOtp task - received cram in first call', + () async { + String atsign = '@charlie'; + String email = 'third-group@email'; + String otp = 'Abcd'; + String cram = 'craaaaaaaaaaaam'; + ValidateOtpResult validateOtpResult = ValidateOtpResult(); + validateOtpResult.taskStatus = ValidateOtpStatus.verified; + validateOtpResult.apiCallStatus = ApiCallStatus.success; + validateOtpResult.data = { + RegistrarConstants.cramKeyName: '$atsign:$cram' + }; + when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, + confirmation: false)) + .thenAnswer((invocation) => Future.value(validateOtpResult)); + + var params = RegisterParams() + ..atsign = atsign + ..confirmation = false + ..email = email + ..otp = otp; + + ValidateOtp validateOtpTask = ValidateOtp(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + RegisterTaskResult result = await validateOtpTask.run(); + + expect(result.data[RegistrarConstants.cramKeyName], cram); + }); + + test( + 'validate positive behaviour of ValidateOtp task - need to followUp with confirmation set to true', + () async { + String atsign = '@charlie123'; + String atsign2 = '@cheesecake'; + String email = 'third-group@email'; + String otp = 'bcde'; + String cram = 'craaaaaaaaaaaam1234'; + + var mockApiRespData = { + 'atsign': ['@old-atsign'], + 'newAtsign': atsign + }; + ValidateOtpResult validateOtpResult = ValidateOtpResult() + ..taskStatus = ValidateOtpStatus.followUp + ..apiCallStatus = ApiCallStatus.success + ..data = {'data': mockApiRespData}; + when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, + confirmation: false)) + .thenAnswer((invocation) => Future.value(validateOtpResult)); + + ValidateOtpResult validateOtpResult2 = ValidateOtpResult() + ..taskStatus = ValidateOtpStatus.verified + ..apiCallStatus = ApiCallStatus.success + ..data = {RegistrarConstants.cramKeyName: '$atsign:$cram'}; + when(() => mockRegistrarApiAccessor.validateOtp(atsign2, email, otp, + confirmation: true)) + .thenAnswer((invocation) => Future.value(validateOtpResult2)); + + var params = RegisterParams() + ..atsign = atsign + ..confirmation = false // confirmation needs to be false for first call + ..email = email + ..otp = otp; + + ValidateOtp validateOtpTask = ValidateOtp(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + RegisterTaskResult result = await validateOtpTask.run(); + expect(params.confirmation, true); + expect(result.apiCallStatus, ApiCallStatus.retry); + expect(validateOtpResult2.taskStatus, ValidateOtpStatus.verified); + + // The above case is when an email has already existing atsigns, select an atsign + // from the list and retry the task with confirmation set to 'true' + params.atsign = atsign2; + result = await validateOtpTask.run(); + expect(result.apiCallStatus, ApiCallStatus.success); + expect(result.data[RegistrarConstants.cramKeyName], cram); + }); + + test('validate behaviour of ValidateOtp task - 3 otp retries exhausted', + () async { + String atsign = '@charlie-otp-retry'; + String email = 'third-group-test-3@email'; + String otp = 'bcaa'; + String cram = 'craaaaaaaaaaaam'; + ValidateOtpResult validateOtpResult = ValidateOtpResult(); + validateOtpResult.taskStatus = ValidateOtpStatus.retry; + validateOtpResult.apiCallStatus = ApiCallStatus.success; + validateOtpResult.data = { + RegistrarConstants.cramKeyName: '$atsign:$cram' + }; + when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, + confirmation: any(named: "confirmation"))) + .thenAnswer((invocation) => Future.value(validateOtpResult)); + + var params = RegisterParams() + ..atsign = atsign + ..confirmation = false + ..email = email + ..otp = otp; + ValidateOtp validateOtpTask = ValidateOtp(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + + expect(() async => await validateOtpTask.run(), + throwsA(ExhaustedVerificationCodeRetriesException)); + + expect(validateOtpTask.retryCount, 1); + }); + }); + + group('test to validate all 3 API calls in sequence', () { + setUp(() => resetMocktailState()); + // + // test('verify all 3 API calls in sequence', () async { + // String atsign = '@lewis'; + // String email = 'lewis44@gmail.com'; + // String cram = 'craaaaaaaaaaaaam'; + // String otp = 'Agbr'; + // // mock for get-free-atsign + // when(() => mockRegistrarApiAccessor.getFreeAtSigns()) + // .thenAnswer((invocation) => Future.value(atsign)); + // // mock for register-atsign + // when(() => mockRegistrarApiAccessor.registerAtSign(atsign, email)) + // .thenAnswer((_) => Future.value(true)); + // // rest of the mocks for validate-otp + // var mockApiRespData = { + // 'atsign': ['@old-atsign'], + // 'newAtsign': atsign + // }; + // ValidateOtpResult validateOtpResult = ValidateOtpResult() + // ..taskStatus = ValidateOtpStatus.followUp + // ..apiCallStatus = ApiCallStatus.success + // ..data = {'data': mockApiRespData}; + // when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, + // confirmation: false)) + // .thenAnswer((invocation) => Future.value(validateOtpResult)); + // + // ValidateOtpResult validateOtpResult2 = ValidateOtpResult() + // ..taskStatus = ValidateOtpStatus.verified + // ..apiCallStatus = ApiCallStatus.success + // ..data = {RegistrarConstants.cramKey: '$atsign:$cram'}; + // when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, + // confirmation: true)) + // .thenAnswer((invocation) => Future.value(validateOtpResult2)); + // + // RegisterParams params = RegisterParams() + // ..email = email + // ..otp = otp + // ..confirmation = false; + // + // await RegistrationFlow(params, mockRegistrarApiAccessor) + // .add(GetFreeAtsign()) + // .add(RegisterAtsign()) + // .add(ValidateOtp()) + // .start(); + // + // expect(params.atsign, atsign); + // expect(params.cram, cram); + // expect(params.confirmation, true); + // }); + }); +} diff --git a/packages/at_register/test/at_register_allow_retry_true_test.dart b/packages/at_register/test/at_register_allow_retry_true_test.dart new file mode 100644 index 00000000..e69de29b From 2833240c2799dadce8c9996e158bee7ed6ce10d0 Mon Sep 17 00:00:00 2001 From: Srie Teja Date: Mon, 4 Mar 2024 02:24:55 +0530 Subject: [PATCH 08/16] reformat: another major refactor | added more tests --- .github/workflows/at_libraries.yaml | 1 + .../at_onboarding_cli/bin/register_cli.dart | 2 +- .../apkam_examples/enroll_app_listen.dart | 2 +- .../example/get_cram_key.dart | 8 +- .../onboard/at_onboarding_service_impl.dart | 4 +- .../lib/src/register_cli/register.dart | 190 +++++++++-- .../src/register_cli/registration_flow.dart | 42 --- .../src/util/at_onboarding_preference.dart | 4 +- packages/at_onboarding_cli/pubspec.yaml | 6 - .../test/at_register_test.dart | 316 ------------------ packages/at_register/lib/at_register.dart | 3 +- .../src/api-interactions/get_free_atsign.dart | 38 ++- .../src/api-interactions/register_atsign.dart | 23 +- .../registrar_api_accessor.dart | 45 ++- .../src/api-interactions/validate_otp.dart | 119 ++++--- .../lib/src/config/registrar_constants.dart | 13 +- .../at_register/lib/src/util/api_util.dart | 16 +- .../lib/src/util/register_params.dart | 6 +- .../lib/src/util/register_task.dart | 29 +- .../lib/src/util/register_task_result.dart | 4 +- .../src/util/validate_otp_task_result.dart | 2 +- .../at_register_allow_retry_false_test.dart | 161 ++++----- .../at_register_allow_retry_true_test.dart | 265 +++++++++++++++ 23 files changed, 672 insertions(+), 627 deletions(-) delete mode 100644 packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart delete mode 100644 packages/at_onboarding_cli/test/at_register_test.dart diff --git a/.github/workflows/at_libraries.yaml b/.github/workflows/at_libraries.yaml index a1229668..53adcf04 100644 --- a/.github/workflows/at_libraries.yaml +++ b/.github/workflows/at_libraries.yaml @@ -65,6 +65,7 @@ jobs: - at_onboarding_cli - at_commons - at_utils + - at_register steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 diff --git a/packages/at_onboarding_cli/bin/register_cli.dart b/packages/at_onboarding_cli/bin/register_cli.dart index 237a5517..a6cca120 100644 --- a/packages/at_onboarding_cli/bin/register_cli.dart +++ b/packages/at_onboarding_cli/bin/register_cli.dart @@ -3,4 +3,4 @@ import 'package:at_onboarding_cli/src/register_cli/register.dart' Future main(List args) async { await register_cli.main(args); -} \ No newline at end of file +} diff --git a/packages/at_onboarding_cli/example/apkam_examples/enroll_app_listen.dart b/packages/at_onboarding_cli/example/apkam_examples/enroll_app_listen.dart index fe424301..c263d09f 100644 --- a/packages/at_onboarding_cli/example/apkam_examples/enroll_app_listen.dart +++ b/packages/at_onboarding_cli/example/apkam_examples/enroll_app_listen.dart @@ -65,7 +65,7 @@ Future _notificationCallback(AtNotification notification, atAuthKeys.defaultEncryptionPrivateKey!, apkamSymmetricKey); var encryptedDefaultSelfEncKey = EncryptionUtil.encryptValue( atAuthKeys.defaultSelfEncryptionKey!, apkamSymmetricKey); - enrollParamsJson['encryptedDefaultEncryptedPrivateKey'] = + enrollParamsJson['encryptedDefaultEncryptionPrivateKey'] = encryptedDefaultPrivateEncKey; enrollParamsJson['encryptedDefaultSelfEncryptionKey'] = encryptedDefaultSelfEncKey; diff --git a/packages/at_onboarding_cli/example/get_cram_key.dart b/packages/at_onboarding_cli/example/get_cram_key.dart index c3cbfb80..1af1b0f5 100644 --- a/packages/at_onboarding_cli/example/get_cram_key.dart +++ b/packages/at_onboarding_cli/example/get_cram_key.dart @@ -1,5 +1,5 @@ import 'package:args/args.dart'; -import 'package:at_register/at_register.dart'; +import 'package:at_onboarding_cli/src/util/onboarding_util.dart'; import 'util/custom_arg_parser.dart'; @@ -7,12 +7,12 @@ Future main(args) async { final argResults = CustomArgParser(getArgParser()).parse(args); // this step sends an OTP to the registered email - await RegisterApiCall().requestAuthenticationOtp( + await OnboardingUtil().requestAuthenticationOtp( argResults['atsign']); // requires a registered atsign // the following step validates the email that was sent in the above step - String? verificationCode = ApiUtil.getVerificationCodeFromUser(); - String cramKey = await RegisterApiCall().getCramKey(argResults['atsign'], + String? verificationCode = OnboardingUtil().getVerificationCodeFromUser(); + String cramKey = await OnboardingUtil().getCramKey(argResults['atsign'], verificationCode); // verification code received on the registered email print('Your cram key is: $cramKey'); diff --git a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart index a5c55cfa..5284dc66 100644 --- a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart +++ b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart @@ -18,9 +18,9 @@ import 'package:encrypt/encrypt.dart'; import 'package:zxing2/qrcode.dart'; import 'package:image/image.dart'; import 'package:path/path.dart' as path; -import 'package:at_register/at_register.dart'; import '../util/home_directory_util.dart'; +import '../util/onboarding_util.dart'; ///class containing service that can onboard/activate/authenticate @signs class AtOnboardingServiceImpl implements AtOnboardingService { @@ -97,7 +97,7 @@ class AtOnboardingServiceImpl implements AtOnboardingService { // get cram_secret from either from AtOnboardingPreference // or fetch from the registrar using verification code sent to email - atOnboardingPreference.cramSecret ??= await RegistrarApiCalls() + atOnboardingPreference.cramSecret ??= await OnboardingUtil() .getCramUsingOtp(_atSign, atOnboardingPreference.registrarUrl); if (atOnboardingPreference.cramSecret == null) { logger.info('Root Server address is ${atOnboardingPreference.rootDomain}:' diff --git a/packages/at_onboarding_cli/lib/src/register_cli/register.dart b/packages/at_onboarding_cli/lib/src/register_cli/register.dart index 4efad2c9..eedc8eb5 100644 --- a/packages/at_onboarding_cli/lib/src/register_cli/register.dart +++ b/packages/at_onboarding_cli/lib/src/register_cli/register.dart @@ -1,19 +1,26 @@ +import 'dart:collection'; import 'dart:io'; import 'package:args/args.dart'; import 'package:at_client/at_client.dart'; import 'package:at_onboarding_cli/src/activate_cli/activate_cli.dart' as activate_cli; +import 'package:at_onboarding_cli/src/util/api_call_status.dart'; +import 'package:at_onboarding_cli/src/util/at_onboarding_exceptions.dart'; +import 'package:at_onboarding_cli/src/util/register_api_result.dart'; +import 'package:at_onboarding_cli/src/util/register_api_task.dart'; import 'package:at_utils/at_logger.dart'; -import 'package:at_register/at_register.dart'; + +import '../util/onboarding_util.dart'; +import '../util/registrar_api_constants.dart'; ///Class containing logic to register a free atsign to email provided ///through [args] by utilizing methods defined in [RegisterUtil] ///Requires List args containing the following arguments: email class Register { Future main(List args) async { - RegisterParams registerParams = RegisterParams(); - RegistrarApiCalls registerUtil = RegistrarApiCalls(); + Map params = HashMap(); + OnboardingUtil registerUtil = OnboardingUtil(); final argParser = ArgParser() ..addOption('email', @@ -33,49 +40,188 @@ class Register { if (!argResults.wasParsed('email')) { stderr.writeln( '[Unable to run Register CLI] Please enter your email address' - '\n[Usage] dart run bin/register.dart -e email@email.com\n[Options]\n${argParser.usage}'); + '\n[Usage] dart run register.dart -e email@email.com\n[Options]\n${argParser.usage}'); exit(6); } - if (ApiUtil.enforceEmailRegex(argResults['email'])) { - registerParams.email = argResults['email']; + if (registerUtil.validateEmail(argResults['email'])) { + params['email'] = argResults['email']; } else { stderr.writeln( '[Unable to run Register CLI] You have entered an invalid email address. Check your email address and try again.'); exit(7); } - // create a queue of tasks each of type [RegisterTask] and then - // call start on the RegistrationFlow object - await RegistrationFlow(registerParams, registerUtil) + //set the following parameter to RegisterApiConstants.apiHostStaging + //to use the staging environment + params['authority'] = RegistrarApiConstants.apiHostProd; + + //create stream of tasks each of type [RegisterApiTask] and then + // call start on the stream of tasks + await RegistrationFlow(params, registerUtil) .add(GetFreeAtsign()) .add(RegisterAtsign()) .add(ValidateOtp()) .start(); - activate_cli - .main(['-a', registerParams.atsign!, '-c', registerParams.cram!]); + activate_cli.main(['-a', params['atsign']!, '-c', params['cramkey']!]); + } +} + +///class that handles multiple tasks of type [RegisterApiTask] +///Initialized with a params map that needs to be populated with - email and api host address +///[add] method can be used to add tasks[RegisterApiTask] to the [processFlow] +///[start] needs to be called after all required tasks are added to the [processFlow] +class RegistrationFlow { + List processFlow = []; + RegisterApiResult result = RegisterApiResult(); + late OnboardingUtil registerUtil; + Map params; + + RegistrationFlow(this.params, this.registerUtil); + + RegistrationFlow add(RegisterApiTask task) { + processFlow.add(task); + return this; + } + + Future start() async { + for (RegisterApiTask task in processFlow) { + task.init(params, registerUtil); + if (RegistrarApiConstants.isDebugMode) { + print('Current Task: $task [params=$params]\n'); + } + result = await task.run(); + if (result.apiCallStatus == ApiCallStatus.retry) { + while ( + task.shouldRetry() && result.apiCallStatus == ApiCallStatus.retry) { + result = await task.run(); + task.retryCount++; + } + } + if (result.apiCallStatus == ApiCallStatus.success) { + params.addAll(result.data); + } else { + throw AtOnboardingException(result.exceptionMessage); + } + } + } +} + +///This is a [RegisterApiTask] that fetches a free atsign +///throws [AtException] with concerned message which was encountered in the +///HTTP GET/POST request +class GetFreeAtsign extends RegisterApiTask { + @override + Future run() async { + stdout + .writeln('[Information] Getting your randomly generated free atSign…'); + try { + List atsignList = + await registerUtil.getFreeAtSigns(authority: params['authority']!); + result.data['atsign'] = atsignList[0]; + stdout.writeln('[Information] Your new atSign is **@${atsignList[0]}**'); + result.apiCallStatus = ApiCallStatus.success; + } on Exception catch (e) { + result.exceptionMessage = e.toString(); + result.apiCallStatus = + shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; + } + + return result; + } +} + +///This is a [RegisterApiTask] that registers a free atsign fetched in +///[GetFreeAtsign] to the email provided as args +///throws [AtException] with concerned message which was encountered in the +///HTTP GET/POST request +class RegisterAtsign extends RegisterApiTask { + @override + Future run() async { + stdout.writeln( + '[Information] Sending verification code to: ${params['email']}'); + try { + result.data['otpSent'] = (await registerUtil.registerAtSign( + params['atsign']!, params['email']!, + authority: params['authority']!)) + .toString(); + stdout.writeln( + '[Information] Verification code sent to: ${params['email']}'); + result.apiCallStatus = ApiCallStatus.success; + } on Exception catch (e) { + result.exceptionMessage = e.toString(); + result.apiCallStatus = + shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; + } + return result; + } +} + +///This is a [RegisterApiTask] that validates the otp which was sent as a part +///of [RegisterAtsign] to email provided in args +///throws [AtException] with concerned message which was encountered in the +///HTTP GET/POST request +class ValidateOtp extends RegisterApiTask { + @override + void init(Map params, OnboardingUtil registerUtil) { + params['confirmation'] = 'false'; + this.params = params; + this.registerUtil = registerUtil; + result.data = HashMap(); + } + + @override + Future run() async { + if (params['otp'] == null) { + params['otp'] = registerUtil.getVerificationCodeFromUser(); + } + stdout.writeln('[Information] Validating your verification code...'); + try { + String apiResponse = await registerUtil.validateOtp( + params['atsign']!, params['email']!, params['otp']!, + confirmation: params['confirmation']!, + authority: params['authority']!); + if (apiResponse == 'retry') { + stderr.writeln( + '[Unable to proceed] The verification code you entered is either invalid or expired.\n' + ' Check your verification code and try again.'); + params['otp'] = registerUtil.getVerificationCodeFromUser(); + result.apiCallStatus = ApiCallStatus.retry; + result.exceptionMessage = + 'Incorrect otp entered 3 times. Max retries reached.'; + } else if (apiResponse == 'follow-up') { + params.update('confirmation', (value) => 'true'); + result.data['otp'] = params['otp']; + result.apiCallStatus = ApiCallStatus.retry; + } else if (apiResponse.startsWith("@")) { + result.data['cramkey'] = apiResponse.split(":")[1]; + stdout.writeln( + '[Information] Your cram secret: ${result.data['cramkey']}'); + stdout.writeln('[Success] Your atSign **@${params['atsign']}** has been' + ' successfully registered to ${params['email']}'); + result.apiCallStatus = ApiCallStatus.success; + } + } on Exception catch (e) { + result.exceptionMessage = e.toString(); + result.apiCallStatus = + shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; + } + return result; } } Future main(List args) async { Register register = Register(); - AtSignLogger.root_level = 'info'; + AtSignLogger.root_level = 'severe'; try { await register.main(args); - } on MaximumAtsignQuotaException { - stdout.writeln( - '[Unable to proceed] This email address already has 10 free atSigns associated with it.\n' - 'To register a new atSign to this email address, please log into the dashboard \'my.atsign.com/login\'.\n' - 'Remove at least 1 atSign from your account and then try again.\n' - 'Alternatively, you can retry this process with a different email address.'); - exit(0); } on FormatException catch (e) { if (e.toString().contains('Missing argument')) { stderr.writeln( '[Unable to run Register CLI] Please re-run with your email address'); stderr - .writeln('Usage: \'dart run bin/register_cli.dart -e email@email.com\''); + .writeln('Usage: \'dart run register_cli.dart -e email@email.com\''); exit(1); } else if (e.toString().contains('Could not find an option or flag')) { stderr @@ -91,11 +237,11 @@ Future main(List args) async { stderr.writeln('Cause: $e'); exit(3); } - } on AtException catch (e) { + } on AtOnboardingException catch (e) { stderr.writeln( '[Error] Failed getting an atsign. It looks like something went wrong on our side.\n' 'Please try again or contact support@atsign.com, quoting the text displayed below.'); - stderr.writeln('Cause: ${e.message} ExceptionType:${e.runtimeType}'); + stderr.writeln('Cause: $e ExceptionType:${e.runtimeType}'); exit(4); } on Exception catch (e) { if (e diff --git a/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart b/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart deleted file mode 100644 index b7b15295..00000000 --- a/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart +++ /dev/null @@ -1,42 +0,0 @@ -import '../../at_register.dart'; - -/// Processes tasks of type [RegisterTask] -/// Initialized with a params map that needs to be populated with - email and api host address -/// [add] method can be used to add tasks[RegisterTask] to the [processQueue] -/// [start] needs to be called after all required tasks are added to the [processQueue] -class RegistrationFlow { - List processQueue = []; - RegisterTaskResult result = RegisterTaskResult(); - late RegistrarApiCalls registrarApiCall; - RegisterParams params; - - RegistrationFlow(this.params, this.registrarApiCall); - - RegistrationFlow add(RegisterTask task) { - processQueue.add(task); - return this; - } - - /// ToDo: should return Result ? or it's fine to populate the params - Future start() async { - for (RegisterTask task in processQueue) { - task.init(params, registrarApiCall); - result = await task.run(); - if (RegistrarConstants.isDebugMode) { - task.logger.shout('Attempt: ${task.retryCount} | params[$params]'); - task.logger.shout('Result: $result'); - } - if (result.apiCallStatus == ApiCallStatus.retry) { - while ( - task.shouldRetry() && result.apiCallStatus == ApiCallStatus.retry) { - result = await task.retry(); - } - } - if (result.apiCallStatus == ApiCallStatus.success) { - params.addFromJson(result.data); - } else { - throw AtRegisterException(result.exceptionMessage!); - } - } - } -} diff --git a/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart b/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart index 0292a3a5..b2f3ee1e 100644 --- a/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart +++ b/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart @@ -2,7 +2,7 @@ import 'dart:core'; import 'package:at_chops/at_chops.dart'; import 'package:at_client/at_client.dart'; -import 'package:at_register/at_register.dart'; +import 'package:at_onboarding_cli/src/util/registrar_api_constants.dart'; class AtOnboardingPreference extends AtClientPreference { /// specify path of .atKeysFile containing encryption keys @@ -28,7 +28,7 @@ class AtOnboardingPreference extends AtClientPreference { bool skipSync = false; /// the hostName of the registrar which will be used to activate the atsign - String registrarUrl = RegistrarConstants.apiHostProd; + String registrarUrl = RegistrarApiConstants.apiHostProd; String? appName; diff --git a/packages/at_onboarding_cli/pubspec.yaml b/packages/at_onboarding_cli/pubspec.yaml index 59a67300..62f8521a 100644 --- a/packages/at_onboarding_cli/pubspec.yaml +++ b/packages/at_onboarding_cli/pubspec.yaml @@ -28,12 +28,6 @@ dependencies: at_server_status: ^1.0.4 at_utils: ^3.0.16 -dependency_overrides: - at_register: - path: /home/srie/Desktop/work/at_libraries/packages/at_register - at_client: - path: /home/srie/Desktop/at_client_sdk/packages/at_client - dev_dependencies: lints: ^2.1.0 test: ^1.24.2 diff --git a/packages/at_onboarding_cli/test/at_register_test.dart b/packages/at_onboarding_cli/test/at_register_test.dart deleted file mode 100644 index 207fad10..00000000 --- a/packages/at_onboarding_cli/test/at_register_test.dart +++ /dev/null @@ -1,316 +0,0 @@ -import 'package:at_register/at_register.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:test/test.dart'; - -class MockRegistrarApiCall extends Mock implements RegistrarApiAccessor {} - -void main() { - RegistrarApiAccessor mockRegistrarApiCall = MockRegistrarApiCall(); - - group('A group of tests to validate GetFreeAtsign', () { - setUp(() => resetMocktailState()); - - test('validate behaviour of GetFreeAtsign', () async { - when(() => mockRegistrarApiCall.getFreeAtSigns()) - .thenAnswer((invocation) => Future.value('@alice')); - - RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; - await RegistrationFlow(params, mockRegistrarApiCall) - .add(GetFreeAtsign()) - .start(); - expect(params.atsign, '@alice'); - }); - - test('validate behaviour of GetFreeAtsign - encounters exception', - () async { - when(() => mockRegistrarApiCall.getFreeAtSigns()) - .thenThrow(Exception('random exception')); - - RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; - GetFreeAtsign getFreeAtsign = GetFreeAtsign(); - - try { - await RegistrationFlow(params, mockRegistrarApiCall) - .add(getFreeAtsign) - .start(); - } on Exception catch (e) { - expect(e.runtimeType, AtRegisterException); - expect(e.toString().contains('random exception'), true); - } - expect(getFreeAtsign.retryCount, RegisterTask.maximumRetries); - expect(getFreeAtsign.result.apiCallStatus, ApiCallStatus.failure); - expect(getFreeAtsign.shouldRetry(), false); - expect(getFreeAtsign.retryCount, 3); - }); - - // test('fetch multiple atsigns using GetFreeAtsign', () async { - // String email = 'first-group-3@testland.test'; - // when(() => mockRegistrarApiCall.getFreeAtSigns()) - // .thenAnswer((invocation) => Future.value(['@alice', '@bob', '@charlie'])); - // - // RegisterParams params = RegisterParams()..email = email; - // GetFreeAtsign getFreeAtsignInstance = GetFreeAtsign(count: 3); - // getFreeAtsignInstance.init(params, mockRegistrarApiCall); - // RegisterTaskResult result = await getFreeAtsignInstance.run(); - // - // expect(result.data['atsign'], ''); - // }); - }); - - group('Group of tests to validate behaviour of RegisterAtsign', () { - setUp(() => resetMocktailState()); - - test('validate RegisterAtsign behaviour in RegistrationFlow', () async { - String atsign = '@bobby'; - String email = 'second-group@email'; - when(() => mockRegistrarApiCall.registerAtSign(atsign, email)) - .thenAnswer((_) => Future.value(true)); - - RegisterParams params = RegisterParams() - ..atsign = atsign - ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(); - await RegistrationFlow(params, mockRegistrarApiCall) - .add(registerAtsignTask) - .start(); - expect(registerAtsignTask.retryCount, 1); // 1 is the default retry count - // this task does not generate any new params. This test validates how RegistrationFlow - // processes the task when otp has been sent to user's email - // successful execution of this test would indicate that the process did not - // encounter any errors/exceptions - }); - - test('RegisterAtsign params reading and updating - positive case', - () async { - String atsign = '@bobby'; - String email = 'second-group@email'; - when(() => mockRegistrarApiCall.registerAtSign(atsign, email)) - .thenAnswer((_) => Future.value(true)); - - RegisterParams params = RegisterParams() - ..atsign = atsign - ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(); - registerAtsignTask.init(params, mockRegistrarApiCall); - RegisterTaskResult result = await registerAtsignTask.run(); - expect(result.apiCallStatus, ApiCallStatus.success); - expect(result.data['otpSent'], 'true'); - }); - - test('RegisterAtsign params reading and updating - negative case', - () async { - String atsign = '@bobby'; - String email = 'second-group@email'; - when(() => mockRegistrarApiCall.registerAtSign(atsign, email)) - .thenAnswer((_) => Future.value(false)); - - RegisterParams params = RegisterParams() - ..atsign = atsign - ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(); - registerAtsignTask.init(params, mockRegistrarApiCall); - RegisterTaskResult result = await registerAtsignTask.run(); - expect(result.apiCallStatus, ApiCallStatus.success); - expect(result.data['otpSent'], 'false'); - expect(registerAtsignTask.shouldRetry(), true); - }); - - test('verify behaviour of RegisterAtsign processing an exception', - () async { - String atsign = '@bobby'; - String email = 'second-group@email'; - when(() => mockRegistrarApiCall.registerAtSign(atsign, email)) - .thenThrow(Exception('another random exception')); - - RegisterParams params = RegisterParams() - ..atsign = atsign - ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(); - registerAtsignTask.init(params, mockRegistrarApiCall); - RegisterTaskResult result = await registerAtsignTask.run(); - expect(result.apiCallStatus, ApiCallStatus.retry); - expect(result.exceptionMessage, 'Exception: another random exception'); - expect(registerAtsignTask.shouldRetry(), true); - }); - - test( - 'verify behaviour of RegistrationFlow processing exception in RegisterAtsign', - () async { - String atsign = '@bobby'; - String email = 'second-group@email'; - when(() => mockRegistrarApiCall.registerAtSign(atsign, email)) - .thenThrow(Exception('another new random exception')); - - RegisterParams params = RegisterParams() - ..atsign = atsign - ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(); - try { - await RegistrationFlow(params, mockRegistrarApiCall) - .add(registerAtsignTask) - .start(); - } on Exception catch (e) { - print(e.toString()); - assert(e.toString().contains('another new random exception')); - } - expect(registerAtsignTask.retryCount, 3); - expect(registerAtsignTask.shouldRetry(), false); - }); - }); - - group('A group of tests to verify ValidateOtp task behaviour', () { - setUp(() => resetMocktailState()); - - test( - 'validate positive behaviour of ValidateOtp task - received cram in first call', - () async { - String atsign = '@charlie'; - String email = 'third-group@email'; - String otp = 'Abcd'; - String cram = 'craaaaaaaaaaaam'; - ValidateOtpResult validateOtpResult = ValidateOtpResult(); - validateOtpResult.taskStatus = ValidateOtpStatus.verified; - validateOtpResult.apiCallStatus = ApiCallStatus.success; - validateOtpResult.data = {RegistrarConstants.cramKey: '$atsign:$cram'}; - when(() => mockRegistrarApiCall.validateOtp(atsign, email, otp, - confirmation: false)) - .thenAnswer((invocation) => Future.value(validateOtpResult)); - - var params = RegisterParams() - ..atsign = atsign - ..confirmation = false - ..email = email - ..otp = otp; - - await RegistrationFlow(params, mockRegistrarApiCall) - .add(ValidateOtp()) - .start(); - - expect(params.cram, cram); - }); - - test( - 'validate positive behaviour of ValidateOtp task - need to followUp with confirmation set to true', - () async { - String atsign = '@charlie123'; - String email = 'third-group@email'; - String otp = 'bcde'; - String cram = 'craaaaaaaaaaaam1234'; - - var mockApiRespData = { - 'atsign': ['@old-atsign'], - 'newAtsign': atsign - }; - ValidateOtpResult validateOtpResult = ValidateOtpResult() - ..taskStatus = ValidateOtpStatus.followUp - ..apiCallStatus = ApiCallStatus.success - ..data = {'data': mockApiRespData}; - when(() => mockRegistrarApiCall.validateOtp(atsign, email, otp, - confirmation: false)) - .thenAnswer((invocation) => Future.value(validateOtpResult)); - - ValidateOtpResult validateOtpResult2 = ValidateOtpResult() - ..taskStatus = ValidateOtpStatus.verified - ..apiCallStatus = ApiCallStatus.success - ..data = {RegistrarConstants.cramKey: '$atsign:$cram'}; - when(() => mockRegistrarApiCall.validateOtp(atsign, email, otp, - confirmation: true)) - .thenAnswer((invocation) => Future.value(validateOtpResult2)); - - var params = RegisterParams() - ..atsign = atsign - ..confirmation = false - ..email = email - ..otp = otp; - // confirmation needs to be false for first call ? - await RegistrationFlow(params, mockRegistrarApiCall) - .add(ValidateOtp()) - .start(); - - expect(params.cram, cram); - expect(params.confirmation, true); - expect(validateOtpResult2.taskStatus, ValidateOtpStatus.verified); - }); - - test('validate behaviour of ValidateOtp task - 3 otp retries exhausted', - () async { - String atsign = '@charlie-otp-retry'; - String email = 'third-group-test-3@email'; - String otp = 'bcaa'; - String cram = 'craaaaaaaaaaaam'; - ValidateOtpResult validateOtpResult = ValidateOtpResult(); - validateOtpResult.taskStatus = ValidateOtpStatus.retry; - validateOtpResult.apiCallStatus = ApiCallStatus.success; - validateOtpResult.data = {RegistrarConstants.cramKey: '$atsign:$cram'}; - when(() => mockRegistrarApiCall.validateOtp(atsign, email, otp, - confirmation: any(named: "confirmation"))) - .thenAnswer((invocation) => Future.value(validateOtpResult)); - - var params = RegisterParams() - ..atsign = atsign - ..confirmation = false - ..email = email - ..otp = otp; - var validateOtpTask = ValidateOtp(); - expect( - () async => await RegistrationFlow(params, mockRegistrarApiCall) - .add(validateOtpTask) - .start(), - throwsA(ExhaustedVerificationCodeRetriesException)); - - expect(validateOtpTask.retryCount, 3); - }); - }); - - group('test to validate all 3 API calls in sequence', () { - setUp(() => resetMocktailState()); - - test('verify all 3 API calls at once', () async { - String atsign = '@lewis'; - String email = 'lewis44@gmail.com'; - String cram = 'craaaaaaaaaaaaam'; - String otp = 'Agbr'; - // mock for get-free-atsign - when(() => mockRegistrarApiCall.getFreeAtSigns()) - .thenAnswer((invocation) => Future.value(atsign)); - // mock for register-atsign - when(() => mockRegistrarApiCall.registerAtSign(atsign, email)) - .thenAnswer((_) => Future.value(true)); - // rest of the mocks for validate-otp - var mockApiRespData = { - 'atsign': ['@old-atsign'], - 'newAtsign': atsign - }; - ValidateOtpResult validateOtpResult = ValidateOtpResult() - ..taskStatus = ValidateOtpStatus.followUp - ..apiCallStatus = ApiCallStatus.success - ..data = {'data': mockApiRespData}; - when(() => mockRegistrarApiCall.validateOtp(atsign, email, otp, - confirmation: false)) - .thenAnswer((invocation) => Future.value(validateOtpResult)); - - ValidateOtpResult validateOtpResult2 = ValidateOtpResult() - ..taskStatus = ValidateOtpStatus.verified - ..apiCallStatus = ApiCallStatus.success - ..data = {RegistrarConstants.cramKey: '$atsign:$cram'}; - when(() => mockRegistrarApiCall.validateOtp(atsign, email, otp, - confirmation: true)) - .thenAnswer((invocation) => Future.value(validateOtpResult2)); - - RegisterParams params = RegisterParams() - ..email = email - ..otp = otp - ..confirmation = false; - - await RegistrationFlow(params, mockRegistrarApiCall) - .add(GetFreeAtsign()) - .add(RegisterAtsign()) - .add(ValidateOtp()) - .start(); - - expect(params.atsign, atsign); - expect(params.cram, cram); - expect(params.confirmation, true); - }); - }); -} diff --git a/packages/at_register/lib/at_register.dart b/packages/at_register/lib/at_register.dart index 5fe930f4..2e41b162 100644 --- a/packages/at_register/lib/at_register.dart +++ b/packages/at_register/lib/at_register.dart @@ -1,7 +1,6 @@ library; -export 'src/api-interactions/registrar_api_calls.dart'; -export 'src/api-interactions/registration_flow.dart'; +export 'src/api-interactions/registrar_api_accessor.dart'; export 'src/api-interactions/get_free_atsign.dart'; export 'src/api-interactions/register_atsign.dart'; export 'src/api-interactions/validate_otp.dart'; diff --git a/packages/at_register/lib/src/api-interactions/get_free_atsign.dart b/packages/at_register/lib/src/api-interactions/get_free_atsign.dart index 9eedd096..6cbee684 100644 --- a/packages/at_register/lib/src/api-interactions/get_free_atsign.dart +++ b/packages/at_register/lib/src/api-interactions/get_free_atsign.dart @@ -1,36 +1,42 @@ import '../../at_register.dart'; -///This is a [RegisterTask] that fetches a list of free atsigns +/// A [RegisterTask] that fetches a list of free atsigns. /// -///throws [AtException] with concerned message which was encountered in the -///HTTP GET/POST request +/// Throws an [AtException] with the concerned message encountered in the +/// HTTP GET/POST request. /// -/// e.g. -/// -/// `GetFreeAtsign getFreeAtsignInstance = GetFreeAtsign();` -/// -/// `await getFreeAtsignInstance.init(RegisterParams(), RegistrarApiCalls());` -/// -/// `RegisterTaskResult result = await getFreeAtsignInstance.run();` -/// -/// atsign stored in result.data['atsign'] +/// Example usage: +/// ```dart +/// GetFreeAtsign getFreeAtsignInstance = GetFreeAtsign(); +/// await getFreeAtsignInstance.init(RegisterParams(), RegistrarApiAccessor()); +/// RegisterTaskResult result = await getFreeAtsignInstance.run(); +/// ``` +/// The fetched atsign will be stored in result.data['atsign']. +/// ToDo: write down what will be the structure inside result.data{} class GetFreeAtsign extends RegisterTask { + GetFreeAtsign(super.registerParams, {super.registrarApiAccessorInstance}); + @override String get name => 'GetFreeAtsignTask'; @override - Future run() async { - logger.info('Getting your randomly generated free atSign…'); + Future run({bool allowRetry = false}) async { + logger.info('Getting a randomly generated free atSign...'); + RegisterTaskResult result = RegisterTaskResult(); try { - String atsign = await registrarApiCalls.getFreeAtSigns( + String atsign = await registrarApiAccessor.getFreeAtSigns( authority: RegistrarConstants.authority); + logger.info('Fetched free atsign: $atsign'); result.data['atsign'] = atsign; result.apiCallStatus = ApiCallStatus.success; } on Exception catch (e) { - result.exceptionMessage = e.toString(); + if (!allowRetry) { + throw AtRegisterException(e.toString()); + } result.apiCallStatus = shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; + result.exceptionMessage = e.toString(); } return result; } diff --git a/packages/at_register/lib/src/api-interactions/register_atsign.dart b/packages/at_register/lib/src/api-interactions/register_atsign.dart index 59525a38..91b82ce7 100644 --- a/packages/at_register/lib/src/api-interactions/register_atsign.dart +++ b/packages/at_register/lib/src/api-interactions/register_atsign.dart @@ -1,32 +1,35 @@ import '../../at_register.dart'; -/// User needs to select an atsign from the list fetched in [GetFreeAtsign]. +/// User selects an atsign from the list fetched in [GetFreeAtsign]. /// -/// Registers the selected atsign to the email provided through [RegisterParams] +/// Registers the selected atsign to the email provided through [RegisterParams]. /// -/// sets [RegisterTaskResult.apiCallStatus] if the HTTP GET/POST request gets any response other than STATUS_OK -/// -/// Note: Provide an atsign through [RegisterParams] if it is not ideal to read -/// user choice through [stdin] +/// Sets [RegisterTaskResult.apiCallStatus] if the HTTP GET/POST request gets any response other than STATUS_OK. class RegisterAtsign extends RegisterTask { + RegisterAtsign(super.registerParams, {super.registrarApiAccessorInstance}); + @override String get name => 'RegisterAtsignTask'; @override - Future run() async { + Future run({bool allowRetry = false}) async { logger.info('Sending verification code to: ${registerParams.email}'); + RegisterTaskResult result = RegisterTaskResult(); try { - result.data['otpSent'] = (await registrarApiCalls.registerAtSign( + result.data['otpSent'] = (await registrarApiAccessor.registerAtSign( registerParams.atsign!, registerParams.email!, authority: RegistrarConstants.authority)) .toString(); logger.info('Verification code sent to: ${registerParams.email}'); - result.apiCallStatus = ApiCallStatus.success; } on Exception catch (e) { - result.exceptionMessage = e.toString(); + if (!allowRetry) { + throw AtRegisterException(e.toString()); + } result.apiCallStatus = shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; + result.exceptionMessage = e.toString(); } + result.apiCallStatus = ApiCallStatus.success; return result; } } diff --git a/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart b/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart index 5b112a04..6edcf4ee 100644 --- a/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart +++ b/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart @@ -10,15 +10,14 @@ import '../../at_register.dart'; /// Contains methods that actually perform the RegistrarAPI calls /// and handle/process the response class RegistrarApiAccessor { - AtSignLogger logger = AtSignLogger('AtRegister'); + AtSignLogger logger = AtSignLogger('RegistrarApiAccessor'); /// Returns a Future> containing free available atSigns /// based on [count] provided as input. Future getFreeAtSigns( {String authority = RegistrarConstants.apiHostProd}) async { - http.Response response; - response = await ApiUtil.getRequest( - authority, RegistrarConstants.pathGetFreeAtSign); + http.Response response = await ApiUtil.getRequest( + authority, RegistrarConstants.getFreeAtSignApiPath); if (response.statusCode == 200) { String? atsign = jsonDecode(response.body)['data']['atsign']; if (atsign != null) { @@ -41,14 +40,14 @@ class RegistrarApiAccessor { /// of verificationCode/otp will take place in [validateOtp] Future registerAtSign(String atSign, String email, {oldEmail, String authority = RegistrarConstants.apiHostProd}) async { - http.Response response = await ApiUtil.postRequest( - authority, RegistrarConstants.pathRegisterAtSign, { + final response = await ApiUtil.postRequest( + authority, RegistrarConstants.registerAtSignApiPath, { 'atsign': atSign, 'email': email, 'oldEmail': oldEmail, }); if (response.statusCode == 200) { - Map jsonDecoded = jsonDecode(response.body); + final jsonDecoded = jsonDecode(response.body) as Map; bool sentSuccessfully = jsonDecoded['message'].toLowerCase().contains('success'); return sentSuccessfully; @@ -84,28 +83,28 @@ class RegistrarApiAccessor { Future validateOtp(String atSign, String email, String otp, {bool confirmation = true, String authority = RegistrarConstants.apiHostProd}) async { - http.Response response = await ApiUtil.postRequest( - authority, RegistrarConstants.pathValidateOtp, { + final response = await ApiUtil.postRequest( + authority, RegistrarConstants.validateOtpApiPath, { 'atsign': atSign, 'email': email, 'otp': otp, 'confirmation': confirmation.toString(), }); - ValidateOtpResult validateOtpResult = ValidateOtpResult(); - Map jsonDecodedResponse; if (response.statusCode == 200) { - validateOtpResult.data = {}; - jsonDecodedResponse = jsonDecode(response.body); + final validateOtpResult = ValidateOtpResult(); + // is this needed ? + final jsonDecodedResponse = jsonDecode(response.body); if (jsonDecodedResponse.containsKey('data')) { - validateOtpResult.data.addAll(jsonDecodedResponse['data']); + validateOtpResult.data + .addAll(jsonDecodedResponse['data'] as Map); } _processValidateOtpApiResponse(jsonDecodedResponse, validateOtpResult); + return validateOtpResult; } else { throw AtRegisterException( - '${response.statusCode} ${response.reasonPhrase}'); + 'Failed to Validate VerificationCode | ${response.statusCode} ${response.reasonPhrase}'); } - return validateOtpResult; } /// processes API response for [validateOtp] call and populates [result] @@ -114,8 +113,8 @@ class RegistrarApiAccessor { (responseJson['message'].toString().toLowerCase()) == 'verified') && responseJson.containsKey('cramkey')) { result.taskStatus = ValidateOtpStatus.verified; - result.data[RegistrarConstants.cramKey] = - responseJson[RegistrarConstants.cramKey]; + result.data[RegistrarConstants.cramKeyName] = + responseJson[RegistrarConstants.cramKeyName]; } else if (responseJson.containsKey('data') && result.data.containsKey('newAtsign')) { result.taskStatus = ValidateOtpStatus.followUp; @@ -142,9 +141,9 @@ class RegistrarApiAccessor { /// 2) Invalid atsign Future requestAuthenticationOtp(String atsign, {String authority = RegistrarConstants.apiHostProd}) async { - http.Response response = await ApiUtil.postRequest(authority, + final response = await ApiUtil.postRequest(authority, RegistrarConstants.requestAuthenticationOtpPath, {'atsign': atsign}); - String apiResponseMessage = jsonDecode(response.body)['message']; + final apiResponseMessage = jsonDecode(response.body)['message']; if (response.statusCode == 200) { if (apiResponseMessage.contains('Sent Successfully')) { logger.info( @@ -165,11 +164,11 @@ class RegistrarApiAccessor { /// Throws exception in the following cases: 1) HTTP 400 BAD_REQUEST Future getCramKey(String atsign, String verificationCode, {String authority = RegistrarConstants.apiHostProd}) async { - http.Response response = await ApiUtil.postRequest( + final response = await ApiUtil.postRequest( authority, RegistrarConstants.getCramKeyWithOtpPath, {'atsign': atsign, 'otp': verificationCode}); - Map jsonDecodedBody = jsonDecode(response.body); + final jsonDecodedBody = jsonDecode(response.body) as Map; if (response.statusCode == 200) { if (jsonDecodedBody['message'] == 'Verified') { String cram = jsonDecodedBody['cramkey']; @@ -190,7 +189,7 @@ class RegistrarApiAccessor { /// 2) fetch the CRAM key from registrar using the verification code Future getCramUsingOtp(String atsign, String registrarUrl) async { await requestAuthenticationOtp(atsign, authority: registrarUrl); - return await getCramKey(atsign, ApiUtil.getVerificationCodeFromUser(), + return await getCramKey(atsign, ApiUtil.readCliVerificationCode(), authority: registrarUrl); } } diff --git a/packages/at_register/lib/src/api-interactions/validate_otp.dart b/packages/at_register/lib/src/api-interactions/validate_otp.dart index 2a4393b6..20dae020 100644 --- a/packages/at_register/lib/src/api-interactions/validate_otp.dart +++ b/packages/at_register/lib/src/api-interactions/validate_otp.dart @@ -1,79 +1,90 @@ -import 'dart:collection'; - import 'package:at_commons/at_commons.dart'; import 'package:at_utils/at_utils.dart'; import '../../at_register.dart'; -///This is a [RegisterTask] that validates the otp which was sent as a part -///of [RegisterAtsign] to email provided in args -///throws [AtException] with concerned message which was encountered in the -///HTTP GET/POST request +/// Task for validating the verification_code sent as part of the registration process. class ValidateOtp extends RegisterTask { - @override - String get name => 'ValidateOtpTask'; + ValidateOtp(super.registerParams, + {super.registrarApiAccessorInstance, bool confirmation = false}); @override - void init( - RegisterParams registerParams, RegistrarApiCalls registrarApiCalls) { - this.registerParams = registerParams; - this.registrarApiCalls = registrarApiCalls; - this.registerParams.confirmation = false; - result.data = HashMap(); - logger = AtSignLogger(name); - } + String get name => 'ValidateOtpTask'; @override - Future run() async { - registerParams.otp ??= ApiUtil.getVerificationCodeFromUser(); - logger.info('Validating your verification code...'); + Future run({bool allowRetry = false}) async { + RegisterTaskResult result = RegisterTaskResult(); + if (registerParams.otp.isNullOrEmpty) { + throw InvalidVerificationCodeException( + 'Verification code cannot be null'); + } try { + logger + .info('Validating verification code for ${registerParams.atsign}...'); registerParams.atsign = AtUtils.fixAtSign(registerParams.atsign!); - ValidateOtpResult validateOtpApiResult = - await registrarApiCalls.validateOtp(registerParams.atsign!, - registerParams.email!, registerParams.otp!, - confirmation: registerParams.confirmation, - authority: RegistrarConstants.authority); - if (validateOtpApiResult.taskStatus == ValidateOtpStatus.retry) { - logger.severe('Invalid or expired verification code.' - ' Check your verification code and try again.'); + final validateOtpApiResult = await registrarApiAccessor.validateOtp( + registerParams.atsign!, + registerParams.email!, + registerParams.otp!, + confirmation: registerParams.confirmation, + authority: RegistrarConstants.authority, + ); - /// ToDo: how can an overrided ApiUtil be injected here - registerParams.otp = ApiUtil.getVerificationCodeFromUser(); - result.apiCallStatus = - shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; - result.exceptionMessage = - 'Incorrect otp entered 3 times. Max retries reached.'; - } else if (validateOtpApiResult.taskStatus == - ValidateOtpStatus.followUp) { - registerParams.confirmation = true; - result.data['otp'] = registerParams.otp; - result.apiCallStatus = ApiCallStatus.retry; - } else if (validateOtpApiResult.taskStatus == - ValidateOtpStatus.verified) { - result.data[RegistrarConstants.cramKey] = - validateOtpApiResult.data[RegistrarConstants.cramKey].split(":")[1]; + switch (validateOtpApiResult.taskStatus) { + case ValidateOtpStatus.retry: + if (!allowRetry) { + throw InvalidVerificationCodeException( + 'Verification Failed: Incorrect verification code provided'); + } else { + logger.warning('Invalid or expired verification code. Retrying...'); + registerParams.otp ??= ApiUtil + .readCliVerificationCode(); // retry reading otp from user through stdin + result.apiCallStatus = + shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; + result.exceptionMessage = + 'Verification Failed: Incorrect verification code provided. Please retry the process again'; + } + break; - logger.info('Your cram secret: ${result.data['cramkey']}'); - logger.shout('Your atSign **@${registerParams.atsign}** has been' - ' successfully registered to ${registerParams.email}'); - result.apiCallStatus = ApiCallStatus.success; - } else if (validateOtpApiResult.taskStatus == ValidateOtpStatus.failure) { - result.apiCallStatus = ApiCallStatus.failure; - result.exceptionMessage = validateOtpApiResult.exceptionMessage; + case ValidateOtpStatus.followUp: + registerParams.confirmation = true; + result.data['otp'] = registerParams.otp; + result.apiCallStatus = ApiCallStatus.retry; + logger.finer( + 'Provided email has existing atsigns, please select one atsign and retry this task'); + break; + + case ValidateOtpStatus.verified: + result.data[RegistrarConstants.cramKeyName] = validateOtpApiResult + .data[RegistrarConstants.cramKeyName] + .split(":")[1]; + logger.info('Cram secret verified.'); + logger.shout('Successful registration for ${registerParams.email}'); + result.apiCallStatus = ApiCallStatus.success; + break; + + case ValidateOtpStatus.failure: + result.apiCallStatus = ApiCallStatus.failure; + result.exceptionMessage = validateOtpApiResult.exceptionMessage; + break; + case null: + result.apiCallStatus = ApiCallStatus.failure; + result.exceptionMessage = validateOtpApiResult.exceptionMessage; + break; } } on MaximumAtsignQuotaException { rethrow; } on ExhaustedVerificationCodeRetriesException { rethrow; - } on AtException catch (e) { - result.exceptionMessage = e.message; + } on InvalidVerificationCodeException { + rethrow; + } catch (e) { + if (!allowRetry) { + throw AtRegisterException(e.toString()); + } result.apiCallStatus = shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; - } on Exception catch (e) { result.exceptionMessage = e.toString(); - result.apiCallStatus = - shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; } return result; } diff --git a/packages/at_register/lib/src/config/registrar_constants.dart b/packages/at_register/lib/src/config/registrar_constants.dart index 66461df7..7834055c 100644 --- a/packages/at_register/lib/src/config/registrar_constants.dart +++ b/packages/at_register/lib/src/config/registrar_constants.dart @@ -3,14 +3,14 @@ class RegistrarConstants { static const String apiHostProd = 'my.atsign.com'; static const String apiHostStaging = 'my.atsign.wtf'; - /// Select [Prod/Dev] + /// Select [Prod/Staging] /// Change to [apiHostStaging] to use AtRegister in a staging env - static const String authority = apiHostStaging; + static const String authority = apiHostProd; /// API Paths - static const String pathGetFreeAtSign = '/api/app/v3/get-free-atsign'; - static const String pathRegisterAtSign = '/api/app/v3/register-person'; - static const String pathValidateOtp = '/api/app/v3/validate-person'; + static const String getFreeAtSignApiPath = '/api/app/v3/get-free-atsign'; + static const String registerAtSignApiPath = '/api/app/v3/register-person'; + static const String validateOtpApiPath = '/api/app/v3/validate-person'; static const String requestAuthenticationOtpPath = '/api/app/v3/authenticate/atsign'; static const String getCramKeyWithOtpPath = @@ -24,5 +24,6 @@ class RegistrarConstants { /// the inner working of Register_cli static const bool isDebugMode = true; - static const String cramKey = 'cramkey'; + static const String cramKeyName = 'cramkey'; + static const String atsignName = 'atsign'; } diff --git a/packages/at_register/lib/src/util/api_util.dart b/packages/at_register/lib/src/util/api_util.dart index 6e68bc80..5c531453 100644 --- a/packages/at_register/lib/src/util/api_util.dart +++ b/packages/at_register/lib/src/util/api_util.dart @@ -46,7 +46,8 @@ class ApiUtil { }, ); if (RegistrarConstants.isDebugMode) { - AtSignLogger('AtRegister').shout('Sent request to url: $uri | Request Body: $body'); + AtSignLogger('AtRegister') + .shout('Sent request to url: $uri | Request Body: $body'); AtSignLogger('AtRegister').shout('Got Response: ${response.body}'); } return response; @@ -64,15 +65,15 @@ class ApiUtil { } return false; } - - static String formatExceptionMessage(String exception){ + + static String formatExceptionMessage(String exception) { return exception.replaceAll('Exception:', ''); } /// Method to get verification code from user input /// validates code locally and retries taking user input if invalid /// Returns only when the user has provided a 4-length String only containing numbers and alphabets - static String getVerificationCodeFromUser() { + static String readCliVerificationCode() { String? otp; stdout.writeln( '[Action Required] Enter your verification code: (verification code is not case-sensitive)'); @@ -106,4 +107,11 @@ class ApiUtil { return atsigns[choice]; } } + + static String formatException(String message) { + if (message.contains('Exception: ')) { + return message.replaceAll('Exception: ', ''); + } + return message; + } } diff --git a/packages/at_register/lib/src/util/register_params.dart b/packages/at_register/lib/src/util/register_params.dart index b1538437..806b8d7b 100644 --- a/packages/at_register/lib/src/util/register_params.dart +++ b/packages/at_register/lib/src/util/register_params.dart @@ -5,11 +5,9 @@ class RegisterParams { String? atsign; String? email; String? oldEmail; - bool confirmation = true; + bool confirmation = false; String? otp; String? cram; - List? fetchedAtsignsList; - /// Populates the current instance of [RegisterParams] using the fields from the json /// @@ -31,7 +29,7 @@ class RegisterParams { if (json.containsKey('oldEmail')) { oldEmail = json['oldEmail']; } - if (json.containsKey(RegistrarConstants.cramKey)) { + if (json.containsKey(RegistrarConstants.cramKeyName)) { cram = json['cramkey']; } } diff --git a/packages/at_register/lib/src/util/register_task.dart b/packages/at_register/lib/src/util/register_task.dart index 6ce67971..97ab0f5a 100644 --- a/packages/at_register/lib/src/util/register_task.dart +++ b/packages/at_register/lib/src/util/register_task.dart @@ -1,5 +1,3 @@ -import 'dart:collection'; - import 'package:at_register/at_register.dart'; import 'package:at_utils/at_logger.dart'; @@ -15,30 +13,29 @@ abstract class RegisterTask { late RegisterParams registerParams; - late RegistrarApiCalls registrarApiCalls; + late RegistrarApiAccessor _registrarApiAccessor; + RegistrarApiAccessor get registrarApiAccessor => _registrarApiAccessor; late AtSignLogger logger; - RegisterTaskResult result = RegisterTaskResult(); - - /// Initializes the Task object with necessary parameters - /// [params] is a map that contains necessary data to complete atsign - /// registration process - void init( - RegisterParams registerParams, RegistrarApiCalls registrarApiCalls) { - this.registerParams = registerParams; - this.registrarApiCalls = registrarApiCalls; - result.data = HashMap(); + RegisterTask(this.registerParams, + {RegistrarApiAccessor? registrarApiAccessorInstance}) { + _registrarApiAccessor = + registrarApiAccessorInstance ?? RegistrarApiAccessor(); logger = AtSignLogger(name); } /// Implementing classes need to implement required logic in this method to /// complete their sub-process in the AtSign registration process - Future run(); + /// + /// If [allowRetry] is set to true, the task will rethrow all exceptions + /// otherwise will catch the exception and store the exception message in + /// [RegisterTaskResult.exceptionMessage] + Future run({bool allowRetry = true}); - Future retry() async { + Future retry({bool allowRetry = true}) async { increaseRetryCount(); - return await run(); + return await run(allowRetry: allowRetry); } /// Increases retry count by 1 diff --git a/packages/at_register/lib/src/util/register_task_result.dart b/packages/at_register/lib/src/util/register_task_result.dart index 591f14d8..8f9f3e71 100644 --- a/packages/at_register/lib/src/util/register_task_result.dart +++ b/packages/at_register/lib/src/util/register_task_result.dart @@ -1,7 +1,9 @@ +import 'dart:collection'; + import 'api_call_status.dart'; class RegisterTaskResult { - dynamic data; + Map data = HashMap(); late ApiCallStatus apiCallStatus; diff --git a/packages/at_register/lib/src/util/validate_otp_task_result.dart b/packages/at_register/lib/src/util/validate_otp_task_result.dart index d3cbfb2f..09b74822 100644 --- a/packages/at_register/lib/src/util/validate_otp_task_result.dart +++ b/packages/at_register/lib/src/util/validate_otp_task_result.dart @@ -2,4 +2,4 @@ import '../../at_register.dart'; class ValidateOtpResult extends RegisterTaskResult { ValidateOtpStatus? taskStatus; -} \ No newline at end of file +} diff --git a/packages/at_register/test/at_register_allow_retry_false_test.dart b/packages/at_register/test/at_register_allow_retry_false_test.dart index e7160bc9..b4adedfe 100644 --- a/packages/at_register/test/at_register_allow_retry_false_test.dart +++ b/packages/at_register/test/at_register_allow_retry_false_test.dart @@ -1,5 +1,3 @@ -import 'dart:collection'; - import 'package:at_register/at_register.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; @@ -16,11 +14,12 @@ void main() { test('validate behaviour of GetFreeAtsign', () async { when(() => mockRegistrarApiAccessor.getFreeAtSigns()) .thenAnswer((invocation) => Future.value('@alice')); - print(mockRegistrarApiAccessor.getFreeAtSigns()); + RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; GetFreeAtsign getFreeAtsign = GetFreeAtsign(params, registrarApiAccessorInstance: mockRegistrarApiAccessor); final result = await getFreeAtsign.run(); + expect(result.data[RegistrarConstants.atsignName], '@alice'); }); @@ -40,7 +39,7 @@ void main() { expect(e.toString().contains('random exception'), true); exceptionFlag = true; } - expect(getFreeAtsign.shouldRetry(), true); + // validates that exception was thrown expect(exceptionFlag, true); }); }); @@ -48,25 +47,6 @@ void main() { group('Group of tests to validate behaviour of RegisterAtsign', () { setUp(() => resetMocktailState()); - test('validate RegisterAtsign behaviour in RegistrationFlow', () async { - String atsign = '@bobby'; - String email = 'second-group@email'; - when(() => mockRegistrarApiAccessor.registerAtSign(atsign, email)) - .thenAnswer((_) => Future.value(true)); - - RegisterParams params = RegisterParams() - ..atsign = atsign - ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); - await registerAtsignTask.run(); - expect(registerAtsignTask.retryCount, 1); // 1 is the default retry count - // this task does not generate any new params. This test validates how RegistrationFlow - // processes the task when otp has been sent to user's email - // successful execution of this test would indicate that the process did not - // encounter any errors/exceptions - }); - test('RegisterAtsign params reading and updating - positive case', () async { String atsign = '@bobby'; @@ -80,6 +60,7 @@ void main() { RegisterAtsign registerAtsignTask = RegisterAtsign(params, registrarApiAccessorInstance: mockRegistrarApiAccessor); RegisterTaskResult result = await registerAtsignTask.run(); + expect(result.apiCallStatus, ApiCallStatus.success); expect(result.data['otpSent'], 'true'); }); @@ -97,9 +78,9 @@ void main() { RegisterAtsign registerAtsignTask = RegisterAtsign(params, registrarApiAccessorInstance: mockRegistrarApiAccessor); RegisterTaskResult result = await registerAtsignTask.run(); + expect(result.apiCallStatus, ApiCallStatus.success); expect(result.data['otpSent'], 'false'); - expect(registerAtsignTask.shouldRetry(), true); }); test('verify behaviour of RegisterAtsign processing an exception', @@ -123,7 +104,7 @@ void main() { expect(e.toString().contains('random exception'), true); exceptionFlag = true; } - expect(registerAtsignTask.shouldRetry(), true); + // validates that an exception was thrown expect(exceptionFlag, true); }); @@ -144,11 +125,9 @@ void main() { try { await registerAtsignTask.run(); } on Exception catch (e) { - print(e.toString()); assert(e.toString().contains('another new random exception')); exceptionFlag = true; } - expect(registerAtsignTask.retryCount, 1); expect(exceptionFlag, true); }); }); @@ -224,33 +203,23 @@ void main() { ValidateOtp validateOtpTask = ValidateOtp(params, registrarApiAccessorInstance: mockRegistrarApiAccessor); RegisterTaskResult result = await validateOtpTask.run(); - expect(params.confirmation, true); + expect(params.confirmation, true); // confirmation set to true by the Task expect(result.apiCallStatus, ApiCallStatus.retry); - expect(validateOtpResult2.taskStatus, ValidateOtpStatus.verified); // The above case is when an email has already existing atsigns, select an atsign // from the list and retry the task with confirmation set to 'true' + // mimic-ing a user selecting an atsign and proceeding ahead params.atsign = atsign2; result = await validateOtpTask.run(); expect(result.apiCallStatus, ApiCallStatus.success); expect(result.data[RegistrarConstants.cramKeyName], cram); }); - test('validate behaviour of ValidateOtp task - 3 otp retries exhausted', + test('validate behaviour of ValidateOtp task - null or empty otp', () async { String atsign = '@charlie-otp-retry'; String email = 'third-group-test-3@email'; - String otp = 'bcaa'; - String cram = 'craaaaaaaaaaaam'; - ValidateOtpResult validateOtpResult = ValidateOtpResult(); - validateOtpResult.taskStatus = ValidateOtpStatus.retry; - validateOtpResult.apiCallStatus = ApiCallStatus.success; - validateOtpResult.data = { - RegistrarConstants.cramKeyName: '$atsign:$cram' - }; - when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, - confirmation: any(named: "confirmation"))) - .thenAnswer((invocation) => Future.value(validateOtpResult)); + String? otp; // invalid null otp var params = RegisterParams() ..atsign = atsign @@ -261,61 +230,65 @@ void main() { registrarApiAccessorInstance: mockRegistrarApiAccessor); expect(() async => await validateOtpTask.run(), - throwsA(ExhaustedVerificationCodeRetriesException)); + throwsA(predicate((e) => e is InvalidVerificationCodeException))); - expect(validateOtpTask.retryCount, 1); + params.otp = ''; + expect(() async => await validateOtpTask.run(), + throwsA(predicate((e) => e is InvalidVerificationCodeException))); }); - }); - group('test to validate all 3 API calls in sequence', () { - setUp(() => resetMocktailState()); - // - // test('verify all 3 API calls in sequence', () async { - // String atsign = '@lewis'; - // String email = 'lewis44@gmail.com'; - // String cram = 'craaaaaaaaaaaaam'; - // String otp = 'Agbr'; - // // mock for get-free-atsign - // when(() => mockRegistrarApiAccessor.getFreeAtSigns()) - // .thenAnswer((invocation) => Future.value(atsign)); - // // mock for register-atsign - // when(() => mockRegistrarApiAccessor.registerAtSign(atsign, email)) - // .thenAnswer((_) => Future.value(true)); - // // rest of the mocks for validate-otp - // var mockApiRespData = { - // 'atsign': ['@old-atsign'], - // 'newAtsign': atsign - // }; - // ValidateOtpResult validateOtpResult = ValidateOtpResult() - // ..taskStatus = ValidateOtpStatus.followUp - // ..apiCallStatus = ApiCallStatus.success - // ..data = {'data': mockApiRespData}; - // when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, - // confirmation: false)) - // .thenAnswer((invocation) => Future.value(validateOtpResult)); - // - // ValidateOtpResult validateOtpResult2 = ValidateOtpResult() - // ..taskStatus = ValidateOtpStatus.verified - // ..apiCallStatus = ApiCallStatus.success - // ..data = {RegistrarConstants.cramKey: '$atsign:$cram'}; - // when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, - // confirmation: true)) - // .thenAnswer((invocation) => Future.value(validateOtpResult2)); - // - // RegisterParams params = RegisterParams() - // ..email = email - // ..otp = otp - // ..confirmation = false; - // - // await RegistrationFlow(params, mockRegistrarApiAccessor) - // .add(GetFreeAtsign()) - // .add(RegisterAtsign()) - // .add(ValidateOtp()) - // .start(); - // - // expect(params.atsign, atsign); - // expect(params.cram, cram); - // expect(params.confirmation, true); - // }); + test('validate behaviour of ValidateOtp task - incorrect otp', () async { + String atsign = '@charlie-otp-incorrect'; + String email = 'third-group-test-3-3@email'; + String otp = 'otpp'; // invalid otp + + var params = RegisterParams() + ..atsign = atsign + ..confirmation = false + ..email = email + ..otp = otp; + + ValidateOtpResult validateOtpResult = ValidateOtpResult() + ..taskStatus = ValidateOtpStatus.retry + ..apiCallStatus = ApiCallStatus.success + ..exceptionMessage = 'incorrect otp'; + when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, + confirmation: false)) + .thenAnswer((invocation) => Future.value(validateOtpResult)); + + ValidateOtp validateOtpTask = ValidateOtp(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + + expect( + () async => await validateOtpTask.run(), + throwsA(predicate((e) => + e is InvalidVerificationCodeException && + e.message.contains('incorrect otp')))); + }); + + test( + 'validate behaviour of ValidateOtp task - maximum free atsign limit reached', + () async { + String atsign = '@charlie-otp-incorrect'; + String email = 'third-group-test-3-3@email'; + String otp = 'otpp'; + + var params = RegisterParams() + ..atsign = atsign + ..confirmation = false + ..email = email + ..otp = otp; + + when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, + confirmation: false)) + .thenThrow( + MaximumAtsignQuotaException('maximum free atsign limit reached')); + + ValidateOtp validateOtpTask = ValidateOtp(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + + expect(() async => await validateOtpTask.run(), + throwsA(predicate((e) => e is MaximumAtsignQuotaException))); + }); }); } diff --git a/packages/at_register/test/at_register_allow_retry_true_test.dart b/packages/at_register/test/at_register_allow_retry_true_test.dart index e69de29b..bd34341c 100644 --- a/packages/at_register/test/at_register_allow_retry_true_test.dart +++ b/packages/at_register/test/at_register_allow_retry_true_test.dart @@ -0,0 +1,265 @@ +import 'package:at_register/at_register.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockRegistrarApiAccessor extends Mock implements RegistrarApiAccessor {} + +/// This test file validates the behaviour of implementations of [RegisterTask] +/// with optional param of [RegisterTask.run] 'allowRetry' set to true. +/// +/// Expected behaviour with this param set to true is that the task handles the +/// exceptions and returns a valid [RegisterTaskResult] object +void main() { + MockRegistrarApiAccessor mockRegistrarApiAccessor = + MockRegistrarApiAccessor(); + + group('A group of tests to validate GetFreeAtsign', () { + setUp(() => resetMocktailState()); + + test('validate behaviour of GetFreeAtsign', () async { + when(() => mockRegistrarApiAccessor.getFreeAtSigns()) + .thenAnswer((invocation) => Future.value('@alice')); + + RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; + GetFreeAtsign getFreeAtsign = GetFreeAtsign(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + final result = await getFreeAtsign.run(allowRetry: true); + expect(result.data[RegistrarConstants.atsignName], '@alice'); + }); + + test('validate behaviour of GetFreeAtsign - encounters exception', + () async { + String testExceptionMessage = 'random exception'; + when(() => mockRegistrarApiAccessor.getFreeAtSigns()) + .thenThrow(Exception(testExceptionMessage)); + + RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; + GetFreeAtsign getFreeAtsign = GetFreeAtsign(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + RegisterTaskResult? result = await getFreeAtsign.run(allowRetry: true); + + expect(result.apiCallStatus, ApiCallStatus.retry); + assert(result.exceptionMessage!.contains(testExceptionMessage)); + }); + }); + + group('Group of tests to validate behaviour of RegisterAtsign', () { + setUp(() => resetMocktailState()); + + test('RegisterAtsign params reading and updating - positive case', + () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + when(() => mockRegistrarApiAccessor.registerAtSign(atsign, email)) + .thenAnswer((_) => Future.value(true)); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + RegisterAtsign registerAtsignTask = RegisterAtsign(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + RegisterTaskResult result = + await registerAtsignTask.run(allowRetry: true); + expect(result.apiCallStatus, ApiCallStatus.success); + expect(result.data['otpSent'], 'true'); + }); + + test('RegisterAtsign params reading and updating - negative case', + () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + when(() => mockRegistrarApiAccessor.registerAtSign(atsign, email)) + .thenAnswer((_) => Future.value(false)); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + RegisterAtsign registerAtsignTask = RegisterAtsign(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + RegisterTaskResult result = + await registerAtsignTask.run(allowRetry: true); + + expect(result.apiCallStatus, ApiCallStatus.success); + expect(result.data['otpSent'], 'false'); + }); + + test('verify behaviour of RegisterAtsign processing an exception', + () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + String testException = 'another random exception'; + when(() => mockRegistrarApiAccessor.registerAtSign(atsign, email)) + .thenThrow(Exception(testException)); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + + RegisterAtsign registerAtsignTask = RegisterAtsign(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + RegisterTaskResult? result = + await registerAtsignTask.run(allowRetry: true); + + expect(registerAtsignTask.shouldRetry(), true); + assert(result.exceptionMessage!.contains(testException)); + }); + + test( + 'verify behaviour of RegistrationFlow processing exception in RegisterAtsign', + () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + String testExceptionMessage = 'another new random exception'; + when(() => mockRegistrarApiAccessor.registerAtSign(atsign, email)) + .thenThrow(Exception(testExceptionMessage)); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + RegisterAtsign registerAtsignTask = RegisterAtsign(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + + var result = await registerAtsignTask.run(allowRetry: true); + assert(result.exceptionMessage!.contains(testExceptionMessage)); + expect(registerAtsignTask.retryCount, 1); + }); + }); + + group('A group of tests to verify ValidateOtp task behaviour', () { + setUp(() => resetMocktailState()); + + test( + 'validate positive behaviour of ValidateOtp task - received cram in first call', + () async { + String atsign = '@charlie'; + String email = 'third-group@email'; + String otp = 'Abcd'; + String cram = 'craaaaaaaaaaaam'; + ValidateOtpResult validateOtpResult = ValidateOtpResult(); + validateOtpResult.taskStatus = ValidateOtpStatus.verified; + validateOtpResult.apiCallStatus = ApiCallStatus.success; + validateOtpResult.data = { + RegistrarConstants.cramKeyName: '$atsign:$cram' + }; + when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, + confirmation: false)) + .thenAnswer((invocation) => Future.value(validateOtpResult)); + + var params = RegisterParams() + ..atsign = atsign + ..confirmation = false + ..email = email + ..otp = otp; + + ValidateOtp validateOtpTask = ValidateOtp(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + RegisterTaskResult result = await validateOtpTask.run(allowRetry: true); + + expect(result.data[RegistrarConstants.cramKeyName], cram); + }); + + test( + 'validate positive behaviour of ValidateOtp task - need to followUp with confirmation set to true', + () async { + String atsign = '@charlie123'; + String atsign2 = '@cheesecake'; + String email = 'third-group@email'; + String otp = 'bcde'; + String cram = 'craaaaaaaaaaaam1234'; + + var mockApiRespData = { + 'atsign': ['@old-atsign'], + 'newAtsign': atsign + }; + ValidateOtpResult validateOtpResult = ValidateOtpResult() + ..taskStatus = ValidateOtpStatus.followUp + ..apiCallStatus = ApiCallStatus.success + ..data = {'data': mockApiRespData}; + when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, + confirmation: false)) + .thenAnswer((invocation) => Future.value(validateOtpResult)); + + ValidateOtpResult validateOtpResult2 = ValidateOtpResult() + ..taskStatus = ValidateOtpStatus.verified + ..apiCallStatus = ApiCallStatus.success + ..data = {RegistrarConstants.cramKeyName: '$atsign:$cram'}; + when(() => mockRegistrarApiAccessor.validateOtp(atsign2, email, otp, + confirmation: true)) + .thenAnswer((invocation) => Future.value(validateOtpResult2)); + + var params = RegisterParams() + ..atsign = atsign + ..confirmation = false // confirmation needs to be false for first call + ..email = email + ..otp = otp; + + ValidateOtp validateOtpTask = ValidateOtp(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + RegisterTaskResult result = await validateOtpTask.run(allowRetry: true); + expect(params.confirmation, + true); // confirmation set to true by RegisterTask + expect(result.apiCallStatus, ApiCallStatus.retry); + print(result.data); + + // The above case is when an email has already existing atsigns, select an atsign + // from the list and retry the task with confirmation set to 'true' + params.atsign = atsign2; + result = await validateOtpTask.run(allowRetry: true); + expect(result.apiCallStatus, ApiCallStatus.success); + expect(result.data[RegistrarConstants.cramKeyName], cram); + }); + + test('validate behaviour of ValidateOtp task - invalid OTP', () async { + String atsign = '@charlie-otp-retry'; + String email = 'third-group-test-3@email'; + String otp = 'bcaa'; + String cram = 'craaaaaaaaaaaam'; + + ValidateOtpResult validateOtpResult = ValidateOtpResult(); + validateOtpResult.taskStatus = ValidateOtpStatus.retry; + validateOtpResult.apiCallStatus = ApiCallStatus.success; + validateOtpResult.data = { + RegistrarConstants.cramKeyName: '$atsign:$cram' + }; + when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, + confirmation: any(named: "confirmation"))) + .thenAnswer((invocation) => Future.value(validateOtpResult)); + + var params = RegisterParams() + ..atsign = atsign + ..confirmation = false + ..email = email + ..otp = otp; + ValidateOtp validateOtpTask = ValidateOtp(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + + RegisterTaskResult result = await validateOtpTask.run(allowRetry: true); + expect(result.apiCallStatus, ApiCallStatus.retry); + }); + + test( + 'validate behaviour of ValidateOtp task - maximum free atsign limit reached', + () async { + String atsign = '@charlie-otp-incorrect'; + String email = 'third-group-test-3-3@email'; + String otp = 'otpp'; + + var params = RegisterParams() + ..atsign = atsign + ..confirmation = false + ..email = email + ..otp = otp; + + when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, + confirmation: false)) + .thenThrow( + MaximumAtsignQuotaException('maximum free atsign limit reached')); + + ValidateOtp validateOtpTask = ValidateOtp(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor); + + expect(() async => await validateOtpTask.run(allowRetry: true), + throwsA(predicate((e) => e is MaximumAtsignQuotaException))); + }); + }); +} From f4e67a059a633bbaa43e54af697196a5d8a373cf Mon Sep 17 00:00:00 2001 From: Srie Teja Date: Mon, 4 Mar 2024 20:18:02 +0530 Subject: [PATCH 09/16] build: introduce pubspec and changelog for at_register --- packages/at_register/CHANGELOG.md | 2 ++ packages/at_register/pubspec.yaml | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 packages/at_register/CHANGELOG.md create mode 100644 packages/at_register/pubspec.yaml diff --git a/packages/at_register/CHANGELOG.md b/packages/at_register/CHANGELOG.md new file mode 100644 index 00000000..6bf3e825 --- /dev/null +++ b/packages/at_register/CHANGELOG.md @@ -0,0 +1,2 @@ +## 1.0.0 +- Initial version. diff --git a/packages/at_register/pubspec.yaml b/packages/at_register/pubspec.yaml new file mode 100644 index 00000000..204bfde2 --- /dev/null +++ b/packages/at_register/pubspec.yaml @@ -0,0 +1,17 @@ +name: at_register +description: Package that has code to interact with the AtRegistrar API +version: 1.0.0 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.2.4 + +dependencies: + at_commons: ^4.0.1 + at_utils: ^3.0.16 + http: ^1.2.0 + mocktail: ^1.0.3 + +dev_dependencies: + lints: ^3.0.0 + test: ^1.24.0 From dcc6609b8626818188be79fb87785bad624c99b1 Mon Sep 17 00:00:00 2001 From: Srie Teja Date: Wed, 6 Mar 2024 03:38:38 +0530 Subject: [PATCH 10/16] reformat: some more code cleaning + readability changes --- packages/at_onboarding_cli/example/README.md | 40 --- .../lib/src/register_cli/register.dart | 204 +++----------- .../src/register_cli/registration_flow.dart | 41 +++ packages/at_onboarding_cli/pubspec.yaml | 7 + .../test/at_register_test.dart | 249 ++++++++++++++++++ packages/at_register/.gitignore | 7 + packages/at_register/LICENSE | 29 ++ packages/at_register/README.md | 81 ++++++ packages/at_register/analysis_options.yaml | 30 +++ .../example/at_register_example.dart | 32 +++ .../example/at_register_usage_explained.dart | 60 +++++ .../src/api-interactions/get_free_atsign.dart | 23 +- .../src/api-interactions/register_atsign.dart | 29 +- .../registrar_api_accessor.dart | 48 ++-- .../src/api-interactions/validate_otp.dart | 48 ++-- .../lib/src/config/registrar_constants.dart | 3 + .../lib/src/util/api_call_status.dart | 2 +- .../lib/src/util/register_task.dart | 33 ++- .../lib/src/util/register_task_result.dart | 2 +- .../src/util/validate_otp_task_result.dart | 2 + packages/at_register/pubspec.yaml | 8 +- .../at_register_allow_retry_false_test.dart | 77 ++++-- .../at_register_allow_retry_true_test.dart | 55 ++-- 23 files changed, 785 insertions(+), 325 deletions(-) delete mode 100644 packages/at_onboarding_cli/example/README.md create mode 100644 packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart create mode 100644 packages/at_onboarding_cli/test/at_register_test.dart create mode 100644 packages/at_register/.gitignore create mode 100644 packages/at_register/LICENSE create mode 100644 packages/at_register/README.md create mode 100644 packages/at_register/analysis_options.yaml create mode 100644 packages/at_register/example/at_register_example.dart create mode 100644 packages/at_register/example/at_register_usage_explained.dart diff --git a/packages/at_onboarding_cli/example/README.md b/packages/at_onboarding_cli/example/README.md deleted file mode 100644 index 495dce87..00000000 --- a/packages/at_onboarding_cli/example/README.md +++ /dev/null @@ -1,40 +0,0 @@ -List of steps to run the examples for checking apkam enrollment - -1. Onboard an atsign which has privilege to approve/deny enrollments:
- - run: `dart example/onboard.dart -a -c -k `
- - e.g. `dart example/onboard.dart -a @alice -k /home/alice/.atsign/@alice_wavikey.atKeys -c b26455a907582760ebf35bc4847de549bc41c24b25c8b1c58d5964f7b4f8a43bc55b0e9a601c9a9657d9a8b8bbc32f88b4e38ffaca03c8710ebae1b14ca9f364`
- - If you do not already have the CRAM Secret for your atsign - run: `dart example/get_cram_key.dart -a <@atsign>` -2. Authenticate using the onboarded atsign:
- - run: `dart example/apkam_authenticate.dart -a -k `
- - e.g. `dart example/apkam_examples/apkam_authenticate.dart -a @alice -k /home/alice/.atsign/@alice_wavikey.atKeys` -3. Run client to approve enrollments:
- - run: `dart example/enroll_app_listen.dart -a -k `
- - e.g `dart example/apkam_examples/enroll_app_listen.dart -a @alice -k /home/alice/.atsign/@alice_wavikey.atKeys` -4. Get OTP for enrollment - - 4.1 Perform a PKAM authentication through the ssl client - - 4.1.1 Get the challenge from the atServer:
- - run: `from:<@atsign>` e.g. `from:@alice`
- - This generates a string which is called the challenge which will be used to generate the authentication token
- - 4.1.2 Create a pkamSignature that can be used to authenticate yourself
- - Clone at_tools from https://github.com/atsign-foundation/at_tools.git - - Change directory into 'at_tools/packages/at_pkam>'
- - run: `dart bin/main.dart -p `
- - e.g `dart bin/main.dart -p /home/alice/.atsign/@alice_wavikey.atKeys -r _70138292-07b5-4e47-8c94-e02e38220775@alice:883ea0aa-c526-400a-926e-48cae9281de9`
- - This should generate a hash, which is called the pkamSignature which will be used to authenticate into the atServer
- - 4.1.3 Now that a pkamSignature is generated, use it to authenticate
- run:`pkam:enrollmentId::` [enrollmentId - get it from the .atKeys file]
- - 4.2 Once authenticated run `otp:get`
- - Now copy the 6-digit alpha-numeric code which is the OTP -5. Request enrollment - - 5.1 Submit enrollment from new client:
- - run:`dart example/apkam_examples/apkam_enroll.dart -a -k -o `
- - Note: this path has to be different from the path provided in Step#1 as this is a new file - - e.g. `dart example/apkam_examples/apkam_enroll.dart -a @alice -k /home/alice/.atsign/@alice_buzzkey.atKeys -o DY4UT4`
- - 5.2 Approve the enrollment from the client from #3
- - To approve the enrollment type `yes` and then Enter - - 5.3 Enrollment should be successful and atKeys file stored in the path specified -6. Authenticate using the enrolled keys file
- - 6.1 run: `dart example/apkam_examples/apkam_authenticate.dart -a -k ` - - Note: this keys file is different from the keys file generated in Step#1. This new file only has access to the data that is allowed to access from this enrollment_id - diff --git a/packages/at_onboarding_cli/lib/src/register_cli/register.dart b/packages/at_onboarding_cli/lib/src/register_cli/register.dart index eedc8eb5..7a6e5cf2 100644 --- a/packages/at_onboarding_cli/lib/src/register_cli/register.dart +++ b/packages/at_onboarding_cli/lib/src/register_cli/register.dart @@ -1,26 +1,20 @@ -import 'dart:collection'; import 'dart:io'; import 'package:args/args.dart'; import 'package:at_client/at_client.dart'; import 'package:at_onboarding_cli/src/activate_cli/activate_cli.dart' as activate_cli; -import 'package:at_onboarding_cli/src/util/api_call_status.dart'; -import 'package:at_onboarding_cli/src/util/at_onboarding_exceptions.dart'; -import 'package:at_onboarding_cli/src/util/register_api_result.dart'; -import 'package:at_onboarding_cli/src/util/register_api_task.dart'; +import 'package:at_onboarding_cli/src/register_cli/registration_flow.dart'; import 'package:at_utils/at_logger.dart'; - -import '../util/onboarding_util.dart'; -import '../util/registrar_api_constants.dart'; +import 'package:at_register/at_register.dart'; ///Class containing logic to register a free atsign to email provided ///through [args] by utilizing methods defined in [RegisterUtil] ///Requires List args containing the following arguments: email class Register { Future main(List args) async { - Map params = HashMap(); - OnboardingUtil registerUtil = OnboardingUtil(); + RegisterParams registerParams = RegisterParams(); + RegistrarApiAccessor registrarApiAccessor = RegistrarApiAccessor(); final argParser = ArgParser() ..addOption('email', @@ -40,188 +34,58 @@ class Register { if (!argResults.wasParsed('email')) { stderr.writeln( '[Unable to run Register CLI] Please enter your email address' - '\n[Usage] dart run register.dart -e email@email.com\n[Options]\n${argParser.usage}'); + '\n[Usage] dart run bin/register.dart -e email@email.com\n[Options]\n${argParser.usage}'); exit(6); } - if (registerUtil.validateEmail(argResults['email'])) { - params['email'] = argResults['email']; + if (ApiUtil.enforceEmailRegex(argResults['email'])) { + registerParams.email = argResults['email']; } else { stderr.writeln( '[Unable to run Register CLI] You have entered an invalid email address. Check your email address and try again.'); exit(7); } - //set the following parameter to RegisterApiConstants.apiHostStaging - //to use the staging environment - params['authority'] = RegistrarApiConstants.apiHostProd; - - //create stream of tasks each of type [RegisterApiTask] and then - // call start on the stream of tasks - await RegistrationFlow(params, registerUtil) - .add(GetFreeAtsign()) - .add(RegisterAtsign()) - .add(ValidateOtp()) - .start(); - - activate_cli.main(['-a', params['atsign']!, '-c', params['cramkey']!]); - } -} - -///class that handles multiple tasks of type [RegisterApiTask] -///Initialized with a params map that needs to be populated with - email and api host address -///[add] method can be used to add tasks[RegisterApiTask] to the [processFlow] -///[start] needs to be called after all required tasks are added to the [processFlow] -class RegistrationFlow { - List processFlow = []; - RegisterApiResult result = RegisterApiResult(); - late OnboardingUtil registerUtil; - Map params; - - RegistrationFlow(this.params, this.registerUtil); - - RegistrationFlow add(RegisterApiTask task) { - processFlow.add(task); - return this; - } - - Future start() async { - for (RegisterApiTask task in processFlow) { - task.init(params, registerUtil); - if (RegistrarApiConstants.isDebugMode) { - print('Current Task: $task [params=$params]\n'); - } - result = await task.run(); - if (result.apiCallStatus == ApiCallStatus.retry) { - while ( - task.shouldRetry() && result.apiCallStatus == ApiCallStatus.retry) { - result = await task.run(); - task.retryCount++; - } - } - if (result.apiCallStatus == ApiCallStatus.success) { - params.addAll(result.data); - } else { - throw AtOnboardingException(result.exceptionMessage); - } - } - } -} - -///This is a [RegisterApiTask] that fetches a free atsign -///throws [AtException] with concerned message which was encountered in the -///HTTP GET/POST request -class GetFreeAtsign extends RegisterApiTask { - @override - Future run() async { - stdout - .writeln('[Information] Getting your randomly generated free atSign…'); - try { - List atsignList = - await registerUtil.getFreeAtSigns(authority: params['authority']!); - result.data['atsign'] = atsignList[0]; - stdout.writeln('[Information] Your new atSign is **@${atsignList[0]}**'); - result.apiCallStatus = ApiCallStatus.success; - } on Exception catch (e) { - result.exceptionMessage = e.toString(); - result.apiCallStatus = - shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; - } + GetFreeAtsign getFreeAtsignTask = GetFreeAtsign(registerParams, + registrarApiAccessorInstance: registrarApiAccessor, ); - return result; - } -} + RegisterAtsign registerAtsignTask = RegisterAtsign(registerParams, + registrarApiAccessorInstance: registrarApiAccessor); -///This is a [RegisterApiTask] that registers a free atsign fetched in -///[GetFreeAtsign] to the email provided as args -///throws [AtException] with concerned message which was encountered in the -///HTTP GET/POST request -class RegisterAtsign extends RegisterApiTask { - @override - Future run() async { - stdout.writeln( - '[Information] Sending verification code to: ${params['email']}'); - try { - result.data['otpSent'] = (await registerUtil.registerAtSign( - params['atsign']!, params['email']!, - authority: params['authority']!)) - .toString(); - stdout.writeln( - '[Information] Verification code sent to: ${params['email']}'); - result.apiCallStatus = ApiCallStatus.success; - } on Exception catch (e) { - result.exceptionMessage = e.toString(); - result.apiCallStatus = - shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; - } - return result; - } -} + ValidateOtp validateOtpTask = ValidateOtp(registerParams, + registrarApiAccessorInstance: registrarApiAccessor); -///This is a [RegisterApiTask] that validates the otp which was sent as a part -///of [RegisterAtsign] to email provided in args -///throws [AtException] with concerned message which was encountered in the -///HTTP GET/POST request -class ValidateOtp extends RegisterApiTask { - @override - void init(Map params, OnboardingUtil registerUtil) { - params['confirmation'] = 'false'; - this.params = params; - this.registerUtil = registerUtil; - result.data = HashMap(); - } + // create a queue of tasks each of type [RegisterTask] and then + // call start on the RegistrationFlow object + await RegistrationFlow() + .add(getFreeAtsignTask) + .add(registerAtsignTask) + .add(validateOtpTask) + .start(); - @override - Future run() async { - if (params['otp'] == null) { - params['otp'] = registerUtil.getVerificationCodeFromUser(); - } - stdout.writeln('[Information] Validating your verification code...'); - try { - String apiResponse = await registerUtil.validateOtp( - params['atsign']!, params['email']!, params['otp']!, - confirmation: params['confirmation']!, - authority: params['authority']!); - if (apiResponse == 'retry') { - stderr.writeln( - '[Unable to proceed] The verification code you entered is either invalid or expired.\n' - ' Check your verification code and try again.'); - params['otp'] = registerUtil.getVerificationCodeFromUser(); - result.apiCallStatus = ApiCallStatus.retry; - result.exceptionMessage = - 'Incorrect otp entered 3 times. Max retries reached.'; - } else if (apiResponse == 'follow-up') { - params.update('confirmation', (value) => 'true'); - result.data['otp'] = params['otp']; - result.apiCallStatus = ApiCallStatus.retry; - } else if (apiResponse.startsWith("@")) { - result.data['cramkey'] = apiResponse.split(":")[1]; - stdout.writeln( - '[Information] Your cram secret: ${result.data['cramkey']}'); - stdout.writeln('[Success] Your atSign **@${params['atsign']}** has been' - ' successfully registered to ${params['email']}'); - result.apiCallStatus = ApiCallStatus.success; - } - } on Exception catch (e) { - result.exceptionMessage = e.toString(); - result.apiCallStatus = - shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; - } - return result; + activate_cli + .main(['-a', registerParams.atsign!, '-c', registerParams.cram!]); } } Future main(List args) async { Register register = Register(); - AtSignLogger.root_level = 'severe'; + AtSignLogger.root_level = 'info'; try { await register.main(args); + } on MaximumAtsignQuotaException { + stdout.writeln( + '[Unable to proceed] This email address already has 10 free atSigns associated with it.\n' + 'To register a new atSign to this email address, please log into the dashboard \'my.atsign.com/login\'.\n' + 'Remove at least 1 atSign from your account and then try again.\n' + 'Alternatively, you can retry this process with a different email address.'); + exit(0); } on FormatException catch (e) { if (e.toString().contains('Missing argument')) { stderr.writeln( '[Unable to run Register CLI] Please re-run with your email address'); - stderr - .writeln('Usage: \'dart run register_cli.dart -e email@email.com\''); + stderr.writeln( + 'Usage: \'dart run bin/register_cli.dart -e email@email.com\''); exit(1); } else if (e.toString().contains('Could not find an option or flag')) { stderr @@ -237,11 +101,11 @@ Future main(List args) async { stderr.writeln('Cause: $e'); exit(3); } - } on AtOnboardingException catch (e) { + } on AtException catch (e) { stderr.writeln( '[Error] Failed getting an atsign. It looks like something went wrong on our side.\n' 'Please try again or contact support@atsign.com, quoting the text displayed below.'); - stderr.writeln('Cause: $e ExceptionType:${e.runtimeType}'); + stderr.writeln('Cause: ${e.message} ExceptionType:${e.runtimeType}'); exit(4); } on Exception catch (e) { if (e diff --git a/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart b/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart new file mode 100644 index 00000000..5d76f7fe --- /dev/null +++ b/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart @@ -0,0 +1,41 @@ +import 'package:at_register/at_register.dart'; + +/// Processes tasks of type [RegisterTask] +/// Initialized with a params map that needs to be populated with - email and api host address +/// [add] method can be used to add tasks[RegisterTask] to the [processQueue] +/// [start] needs to be called after all required tasks are added to the [processQueue] +class RegistrationFlow { + List processQueue = []; + RegisterTaskResult _result = RegisterTaskResult(); + RegisterParams params = RegisterParams(); + + RegistrationFlow(); + + RegistrationFlow add(RegisterTask task) { + processQueue.add(task); + return this; + } + + Future start() async { + for (RegisterTask task in processQueue) { + // setting allowRetry to false as this method has logic to retry each + // failed task 3-times and then throw an exception if still failing + _result = await task.run(); + task.logger.finer('Attempt: ${task.retryCount} | params[$params]'); + task.logger.finer('Result: $_result'); + + if (_result.apiCallStatus == ApiCallStatus.retry) { + while ( + task.shouldRetry() && _result.apiCallStatus == ApiCallStatus.retry) { + _result = await task.retry(); + } + } + if (_result.apiCallStatus == ApiCallStatus.success) { + params.addFromJson(_result.data); + } else { + throw AtRegisterException(_result.exceptionMessage!); + } + } + return _result; + } +} diff --git a/packages/at_onboarding_cli/pubspec.yaml b/packages/at_onboarding_cli/pubspec.yaml index 62f8521a..acf0c6de 100644 --- a/packages/at_onboarding_cli/pubspec.yaml +++ b/packages/at_onboarding_cli/pubspec.yaml @@ -28,6 +28,13 @@ dependencies: at_server_status: ^1.0.4 at_utils: ^3.0.16 +dependency_overrides: + at_register: + git: + url: https://github.com/atsign-foundation/at_libraries.git + path: packages/at_register + ref: at_register_package + dev_dependencies: lints: ^2.1.0 test: ^1.24.2 diff --git a/packages/at_onboarding_cli/test/at_register_test.dart b/packages/at_onboarding_cli/test/at_register_test.dart new file mode 100644 index 00000000..8ec06d03 --- /dev/null +++ b/packages/at_onboarding_cli/test/at_register_test.dart @@ -0,0 +1,249 @@ +import 'package:at_onboarding_cli/src/register_cli/registration_flow.dart'; +import 'package:at_register/at_register.dart'; +import 'package:at_utils/at_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockRegistrarApiCall extends Mock implements RegistrarApiAccessor {} + +void main() { + RegistrarApiAccessor accessorInstance = MockRegistrarApiCall(); + AtSignLogger.root_level = 'finer'; + + group('A group of tests to validate GetFreeAtsign', () { + setUp(() => resetMocktailState()); + + test('validate behaviour of GetFreeAtsign - encounters exception', + () async { + when(() => accessorInstance.getFreeAtSigns()) + .thenThrow(Exception('random exception')); + + RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; + GetFreeAtsign getFreeAtsign = GetFreeAtsign(params, + registrarApiAccessorInstance: accessorInstance, allowRetry: true); + try { + await RegistrationFlow().add(getFreeAtsign).start(); + } on Exception catch (e) { + expect(e.runtimeType, AtRegisterException); + expect(e.toString().contains('random exception'), true); + } + expect(getFreeAtsign.retryCount, getFreeAtsign.maximumRetries); + expect(getFreeAtsign.shouldRetry(), false); + }); + }); + + group('Group of tests to validate behaviour of RegisterAtsign', () { + setUp(() => resetMocktailState()); + + test('RegisterAtsign params reading and updating - negative case', + () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + when(() => accessorInstance.registerAtSign(atsign, email)) + .thenAnswer((_) => Future.value(false)); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + RegisterAtsign registerAtsignTask = RegisterAtsign(params, + registrarApiAccessorInstance: accessorInstance, allowRetry: true); + + await RegistrationFlow().add(registerAtsignTask).start(); + expect(registerAtsignTask.retryCount, registerAtsignTask.maximumRetries); + expect(registerAtsignTask.shouldRetry(), false); + }); + + test('verify behaviour of RegisterAtsign processing an exception', + () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + when(() => accessorInstance.registerAtSign(atsign, email)) + .thenThrow(Exception('another random exception')); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + RegisterAtsign registerAtsignTask = RegisterAtsign(params, + registrarApiAccessorInstance: accessorInstance, allowRetry: true); + + expect(registerAtsignTask.retryCount, registerAtsignTask.maximumRetries); + expect(registerAtsignTask.shouldRetry(), false); + try { + await RegistrationFlow().add(registerAtsignTask).start(); + } on Exception catch (e) { + assert(e is AtRegisterException && + e.message.contains('another random exception')); + } + }); + + test( + 'verify behaviour of RegistrationFlow processing exception in RegisterAtsign', + () async { + String atsign = '@bobby'; + String email = 'second-group@email'; + when(() => accessorInstance.registerAtSign(atsign, email)) + .thenThrow(Exception('another new random exception')); + + RegisterParams params = RegisterParams() + ..atsign = atsign + ..email = email; + RegisterAtsign registerAtsignTask = RegisterAtsign(params, + registrarApiAccessorInstance: accessorInstance, allowRetry: true); + + try { + await RegistrationFlow().add(registerAtsignTask).start(); + } on Exception catch (e) { + print(e.toString()); + assert(e.toString().contains('another new random exception')); + } + expect(registerAtsignTask.retryCount, 3); + expect(registerAtsignTask.shouldRetry(), false); + }); + }); + + group('A group of tests to verify ValidateOtp task behaviour', () { + setUp(() => resetMocktailState()); + + test( + 'validate positive behaviour of ValidateOtp task - need to followUp with confirmation set to true', + () async { + // In this test Registration flow is supposed to call the validateOtpTask + // with confirmation first set to false and then with true without dev + // intervention + String atsign = '@charlie123'; + String email = 'third-group@email'; + String otp = 'bcde'; + String cram = 'craaaaaaaaaaaam1234'; + + var mockApiRespData = { + 'atsign': ['@old-atsign'], + 'newAtsign': atsign + }; + ValidateOtpResult validateOtpResult = ValidateOtpResult() + ..taskStatus = ValidateOtpStatus.followUp + ..apiCallStatus = ApiCallStatus.success + ..data = {'data': mockApiRespData}; + when(() => accessorInstance.validateOtp(atsign, email, otp, + confirmation: false)) + .thenAnswer((invocation) => Future.value(validateOtpResult)); + + ValidateOtpResult validateOtpResult2 = ValidateOtpResult() + ..taskStatus = ValidateOtpStatus.verified + ..apiCallStatus = ApiCallStatus.success + ..data = {RegistrarConstants.cramKeyName: '$atsign:$cram'}; + when(() => accessorInstance.validateOtp(atsign, email, otp, + confirmation: true)) + .thenAnswer((invocation) => Future.value(validateOtpResult2)); + + var params = RegisterParams() + ..atsign = atsign + ..confirmation = false + ..email = email + ..otp = otp; + + ValidateOtp validateOtpTask = ValidateOtp(params, + registrarApiAccessorInstance: accessorInstance, allowRetry: true); + RegisterTaskResult result = + await RegistrationFlow().add(validateOtpTask).start(); + + expect(result.data[RegistrarConstants.cramKeyName], cram); + expect(params.confirmation, true); + expect(validateOtpResult2.taskStatus, ValidateOtpStatus.verified); + + expect(validateOtpTask.retryCount, 2); + expect(validateOtpTask.shouldRetry(), true); + }); + + test('validate behaviour of ValidateOtp task - 3 otp retries exhausted', + () async { + String atsign = '@charlie-retry'; + String email = 'third-group-test-3@email'; + String otp = 'bcaa'; + String cram = 'craaaaaaaaaaaam'; + ValidateOtpResult validateOtpResult = ValidateOtpResult(); + validateOtpResult.taskStatus = ValidateOtpStatus.retry; + validateOtpResult.apiCallStatus = ApiCallStatus.success; + validateOtpResult.data = { + RegistrarConstants.cramKeyName: '$atsign:$cram' + }; + when(() => accessorInstance.validateOtp(atsign, email, otp, + confirmation: any(named: "confirmation"))) + .thenAnswer((invocation) => Future.value(validateOtpResult)); + + var params = RegisterParams() + ..atsign = atsign + ..confirmation = false + ..email = email + ..otp = otp; + + ValidateOtp validateOtpTask = ValidateOtp(params, + registrarApiAccessorInstance: accessorInstance, allowRetry: true); + expect(() async => await RegistrationFlow().add(validateOtpTask).start(), + throwsA(ExhaustedVerificationCodeRetriesException)); + + expect(validateOtpTask.retryCount, validateOtpTask.maximumRetries); + expect(validateOtpTask.shouldRetry(), false); + }); + }); + + group('test to validate all 3 API calls in sequence', () { + setUp(() => resetMocktailState()); + + test('verify all 3 API calls at once', () async { + String atsign = '@lewis'; + String email = 'lewis44@gmail.com'; + String cram = 'craaaaaaaaaaaaam'; + String otp = 'Agbr'; + // mock for get-free-atsign + when(() => accessorInstance.getFreeAtSigns()) + .thenAnswer((invocation) => Future.value(atsign)); + // mock for register-atsign + when(() => accessorInstance.registerAtSign(atsign, email)) + .thenAnswer((_) => Future.value(true)); + // rest of the mocks for validate-otp + var mockApiRespData = { + 'atsign': ['@old-atsign'], + 'newAtsign': atsign + }; + ValidateOtpResult validateOtpResult = ValidateOtpResult() + ..taskStatus = ValidateOtpStatus.followUp + ..apiCallStatus = ApiCallStatus.success + ..data = {'data': mockApiRespData}; + when(() => accessorInstance.validateOtp(atsign, email, otp, + confirmation: false)) + .thenAnswer((invocation) => Future.value(validateOtpResult)); + + ValidateOtpResult validateOtpResult2 = ValidateOtpResult() + ..taskStatus = ValidateOtpStatus.verified + ..apiCallStatus = ApiCallStatus.success + ..data = {RegistrarConstants.cramKeyName: '$atsign:$cram'}; + when(() => accessorInstance.validateOtp(atsign, email, otp, + confirmation: true)) + .thenAnswer((invocation) => Future.value(validateOtpResult2)); + + RegisterParams params = RegisterParams() + ..email = email + ..otp = otp + ..confirmation = false; + + GetFreeAtsign getFreeAtsignTask = GetFreeAtsign(params, + registrarApiAccessorInstance: accessorInstance, allowRetry: true); + + RegisterAtsign registerAtsignTask = RegisterAtsign(params, + registrarApiAccessorInstance: accessorInstance, allowRetry: true); + + ValidateOtp validateOtpTask = ValidateOtp(params, + registrarApiAccessorInstance: accessorInstance, allowRetry: true); + + await RegistrationFlow() + .add(getFreeAtsignTask) + .add(registerAtsignTask) + .add(validateOtpTask) + .start(); + + expect(params.atsign, atsign); + expect(params.cram, cram); + expect(params.confirmation, true); + }); + }); +} diff --git a/packages/at_register/.gitignore b/packages/at_register/.gitignore new file mode 100644 index 00000000..3cceda55 --- /dev/null +++ b/packages/at_register/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/at_register/LICENSE b/packages/at_register/LICENSE new file mode 100644 index 00000000..caf63d72 --- /dev/null +++ b/packages/at_register/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2020, The @ Foundation +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/at_register/README.md b/packages/at_register/README.md new file mode 100644 index 00000000..f6cc65d9 --- /dev/null +++ b/packages/at_register/README.md @@ -0,0 +1,81 @@ +The Atsign FoundationThe Atsign Foundation + +[![pub package](https://img.shields.io/pub/v/at_register)](https://pub.dev/packages/at_lookup) [![pub points](https://img.shields.io/pub/points/at_register?logo=dart)](https://pub.dev/packages/at_lookup/score) [![gitHub license](https://img.shields.io/badge/license-BSD3-blue.svg)](./LICENSE) + +# at_register + +## Overview: +This package contains code components that can interact with the AtRegistrar API and process the data received from the +API. +This serves as a collection of code components +that can be consumed by other packages to fetch free atsigns and register them to emails. + +Note: +If you're looking for a utility to get and activate a free atsign, +please feel free to take a look at `at_onboarding_cli/register_cli` +(or) any of the at_platforms apps available on PlayStore/AppStore + +## Get started: + +### Installation: + +To add this package as the dependency, add it to your pubspec.yaml + +```dart +dependencies: + at_register: ^1.0.0 +``` + +#### Add to your project + +```sh +dart pub get +``` + +#### Import in your application code + +```dart +import 'package:at_register/at_register.dart'; +``` + +### Clone it from GitHub + +Feel free to fork a copy of the source from the [GitHub Repo](https://github.com/atsign-foundation/at_libraries) + +## Usage +### 0) Creating a RegisterParams object +```dart +RegisterParams params = RegisterParams()..email = 'email@email.com'; +``` + +### 1) To fetch a free atsign + +```dart +GetFreeAtsign getFreeAtsignTask = GetFreeAtsign(params); +RegisterTaskResult result = await getFreeAtsignTask.run(); +print(result.data['atsign']); +``` + +### 2) To register the free atsign fetched in (1) + +```dart +params.atsign = '@alice'; // preferably use the one fetched in (1) +RegisterAtsign registerAtsignTask = RegisterAtsign(params); +RegisterTaskResult result = await registerAtsignTask.run(); +print(result.data['otpSent']); // contains true/false if verification code was delivered to email +``` + +### 3) To validate the verification code + +```dart +params.otp = 'AB1C'; // to be fetched from user +ValidateOtp validateOtpTask = ValidateOtp(params); +RegisterTaskResult result = await validateOtp.run(); +print(result.data['cram']); +``` +Please refer to [examples](https://github.com/atsign-foundation/at_libraries/blob/doc_at_lookup/at_lookup/example/bin/example.dart) for more details. + +## Open source usage and contributions + +This is freely licensed open source code, so feel free to use it as is, suggest changes or enhancements or create your +own version. See CONTRIBUTING.md for detailed guidance on how to setup tools, tests and make a pull request. \ No newline at end of file diff --git a/packages/at_register/analysis_options.yaml b/packages/at_register/analysis_options.yaml new file mode 100644 index 00000000..0246f5be --- /dev/null +++ b/packages/at_register/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types +# +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/at_register/example/at_register_example.dart b/packages/at_register/example/at_register_example.dart new file mode 100644 index 00000000..eb40b25f --- /dev/null +++ b/packages/at_register/example/at_register_example.dart @@ -0,0 +1,32 @@ +import 'package:at_register/at_register.dart'; + +void main() async { + String email = ''; + RegisterParams params = RegisterParams()..email = email; + + GetFreeAtsign getFreeAtsignTask = GetFreeAtsign(params); + RegisterTaskResult result = await getFreeAtsignTask.run(); + params.addFromJson(result.data); + + RegisterAtsign registerAtsignTask = RegisterAtsign(params); + result = await registerAtsignTask.run(); + params.addFromJson(result.data); + + // verification code sent to email provided in the beginning + // check the same email and enter that verification code through terminal/stdin + params.otp = ApiUtil.readCliVerificationCode(); + ValidateOtp validateOtpTask = ValidateOtp(params); + result = await validateOtpTask.run(); + if(result.apiCallStatus == ApiCallStatus.success){ + print(result.data[RegistrarConstants.cramKeyName]); + } else { + // this is the case where the email has existing atsigns + // set task.params.confirmation to true, select an atsign (existing/new) from + // the + String newAtsign = result.data[RegistrarConstants.newAtsignName]; + validateOtpTask.registerParams.atsign = newAtsign; + validateOtpTask.registerParams.confirmation = true; + result = await validateOtpTask.run(); + print(result.data[RegistrarConstants.cramKeyName]); + } +} \ No newline at end of file diff --git a/packages/at_register/example/at_register_usage_explained.dart b/packages/at_register/example/at_register_usage_explained.dart new file mode 100644 index 00000000..10fdea84 --- /dev/null +++ b/packages/at_register/example/at_register_usage_explained.dart @@ -0,0 +1,60 @@ +import 'package:at_register/at_register.dart'; + +Future main() async { + RegisterParams params = RegisterParams()..email = 'abcd@email.com'; + RegistrarApiAccessor accessorInstance = RegistrarApiAccessor(); + + /// Example for GetFreeAtsign task + GetFreeAtsign getFreeAtsignTask = + GetFreeAtsign(params, registrarApiAccessorInstance: accessorInstance); + RegisterTaskResult getFreeAtsignResult = await getFreeAtsignTask.run(); + // api call status present in result.apiCallStatus + print(getFreeAtsignResult.apiCallStatus); + // all relevant data present in result.data which is a Map + print(getFreeAtsignResult.data); + + // this step is optional + // Can be used to propagates the data received in the current task to the next + params.addFromJson(getFreeAtsignResult.data); + // ---------------------------------------------------- + + /// Example for RegisterAtsign task + RegisterAtsign registerAtsignTask = + RegisterAtsign(params, registrarApiAccessorInstance: accessorInstance); + RegisterTaskResult registerAtsignResult = await registerAtsignTask.run(); + // registerAtsignResult.data should have a key named 'otpSent' which contains + // true/false reg the status of verificationCodeSent to provided email + print(registerAtsignResult.data[RegistrarConstants.otpSentName]); + + params.addFromJson(registerAtsignResult.data); + // -------------------------------------------------------- + + /// Example for ValidateOtp task + ValidateOtp validateOtpTask = + ValidateOtp(params, registrarApiAccessorInstance: accessorInstance); + // Note: confirmation is set to false by default + RegisterTaskResult validateOtpResult = await validateOtpTask.run(); + + // CASE 1: if this is the first atsign for the provided email, CRAM should be + // present in validateOtpResult.data with key RegistrarConstants.cramKeyName + print(validateOtpResult.data[RegistrarConstants.cramKeyName]); + + // CASE 2: if this is not the first atsign, data contains the list of + // existing atsigns registered to that email + print(validateOtpResult.data[RegistrarConstants.fetchedAtsignListName]); + // and the new atsign fetched in the previous task + print(validateOtpResult.data[RegistrarConstants.newAtsignName]); + // either select the newAtsign or one of the existing atsigns and re-run the + // validateOtpTask but this time with confirmation set to true + // now this will return a result with the cram key in result.data + List fetchedAtsignList = + validateOtpResult.data[RegistrarConstants.fetchedAtsignListName]; + validateOtpTask.registerParams.atsign = fetchedAtsignList[0]; + validateOtpResult = await validateOtpTask.run(); + print(validateOtpResult.data[RegistrarConstants.cramKeyName]); + + // CASE 3: if the otp is incorrect, fetch the correct otp from user and re-run + // the validateOtpTask + validateOtpTask.registerParams.otp = 'AB14'; // correct otp + validateOtpResult = await validateOtpTask.run(); +} diff --git a/packages/at_register/lib/src/api-interactions/get_free_atsign.dart b/packages/at_register/lib/src/api-interactions/get_free_atsign.dart index 6cbee684..fe9c1ccb 100644 --- a/packages/at_register/lib/src/api-interactions/get_free_atsign.dart +++ b/packages/at_register/lib/src/api-interactions/get_free_atsign.dart @@ -1,3 +1,5 @@ +import 'package:at_commons/at_commons.dart'; + import '../../at_register.dart'; /// A [RegisterTask] that fetches a list of free atsigns. @@ -14,30 +16,35 @@ import '../../at_register.dart'; /// The fetched atsign will be stored in result.data['atsign']. /// ToDo: write down what will be the structure inside result.data{} class GetFreeAtsign extends RegisterTask { - GetFreeAtsign(super.registerParams, {super.registrarApiAccessorInstance}); + GetFreeAtsign(super.registerParams, + {super.registrarApiAccessorInstance, super.allowRetry}); @override String get name => 'GetFreeAtsignTask'; @override - Future run({bool allowRetry = false}) async { + Future run() async { logger.info('Getting a randomly generated free atSign...'); RegisterTaskResult result = RegisterTaskResult(); try { String atsign = await registrarApiAccessor.getFreeAtSigns( authority: RegistrarConstants.authority); - logger.info('Fetched free atsign: $atsign'); result.data['atsign'] = atsign; - result.apiCallStatus = ApiCallStatus.success; } on Exception catch (e) { - if (!allowRetry) { + if (canThrowException()) { throw AtRegisterException(e.toString()); } - result.apiCallStatus = - shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; - result.exceptionMessage = e.toString(); + populateApiCallStatus(result, e); } + result.apiCallStatus = ApiCallStatus.success; return result; } + + @override + void validateInputParams() { + if(registerParams.email.isNullOrEmpty){ + throw IllegalArgumentException('email cannot be null for get-free-atsign-task'); + } + } } diff --git a/packages/at_register/lib/src/api-interactions/register_atsign.dart b/packages/at_register/lib/src/api-interactions/register_atsign.dart index 91b82ce7..da6d8f26 100644 --- a/packages/at_register/lib/src/api-interactions/register_atsign.dart +++ b/packages/at_register/lib/src/api-interactions/register_atsign.dart @@ -1,3 +1,5 @@ +import 'package:at_commons/at_commons.dart'; + import '../../at_register.dart'; /// User selects an atsign from the list fetched in [GetFreeAtsign]. @@ -6,30 +8,39 @@ import '../../at_register.dart'; /// /// Sets [RegisterTaskResult.apiCallStatus] if the HTTP GET/POST request gets any response other than STATUS_OK. class RegisterAtsign extends RegisterTask { - RegisterAtsign(super.registerParams, {super.registrarApiAccessorInstance}); + RegisterAtsign(super.registerParams, + {super.registrarApiAccessorInstance, super.allowRetry}); @override String get name => 'RegisterAtsignTask'; @override - Future run({bool allowRetry = false}) async { + Future run() async { logger.info('Sending verification code to: ${registerParams.email}'); RegisterTaskResult result = RegisterTaskResult(); try { - result.data['otpSent'] = (await registrarApiAccessor.registerAtSign( - registerParams.atsign!, registerParams.email!, - authority: RegistrarConstants.authority)) + result.data[RegistrarConstants.otpSentName] = (await registrarApiAccessor + .registerAtSign(registerParams.atsign!, registerParams.email!, + authority: RegistrarConstants.authority)) .toString(); logger.info('Verification code sent to: ${registerParams.email}'); } on Exception catch (e) { - if (!allowRetry) { + if (canThrowException()) { throw AtRegisterException(e.toString()); } - result.apiCallStatus = - shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; - result.exceptionMessage = e.toString(); + populateApiCallStatus(result, e); } result.apiCallStatus = ApiCallStatus.success; return result; } + + @override + void validateInputParams() { + if(registerParams.atsign.isNullOrEmpty){ + throw IllegalArgumentException('Atsign cannot be null for register-atsign-task'); + } + if(registerParams.email.isNullOrEmpty){ + throw IllegalArgumentException('e-mail cannot be null for register-atsign-task'); + } + } } diff --git a/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart b/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart index 6edcf4ee..dbdcf178 100644 --- a/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart +++ b/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart @@ -74,8 +74,9 @@ class RegistrarApiAccessor { /// /// Case 1("verified") - the API has registered the atsign to provided email and CRAM key present in HTTP_RESPONSE Body. /// - /// Case 2("follow-up"): User already has existing atsigns and new atsign registered successfully. To receive the CRAM key, follow-up by calling - /// the API with one of the existing listed atsigns, with confirmation set to true. + /// Case 2("follow-up"): User already has existing atsigns and new atsign + /// registered successfully. To receive the CRAM key, retry the call with one + /// of the existing listed atsigns and confirmation set to true. /// /// Case 3("retry"): Incorrect OTP send request again with correct OTP. /// @@ -92,13 +93,12 @@ class RegistrarApiAccessor { }); if (response.statusCode == 200) { - final validateOtpResult = ValidateOtpResult(); - // is this needed ? + ValidateOtpResult validateOtpResult = ValidateOtpResult(); final jsonDecodedResponse = jsonDecode(response.body); - if (jsonDecodedResponse.containsKey('data')) { - validateOtpResult.data - .addAll(jsonDecodedResponse['data'] as Map); - } + // if (jsonDecodedResponse.containsKey('data')) { + // validateOtpResult.data + // .addAll(jsonDecodedResponse['data'] as Map); + // } _processValidateOtpApiResponse(jsonDecodedResponse, validateOtpResult); return validateOtpResult; } else { @@ -108,28 +108,34 @@ class RegistrarApiAccessor { } /// processes API response for [validateOtp] call and populates [result] - void _processValidateOtpApiResponse(responseJson, result) { - if ((responseJson.containsKey('message') && - (responseJson['message'].toString().toLowerCase()) == 'verified') && - responseJson.containsKey('cramkey')) { + void _processValidateOtpApiResponse( + Map apiResponse, ValidateOtpResult result) { + if ((apiResponse.containsKey('message') && + (apiResponse['message'].toString().toLowerCase()) == + ValidateOtpStatus.verified.name) && + apiResponse.containsKey(RegistrarConstants.cramKeyName)) { result.taskStatus = ValidateOtpStatus.verified; result.data[RegistrarConstants.cramKeyName] = - responseJson[RegistrarConstants.cramKeyName]; - } else if (responseJson.containsKey('data') && - result.data.containsKey('newAtsign')) { + apiResponse[RegistrarConstants.cramKeyName]; + } else if (apiResponse.containsKey('data') && + result.data.containsKey(RegistrarConstants.newAtsignName)) { + result.data[RegistrarConstants.fetchedAtsignListName] = + apiResponse[RegistrarConstants.atsignName]; + result.data[RegistrarConstants.newAtsignName] = + apiResponse[RegistrarConstants.newAtsignName]; result.taskStatus = ValidateOtpStatus.followUp; - } else if (responseJson.containsKey('message') && - responseJson['message'] == + } else if (apiResponse.containsKey('message') && + apiResponse['message'] == 'The code you have entered is invalid or expired. Please try again?') { result.taskStatus = ValidateOtpStatus.retry; - result.exceptionMessage = responseJson['message']; - } else if (responseJson.containsKey('message') && - (responseJson['message'] == + result.exceptionMessage = apiResponse['message']; + } else if (apiResponse.containsKey('message') && + (apiResponse['message'] == 'Oops! You already have the maximum number of free atSigns. Please select one of your existing atSigns.')) { throw MaximumAtsignQuotaException( 'Maximum free atsign limit reached for current email'); } else { - throw AtRegisterException('${responseJson['message']}'); + throw AtRegisterException('${apiResponse['message']}'); } } diff --git a/packages/at_register/lib/src/api-interactions/validate_otp.dart b/packages/at_register/lib/src/api-interactions/validate_otp.dart index 20dae020..3912c72b 100644 --- a/packages/at_register/lib/src/api-interactions/validate_otp.dart +++ b/packages/at_register/lib/src/api-interactions/validate_otp.dart @@ -6,21 +6,16 @@ import '../../at_register.dart'; /// Task for validating the verification_code sent as part of the registration process. class ValidateOtp extends RegisterTask { ValidateOtp(super.registerParams, - {super.registrarApiAccessorInstance, bool confirmation = false}); + {super.registrarApiAccessorInstance, super.allowRetry}); @override String get name => 'ValidateOtpTask'; @override - Future run({bool allowRetry = false}) async { + Future run() async { RegisterTaskResult result = RegisterTaskResult(); - if (registerParams.otp.isNullOrEmpty) { - throw InvalidVerificationCodeException( - 'Verification code cannot be null'); - } try { - logger - .info('Validating verification code for ${registerParams.atsign}...'); + logger.info('Validating code with ${registerParams.atsign}...'); registerParams.atsign = AtUtils.fixAtSign(registerParams.atsign!); final validateOtpApiResult = await registrarApiAccessor.validateOtp( registerParams.atsign!, @@ -32,7 +27,7 @@ class ValidateOtp extends RegisterTask { switch (validateOtpApiResult.taskStatus) { case ValidateOtpStatus.retry: - if (!allowRetry) { + if (canThrowException()) { throw InvalidVerificationCodeException( 'Verification Failed: Incorrect verification code provided'); } else { @@ -49,6 +44,11 @@ class ValidateOtp extends RegisterTask { case ValidateOtpStatus.followUp: registerParams.confirmation = true; result.data['otp'] = registerParams.otp; + result.data[RegistrarConstants.fetchedAtsignListName] = + validateOtpApiResult + .data[RegistrarConstants.fetchedAtsignListName]; + result.data[RegistrarConstants.newAtsignName] = + validateOtpApiResult.data[RegistrarConstants.newAtsignName]; result.apiCallStatus = ApiCallStatus.retry; logger.finer( 'Provided email has existing atsigns, please select one atsign and retry this task'); @@ -58,8 +58,8 @@ class ValidateOtp extends RegisterTask { result.data[RegistrarConstants.cramKeyName] = validateOtpApiResult .data[RegistrarConstants.cramKeyName] .split(":")[1]; - logger.info('Cram secret verified.'); - logger.shout('Successful registration for ${registerParams.email}'); + logger.info('Cram secret verified'); + logger.info('Successful registration for ${registerParams.email}'); result.apiCallStatus = ApiCallStatus.success; break; @@ -67,6 +67,7 @@ class ValidateOtp extends RegisterTask { result.apiCallStatus = ApiCallStatus.failure; result.exceptionMessage = validateOtpApiResult.exceptionMessage; break; + case null: result.apiCallStatus = ApiCallStatus.failure; result.exceptionMessage = validateOtpApiResult.exceptionMessage; @@ -78,14 +79,29 @@ class ValidateOtp extends RegisterTask { rethrow; } on InvalidVerificationCodeException { rethrow; - } catch (e) { - if (!allowRetry) { + } on Exception catch (e) { + if (canThrowException()) { throw AtRegisterException(e.toString()); } - result.apiCallStatus = - shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; - result.exceptionMessage = e.toString(); + populateApiCallStatus(result, e); } return result; } + + @override + void validateInputParams() { + if (registerParams.atsign.isNullOrEmpty) { + throw IllegalArgumentException( + 'Atsign cannot be null for register-atsign-task'); + } + if (registerParams.email.isNullOrEmpty) { + throw IllegalArgumentException( + 'e-mail cannot be null for register-atsign-task'); + } + if (registerParams.otp.isNullOrEmpty) { + throw InvalidVerificationCodeException( + 'Verification code cannot be null/empty'); + } + + } } diff --git a/packages/at_register/lib/src/config/registrar_constants.dart b/packages/at_register/lib/src/config/registrar_constants.dart index 7834055c..5e306996 100644 --- a/packages/at_register/lib/src/config/registrar_constants.dart +++ b/packages/at_register/lib/src/config/registrar_constants.dart @@ -26,4 +26,7 @@ class RegistrarConstants { static const String cramKeyName = 'cramkey'; static const String atsignName = 'atsign'; + static const String otpSentName = 'otpSent'; + static const String fetchedAtsignListName = 'fetchedAtsignList'; + static const String newAtsignName = 'newAtsign'; } diff --git a/packages/at_register/lib/src/util/api_call_status.dart b/packages/at_register/lib/src/util/api_call_status.dart index dd2635ff..ab322e77 100644 --- a/packages/at_register/lib/src/util/api_call_status.dart +++ b/packages/at_register/lib/src/util/api_call_status.dart @@ -1,3 +1,3 @@ enum ApiCallStatus { success, failure, retry } -enum ValidateOtpStatus{verified, followUp, retry, failure} +enum ValidateOtpStatus { verified, followUp, retry, failure } diff --git a/packages/at_register/lib/src/util/register_task.dart b/packages/at_register/lib/src/util/register_task.dart index 97ab0f5a..cec41d78 100644 --- a/packages/at_register/lib/src/util/register_task.dart +++ b/packages/at_register/lib/src/util/register_task.dart @@ -5,12 +5,14 @@ import 'package:at_utils/at_logger.dart'; abstract class RegisterTask { late String name; - static final maximumRetries = 3; + final maximumRetries = 3; int _retryCount = 1; int get retryCount => _retryCount; + late bool _allowRetry = false; + late RegisterParams registerParams; late RegistrarApiAccessor _registrarApiAccessor; @@ -19,9 +21,12 @@ abstract class RegisterTask { late AtSignLogger logger; RegisterTask(this.registerParams, - {RegistrarApiAccessor? registrarApiAccessorInstance}) { + {RegistrarApiAccessor? registrarApiAccessorInstance, + bool allowRetry = false}) { _registrarApiAccessor = registrarApiAccessorInstance ?? RegistrarApiAccessor(); + _allowRetry = allowRetry; + validateInputParams(); logger = AtSignLogger(name); } @@ -31,13 +36,29 @@ abstract class RegisterTask { /// If [allowRetry] is set to true, the task will rethrow all exceptions /// otherwise will catch the exception and store the exception message in /// [RegisterTaskResult.exceptionMessage] - Future run({bool allowRetry = true}); + Future run(); - Future retry({bool allowRetry = true}) async { + Future retry() async { increaseRetryCount(); - return await run(allowRetry: allowRetry); + return await run(); + } + + bool canThrowException() { + // cannot throw exception if retries allowed + return !_allowRetry; } + void populateApiCallStatus(RegisterTaskResult result, Exception e) { + result.apiCallStatus = + shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; + result.exceptionMessage = e.toString(); + } + + /// Each task implementation will have a set of params that are required to + /// complete the respective task. This method will ensure all required params + /// are provided/populated + void validateInputParams(); + /// Increases retry count by 1 /// /// This method is to ensure that retryCount cannot be reduced @@ -48,6 +69,6 @@ abstract class RegisterTask { /// In case the task has returned a [RegisterTaskResult] with status retry, /// this method checks and returns if the task can be retried bool shouldRetry() { - return _retryCount < maximumRetries; + return _retryCount <= maximumRetries; } } diff --git a/packages/at_register/lib/src/util/register_task_result.dart b/packages/at_register/lib/src/util/register_task_result.dart index 8f9f3e71..08fc338d 100644 --- a/packages/at_register/lib/src/util/register_task_result.dart +++ b/packages/at_register/lib/src/util/register_task_result.dart @@ -3,7 +3,7 @@ import 'dart:collection'; import 'api_call_status.dart'; class RegisterTaskResult { - Map data = HashMap(); + Map data = HashMap(); late ApiCallStatus apiCallStatus; diff --git a/packages/at_register/lib/src/util/validate_otp_task_result.dart b/packages/at_register/lib/src/util/validate_otp_task_result.dart index 09b74822..283990c1 100644 --- a/packages/at_register/lib/src/util/validate_otp_task_result.dart +++ b/packages/at_register/lib/src/util/validate_otp_task_result.dart @@ -1,5 +1,7 @@ import '../../at_register.dart'; +/// This class object if for internal use +/// Used to return class ValidateOtpResult extends RegisterTaskResult { ValidateOtpStatus? taskStatus; } diff --git a/packages/at_register/pubspec.yaml b/packages/at_register/pubspec.yaml index 204bfde2..ea6db1ca 100644 --- a/packages/at_register/pubspec.yaml +++ b/packages/at_register/pubspec.yaml @@ -1,17 +1,17 @@ name: at_register description: Package that has code to interact with the AtRegistrar API version: 1.0.0 -# repository: https://github.com/my_org/my_repo +repository: https://github.com/atsign-foundation/at_libraries environment: sdk: ^3.2.4 dependencies: - at_commons: ^4.0.1 + at_commons: ^4.0.3 at_utils: ^3.0.16 - http: ^1.2.0 - mocktail: ^1.0.3 + http: ^1.2.1 dev_dependencies: lints: ^3.0.0 test: ^1.24.0 + mocktail: ^1.0.3 diff --git a/packages/at_register/test/at_register_allow_retry_false_test.dart b/packages/at_register/test/at_register_allow_retry_false_test.dart index b4adedfe..c2ebb5d8 100644 --- a/packages/at_register/test/at_register_allow_retry_false_test.dart +++ b/packages/at_register/test/at_register_allow_retry_false_test.dart @@ -1,3 +1,4 @@ +import 'package:at_commons/at_commons.dart'; import 'package:at_register/at_register.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; @@ -8,6 +9,52 @@ void main() { MockRegistrarApiAccessor mockRegistrarApiAccessor = MockRegistrarApiAccessor(); + group('Validate individual tasks behaviour with invalid params', () { + test('validate behaviour of GetFreeAtsign', () async { + RegisterParams params = RegisterParams()..email = ''; + + expect( + () => GetFreeAtsign(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor), + throwsA(predicate((e) => e is IllegalArgumentException))); + }); + + test('validate behaviour of RegisterAtsign', () async { + RegisterParams params = RegisterParams() + ..email = 'email@email' + ..atsign = null; + + expect( + () => RegisterAtsign(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor), + throwsA(predicate((e) => e is IllegalArgumentException))); + }); + + test('validate behaviour of ValidateOtp with null atsign', () async { + RegisterParams params = RegisterParams() + ..email = 'email@email' + ..otp = null; + + expect( + () => ValidateOtp(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor), + throwsA(predicate((e) => e is IllegalArgumentException))); + }); + + test('validate behaviour of ValidateOtp with null verification code', + () async { + RegisterParams params = RegisterParams() + ..email = 'email@email' + ..atsign = '@abcd' + ..otp = null; + + expect( + () => ValidateOtp(params, + registrarApiAccessorInstance: mockRegistrarApiAccessor), + throwsA(predicate((e) => e is InvalidVerificationCodeException))); + }); + }); + group('A group of tests to validate GetFreeAtsign', () { setUp(() => resetMocktailState()); @@ -205,6 +252,9 @@ void main() { RegisterTaskResult result = await validateOtpTask.run(); expect(params.confirmation, true); // confirmation set to true by the Task expect(result.apiCallStatus, ApiCallStatus.retry); + // expect(result.data[RegistrarConstants.fetchedAtsignListName], + // ['@old-atsign']); + expect(result.data[RegistrarConstants.newAtsignName], atsign); // The above case is when an email has already existing atsigns, select an atsign // from the list and retry the task with confirmation set to 'true' @@ -215,28 +265,6 @@ void main() { expect(result.data[RegistrarConstants.cramKeyName], cram); }); - test('validate behaviour of ValidateOtp task - null or empty otp', - () async { - String atsign = '@charlie-otp-retry'; - String email = 'third-group-test-3@email'; - String? otp; // invalid null otp - - var params = RegisterParams() - ..atsign = atsign - ..confirmation = false - ..email = email - ..otp = otp; - ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); - - expect(() async => await validateOtpTask.run(), - throwsA(predicate((e) => e is InvalidVerificationCodeException))); - - params.otp = ''; - expect(() async => await validateOtpTask.run(), - throwsA(predicate((e) => e is InvalidVerificationCodeException))); - }); - test('validate behaviour of ValidateOtp task - incorrect otp', () async { String atsign = '@charlie-otp-incorrect'; String email = 'third-group-test-3-3@email'; @@ -250,8 +278,7 @@ void main() { ValidateOtpResult validateOtpResult = ValidateOtpResult() ..taskStatus = ValidateOtpStatus.retry - ..apiCallStatus = ApiCallStatus.success - ..exceptionMessage = 'incorrect otp'; + ..apiCallStatus = ApiCallStatus.success; when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, confirmation: false)) .thenAnswer((invocation) => Future.value(validateOtpResult)); @@ -263,7 +290,7 @@ void main() { () async => await validateOtpTask.run(), throwsA(predicate((e) => e is InvalidVerificationCodeException && - e.message.contains('incorrect otp')))); + e.message.contains('Incorrect verification code provided')))); }); test( diff --git a/packages/at_register/test/at_register_allow_retry_true_test.dart b/packages/at_register/test/at_register_allow_retry_true_test.dart index bd34341c..d058b563 100644 --- a/packages/at_register/test/at_register_allow_retry_true_test.dart +++ b/packages/at_register/test/at_register_allow_retry_true_test.dart @@ -22,8 +22,9 @@ void main() { RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; GetFreeAtsign getFreeAtsign = GetFreeAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); - final result = await getFreeAtsign.run(allowRetry: true); + registrarApiAccessorInstance: mockRegistrarApiAccessor, + allowRetry: true); + final result = await getFreeAtsign.run(); expect(result.data[RegistrarConstants.atsignName], '@alice'); }); @@ -35,8 +36,9 @@ void main() { RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; GetFreeAtsign getFreeAtsign = GetFreeAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); - RegisterTaskResult? result = await getFreeAtsign.run(allowRetry: true); + registrarApiAccessorInstance: mockRegistrarApiAccessor, + allowRetry: true); + RegisterTaskResult? result = await getFreeAtsign.run(); expect(result.apiCallStatus, ApiCallStatus.retry); assert(result.exceptionMessage!.contains(testExceptionMessage)); @@ -57,9 +59,9 @@ void main() { ..atsign = atsign ..email = email; RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); - RegisterTaskResult result = - await registerAtsignTask.run(allowRetry: true); + registrarApiAccessorInstance: mockRegistrarApiAccessor, + allowRetry: true); + RegisterTaskResult result = await registerAtsignTask.run(); expect(result.apiCallStatus, ApiCallStatus.success); expect(result.data['otpSent'], 'true'); }); @@ -75,9 +77,9 @@ void main() { ..atsign = atsign ..email = email; RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); - RegisterTaskResult result = - await registerAtsignTask.run(allowRetry: true); + registrarApiAccessorInstance: mockRegistrarApiAccessor, + allowRetry: true); + RegisterTaskResult result = await registerAtsignTask.run(); expect(result.apiCallStatus, ApiCallStatus.success); expect(result.data['otpSent'], 'false'); @@ -96,9 +98,9 @@ void main() { ..email = email; RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); - RegisterTaskResult? result = - await registerAtsignTask.run(allowRetry: true); + registrarApiAccessorInstance: mockRegistrarApiAccessor, + allowRetry: true); + RegisterTaskResult? result = await registerAtsignTask.run(); expect(registerAtsignTask.shouldRetry(), true); assert(result.exceptionMessage!.contains(testException)); @@ -117,9 +119,10 @@ void main() { ..atsign = atsign ..email = email; RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); + registrarApiAccessorInstance: mockRegistrarApiAccessor, + allowRetry: true); - var result = await registerAtsignTask.run(allowRetry: true); + var result = await registerAtsignTask.run(); assert(result.exceptionMessage!.contains(testExceptionMessage)); expect(registerAtsignTask.retryCount, 1); }); @@ -152,8 +155,9 @@ void main() { ..otp = otp; ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); - RegisterTaskResult result = await validateOtpTask.run(allowRetry: true); + registrarApiAccessorInstance: mockRegistrarApiAccessor, + allowRetry: true); + RegisterTaskResult result = await validateOtpTask.run(); expect(result.data[RegistrarConstants.cramKeyName], cram); }); @@ -194,8 +198,9 @@ void main() { ..otp = otp; ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); - RegisterTaskResult result = await validateOtpTask.run(allowRetry: true); + registrarApiAccessorInstance: mockRegistrarApiAccessor, + allowRetry: true); + RegisterTaskResult result = await validateOtpTask.run(); expect(params.confirmation, true); // confirmation set to true by RegisterTask expect(result.apiCallStatus, ApiCallStatus.retry); @@ -204,7 +209,7 @@ void main() { // The above case is when an email has already existing atsigns, select an atsign // from the list and retry the task with confirmation set to 'true' params.atsign = atsign2; - result = await validateOtpTask.run(allowRetry: true); + result = await validateOtpTask.run(); expect(result.apiCallStatus, ApiCallStatus.success); expect(result.data[RegistrarConstants.cramKeyName], cram); }); @@ -231,9 +236,10 @@ void main() { ..email = email ..otp = otp; ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); + registrarApiAccessorInstance: mockRegistrarApiAccessor, + allowRetry: true); - RegisterTaskResult result = await validateOtpTask.run(allowRetry: true); + RegisterTaskResult result = await validateOtpTask.run(); expect(result.apiCallStatus, ApiCallStatus.retry); }); @@ -256,9 +262,10 @@ void main() { MaximumAtsignQuotaException('maximum free atsign limit reached')); ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); + registrarApiAccessorInstance: mockRegistrarApiAccessor, + allowRetry: true); - expect(() async => await validateOtpTask.run(allowRetry: true), + expect(() async => await validateOtpTask.run(), throwsA(predicate((e) => e is MaximumAtsignQuotaException))); }); }); From 03e5b093d1f77c8568767f2ffd743f422c6c9910 Mon Sep 17 00:00:00 2001 From: Srie Teja Date: Wed, 6 Mar 2024 03:46:56 +0530 Subject: [PATCH 11/16] fix: add files in onboarding_cli that were previously removed --- packages/at_onboarding_cli/Dockerfile | 30 -------------- .../at_onboarding_cli/bin/delete_test.dart | 0 .../at_onboarding_cli/bin/register_cli.dart | 2 +- packages/at_onboarding_cli/example/README.md | 40 +++++++++++++++++++ .../apkam_examples/enroll_app_listen.dart | 2 +- .../example/get_cram_key.dart | 8 ++-- .../onboard/at_onboarding_service_impl.dart | 4 +- .../src/util/at_onboarding_preference.dart | 4 +- packages/at_onboarding_cli/pubspec.yaml | 9 +++-- 9 files changed, 55 insertions(+), 44 deletions(-) delete mode 100644 packages/at_onboarding_cli/Dockerfile create mode 100644 packages/at_onboarding_cli/bin/delete_test.dart create mode 100644 packages/at_onboarding_cli/example/README.md diff --git a/packages/at_onboarding_cli/Dockerfile b/packages/at_onboarding_cli/Dockerfile deleted file mode 100644 index b3f18b91..00000000 --- a/packages/at_onboarding_cli/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -FROM dart:2.19.4@sha256:5d2f42239d719bef7fe60abb37273aea16d8526980662b696c218663ac005d96 AS buildimage -ENV HOMEDIR=/atsign -ENV BINARYDIR=${HOMEDIR}/at_activate -ENV USER_ID=1024 -ENV GROUP_ID=1024 -WORKDIR ${HOMEDIR} -# Context for this Dockerfile needs to be {at_libraries_repo}/packages/at_onboarding_cli -# If building manually then (from packages/at_onboarding_cli): -## docker build -t atsigncompany/at_activate . -COPY . . -RUN \ - mkdir -p "$HOMEDIR" ; \ - mkdir -p "$BINARYDIR" ; \ - case "$(dpkg --print-architecture)" in \ - amd64) \ - ARCH="x64";; \ - armhf) \ - ARCH="arm";; \ - arm64) \ - ARCH="arm64";; \ - esac; \ - dart pub get ; \ - dart pub upgrade ; \ - dart compile exe bin/activate_cli.dart -o "$BINARYDIR"/at_activate ; \ - tar cvzf /atsign/at_activate-linux-"$ARCH".tgz "$BINARYDIR"/at_activate - - -# Second stage of build FROM scratch -FROM scratch AS export-stage -COPY --from=buildimage /atsign/at_activate-linux-*.tgz . \ No newline at end of file diff --git a/packages/at_onboarding_cli/bin/delete_test.dart b/packages/at_onboarding_cli/bin/delete_test.dart new file mode 100644 index 00000000..e69de29b diff --git a/packages/at_onboarding_cli/bin/register_cli.dart b/packages/at_onboarding_cli/bin/register_cli.dart index a6cca120..237a5517 100644 --- a/packages/at_onboarding_cli/bin/register_cli.dart +++ b/packages/at_onboarding_cli/bin/register_cli.dart @@ -3,4 +3,4 @@ import 'package:at_onboarding_cli/src/register_cli/register.dart' Future main(List args) async { await register_cli.main(args); -} +} \ No newline at end of file diff --git a/packages/at_onboarding_cli/example/README.md b/packages/at_onboarding_cli/example/README.md new file mode 100644 index 00000000..495dce87 --- /dev/null +++ b/packages/at_onboarding_cli/example/README.md @@ -0,0 +1,40 @@ +List of steps to run the examples for checking apkam enrollment + +1. Onboard an atsign which has privilege to approve/deny enrollments:
+ - run: `dart example/onboard.dart -a -c -k `
+ - e.g. `dart example/onboard.dart -a @alice -k /home/alice/.atsign/@alice_wavikey.atKeys -c b26455a907582760ebf35bc4847de549bc41c24b25c8b1c58d5964f7b4f8a43bc55b0e9a601c9a9657d9a8b8bbc32f88b4e38ffaca03c8710ebae1b14ca9f364`
+ - If you do not already have the CRAM Secret for your atsign + run: `dart example/get_cram_key.dart -a <@atsign>` +2. Authenticate using the onboarded atsign:
+ - run: `dart example/apkam_authenticate.dart -a -k `
+ - e.g. `dart example/apkam_examples/apkam_authenticate.dart -a @alice -k /home/alice/.atsign/@alice_wavikey.atKeys` +3. Run client to approve enrollments:
+ - run: `dart example/enroll_app_listen.dart -a -k `
+ - e.g `dart example/apkam_examples/enroll_app_listen.dart -a @alice -k /home/alice/.atsign/@alice_wavikey.atKeys` +4. Get OTP for enrollment + - 4.1 Perform a PKAM authentication through the ssl client + - 4.1.1 Get the challenge from the atServer:
+ - run: `from:<@atsign>` e.g. `from:@alice`
+ - This generates a string which is called the challenge which will be used to generate the authentication token
+ - 4.1.2 Create a pkamSignature that can be used to authenticate yourself
+ - Clone at_tools from https://github.com/atsign-foundation/at_tools.git + - Change directory into 'at_tools/packages/at_pkam>'
+ - run: `dart bin/main.dart -p `
+ - e.g `dart bin/main.dart -p /home/alice/.atsign/@alice_wavikey.atKeys -r _70138292-07b5-4e47-8c94-e02e38220775@alice:883ea0aa-c526-400a-926e-48cae9281de9`
+ - This should generate a hash, which is called the pkamSignature which will be used to authenticate into the atServer
+ - 4.1.3 Now that a pkamSignature is generated, use it to authenticate
+ run:`pkam:enrollmentId::` [enrollmentId - get it from the .atKeys file]
+ - 4.2 Once authenticated run `otp:get`
+ - Now copy the 6-digit alpha-numeric code which is the OTP +5. Request enrollment + - 5.1 Submit enrollment from new client:
+ - run:`dart example/apkam_examples/apkam_enroll.dart -a -k -o `
+ - Note: this path has to be different from the path provided in Step#1 as this is a new file + - e.g. `dart example/apkam_examples/apkam_enroll.dart -a @alice -k /home/alice/.atsign/@alice_buzzkey.atKeys -o DY4UT4`
+ - 5.2 Approve the enrollment from the client from #3
+ - To approve the enrollment type `yes` and then Enter + - 5.3 Enrollment should be successful and atKeys file stored in the path specified +6. Authenticate using the enrolled keys file
+ - 6.1 run: `dart example/apkam_examples/apkam_authenticate.dart -a -k ` + - Note: this keys file is different from the keys file generated in Step#1. This new file only has access to the data that is allowed to access from this enrollment_id + diff --git a/packages/at_onboarding_cli/example/apkam_examples/enroll_app_listen.dart b/packages/at_onboarding_cli/example/apkam_examples/enroll_app_listen.dart index c263d09f..fe424301 100644 --- a/packages/at_onboarding_cli/example/apkam_examples/enroll_app_listen.dart +++ b/packages/at_onboarding_cli/example/apkam_examples/enroll_app_listen.dart @@ -65,7 +65,7 @@ Future _notificationCallback(AtNotification notification, atAuthKeys.defaultEncryptionPrivateKey!, apkamSymmetricKey); var encryptedDefaultSelfEncKey = EncryptionUtil.encryptValue( atAuthKeys.defaultSelfEncryptionKey!, apkamSymmetricKey); - enrollParamsJson['encryptedDefaultEncryptionPrivateKey'] = + enrollParamsJson['encryptedDefaultEncryptedPrivateKey'] = encryptedDefaultPrivateEncKey; enrollParamsJson['encryptedDefaultSelfEncryptionKey'] = encryptedDefaultSelfEncKey; diff --git a/packages/at_onboarding_cli/example/get_cram_key.dart b/packages/at_onboarding_cli/example/get_cram_key.dart index 1af1b0f5..b728503d 100644 --- a/packages/at_onboarding_cli/example/get_cram_key.dart +++ b/packages/at_onboarding_cli/example/get_cram_key.dart @@ -1,5 +1,5 @@ import 'package:args/args.dart'; -import 'package:at_onboarding_cli/src/util/onboarding_util.dart'; +import 'package:at_register/at_register.dart'; import 'util/custom_arg_parser.dart'; @@ -7,12 +7,12 @@ Future main(args) async { final argResults = CustomArgParser(getArgParser()).parse(args); // this step sends an OTP to the registered email - await OnboardingUtil().requestAuthenticationOtp( + await RegistrarApiAccessor().requestAuthenticationOtp( argResults['atsign']); // requires a registered atsign // the following step validates the email that was sent in the above step - String? verificationCode = OnboardingUtil().getVerificationCodeFromUser(); - String cramKey = await OnboardingUtil().getCramKey(argResults['atsign'], + String? verificationCode = ApiUtil.readCliVerificationCode(); + String cramKey = await RegistrarApiAccessor().getCramKey(argResults['atsign'], verificationCode); // verification code received on the registered email print('Your cram key is: $cramKey'); diff --git a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart index 5284dc66..ca4b4c82 100644 --- a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart +++ b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart @@ -18,9 +18,9 @@ import 'package:encrypt/encrypt.dart'; import 'package:zxing2/qrcode.dart'; import 'package:image/image.dart'; import 'package:path/path.dart' as path; +import 'package:at_register/at_register.dart'; import '../util/home_directory_util.dart'; -import '../util/onboarding_util.dart'; ///class containing service that can onboard/activate/authenticate @signs class AtOnboardingServiceImpl implements AtOnboardingService { @@ -97,7 +97,7 @@ class AtOnboardingServiceImpl implements AtOnboardingService { // get cram_secret from either from AtOnboardingPreference // or fetch from the registrar using verification code sent to email - atOnboardingPreference.cramSecret ??= await OnboardingUtil() + atOnboardingPreference.cramSecret ??= await RegistrarApiAccessor() .getCramUsingOtp(_atSign, atOnboardingPreference.registrarUrl); if (atOnboardingPreference.cramSecret == null) { logger.info('Root Server address is ${atOnboardingPreference.rootDomain}:' diff --git a/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart b/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart index b2f3ee1e..0292a3a5 100644 --- a/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart +++ b/packages/at_onboarding_cli/lib/src/util/at_onboarding_preference.dart @@ -2,7 +2,7 @@ import 'dart:core'; import 'package:at_chops/at_chops.dart'; import 'package:at_client/at_client.dart'; -import 'package:at_onboarding_cli/src/util/registrar_api_constants.dart'; +import 'package:at_register/at_register.dart'; class AtOnboardingPreference extends AtClientPreference { /// specify path of .atKeysFile containing encryption keys @@ -28,7 +28,7 @@ class AtOnboardingPreference extends AtClientPreference { bool skipSync = false; /// the hostName of the registrar which will be used to activate the atsign - String registrarUrl = RegistrarApiConstants.apiHostProd; + String registrarUrl = RegistrarConstants.apiHostProd; String? appName; diff --git a/packages/at_onboarding_cli/pubspec.yaml b/packages/at_onboarding_cli/pubspec.yaml index acf0c6de..016c4dbc 100644 --- a/packages/at_onboarding_cli/pubspec.yaml +++ b/packages/at_onboarding_cli/pubspec.yaml @@ -30,10 +30,11 @@ dependencies: dependency_overrides: at_register: - git: - url: https://github.com/atsign-foundation/at_libraries.git - path: packages/at_register - ref: at_register_package + path: /home/srie/Desktop/work/at_libraries/packages/at_register +# git: +# url: https://github.com/atsign-foundation/at_libraries.git +# path: packages/at_register +# ref: at_register_package dev_dependencies: lints: ^2.1.0 From ee1222194f9b953a4eea92167610f209d349b988 Mon Sep 17 00:00:00 2001 From: Srie Teja Date: Wed, 6 Mar 2024 03:50:00 +0530 Subject: [PATCH 12/16] fix: at_register dep_override --- packages/at_onboarding_cli/Dockerfile | 30 +++++++++++++++++++++++++ packages/at_onboarding_cli/pubspec.yaml | 9 ++++---- 2 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 packages/at_onboarding_cli/Dockerfile diff --git a/packages/at_onboarding_cli/Dockerfile b/packages/at_onboarding_cli/Dockerfile new file mode 100644 index 00000000..b3f18b91 --- /dev/null +++ b/packages/at_onboarding_cli/Dockerfile @@ -0,0 +1,30 @@ +FROM dart:2.19.4@sha256:5d2f42239d719bef7fe60abb37273aea16d8526980662b696c218663ac005d96 AS buildimage +ENV HOMEDIR=/atsign +ENV BINARYDIR=${HOMEDIR}/at_activate +ENV USER_ID=1024 +ENV GROUP_ID=1024 +WORKDIR ${HOMEDIR} +# Context for this Dockerfile needs to be {at_libraries_repo}/packages/at_onboarding_cli +# If building manually then (from packages/at_onboarding_cli): +## docker build -t atsigncompany/at_activate . +COPY . . +RUN \ + mkdir -p "$HOMEDIR" ; \ + mkdir -p "$BINARYDIR" ; \ + case "$(dpkg --print-architecture)" in \ + amd64) \ + ARCH="x64";; \ + armhf) \ + ARCH="arm";; \ + arm64) \ + ARCH="arm64";; \ + esac; \ + dart pub get ; \ + dart pub upgrade ; \ + dart compile exe bin/activate_cli.dart -o "$BINARYDIR"/at_activate ; \ + tar cvzf /atsign/at_activate-linux-"$ARCH".tgz "$BINARYDIR"/at_activate + + +# Second stage of build FROM scratch +FROM scratch AS export-stage +COPY --from=buildimage /atsign/at_activate-linux-*.tgz . \ No newline at end of file diff --git a/packages/at_onboarding_cli/pubspec.yaml b/packages/at_onboarding_cli/pubspec.yaml index 016c4dbc..acf0c6de 100644 --- a/packages/at_onboarding_cli/pubspec.yaml +++ b/packages/at_onboarding_cli/pubspec.yaml @@ -30,11 +30,10 @@ dependencies: dependency_overrides: at_register: - path: /home/srie/Desktop/work/at_libraries/packages/at_register -# git: -# url: https://github.com/atsign-foundation/at_libraries.git -# path: packages/at_register -# ref: at_register_package + git: + url: https://github.com/atsign-foundation/at_libraries.git + path: packages/at_register + ref: at_register_package dev_dependencies: lints: ^2.1.0 From 8d6424ad86b39fb8b19ee6ba3e51e8b07e99a2da Mon Sep 17 00:00:00 2001 From: Srie Teja Date: Thu, 7 Mar 2024 04:30:16 +0530 Subject: [PATCH 13/16] fix: failing tests + more refactoring --- .../lib/src/register_cli/register.dart | 15 +-- .../src/register_cli/registration_flow.dart | 44 +++++--- ...er_test.dart => at_register_cli_test.dart} | 100 ++++++++--------- packages/at_register/README.md | 20 ++-- .../example/at_register_example.dart | 18 +-- .../example/at_register_usage_explained.dart | 20 ++-- packages/at_register/lib/at_register.dart | 2 +- .../src/api-interactions/get_free_atsign.dart | 29 ++--- .../src/api-interactions/register_atsign.dart | 55 ++++++--- .../registrar_api_accessor.dart | 35 +++--- .../src/api-interactions/validate_otp.dart | 79 ++++++++----- .../at_register/lib/src/util/api_util.dart | 19 ++-- .../lib/src/util/at_register_exception.dart | 19 ++-- .../lib/src/util/register_task.dart | 33 ++---- .../lib/src/util/register_task_result.dart | 6 +- packages/at_register/pubspec.yaml | 2 +- .../at_register_allow_retry_false_test.dart | 105 +++++++++--------- .../at_register_allow_retry_true_test.dart | 88 +++++++-------- 18 files changed, 361 insertions(+), 328 deletions(-) rename packages/at_onboarding_cli/test/{at_register_test.dart => at_register_cli_test.dart} (73%) diff --git a/packages/at_onboarding_cli/lib/src/register_cli/register.dart b/packages/at_onboarding_cli/lib/src/register_cli/register.dart index 7a6e5cf2..9a2a4f01 100644 --- a/packages/at_onboarding_cli/lib/src/register_cli/register.dart +++ b/packages/at_onboarding_cli/lib/src/register_cli/register.dart @@ -46,18 +46,19 @@ class Register { exit(7); } - GetFreeAtsign getFreeAtsignTask = GetFreeAtsign(registerParams, - registrarApiAccessorInstance: registrarApiAccessor, ); + GetFreeAtsign getFreeAtsignTask = GetFreeAtsign( + apiAccessorInstance: registrarApiAccessor, + ); - RegisterAtsign registerAtsignTask = RegisterAtsign(registerParams, - registrarApiAccessorInstance: registrarApiAccessor); + RegisterAtsign registerAtsignTask = + RegisterAtsign(apiAccessorInstance: registrarApiAccessor); - ValidateOtp validateOtpTask = ValidateOtp(registerParams, - registrarApiAccessorInstance: registrarApiAccessor); + ValidateOtp validateOtpTask = + ValidateOtp(apiAccessorInstance: registrarApiAccessor); // create a queue of tasks each of type [RegisterTask] and then // call start on the RegistrationFlow object - await RegistrationFlow() + await RegistrationFlow(registerParams) .add(getFreeAtsignTask) .add(registerAtsignTask) .add(validateOtpTask) diff --git a/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart b/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart index 5d76f7fe..33c8aada 100644 --- a/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart +++ b/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart @@ -7,9 +7,10 @@ import 'package:at_register/at_register.dart'; class RegistrationFlow { List processQueue = []; RegisterTaskResult _result = RegisterTaskResult(); - RegisterParams params = RegisterParams(); + late RegisterParams params; + String defaultExceptionMessage = 'Could not complete the task. Please retry'; - RegistrationFlow(); + RegistrationFlow(this.params); RegistrationFlow add(RegisterTask task) { processQueue.add(task); @@ -18,22 +19,35 @@ class RegistrationFlow { Future start() async { for (RegisterTask task in processQueue) { - // setting allowRetry to false as this method has logic to retry each + // setting allowRetry to true as this method has logic to retry each // failed task 3-times and then throw an exception if still failing - _result = await task.run(); - task.logger.finer('Attempt: ${task.retryCount} | params[$params]'); - task.logger.finer('Result: $_result'); + try { + _result = await task.run(params); + task.logger.finer('Attempt: ${task.retryCount} | params[$params]'); + task.logger.finer('Result: $_result'); - if (_result.apiCallStatus == ApiCallStatus.retry) { - while ( - task.shouldRetry() && _result.apiCallStatus == ApiCallStatus.retry) { - _result = await task.retry(); + while (_result.apiCallStatus == ApiCallStatus.retry && + task.shouldRetry()) { + _result = await task.retry(params); + task.logger.finer('Attempt: ${task.retryCount} | params[$params]'); + task.logger.finer('Result: $_result'); } - } - if (_result.apiCallStatus == ApiCallStatus.success) { - params.addFromJson(_result.data); - } else { - throw AtRegisterException(_result.exceptionMessage!); + if (_result.apiCallStatus == ApiCallStatus.success) { + params.addFromJson(_result.data); + } else { + throw _result.exception ?? + AtRegisterException('${task.name}: $defaultExceptionMessage'); + } + } on MaximumAtsignQuotaException { + rethrow; + } on ExhaustedVerificationCodeRetriesException { + rethrow; + } on InvalidVerificationCodeException { + rethrow; + } on AtRegisterException { + rethrow; + } on Exception catch (e) { + throw AtRegisterException(e.toString()); } } return _result; diff --git a/packages/at_onboarding_cli/test/at_register_test.dart b/packages/at_onboarding_cli/test/at_register_cli_test.dart similarity index 73% rename from packages/at_onboarding_cli/test/at_register_test.dart rename to packages/at_onboarding_cli/test/at_register_cli_test.dart index 8ec06d03..ca704c05 100644 --- a/packages/at_onboarding_cli/test/at_register_test.dart +++ b/packages/at_onboarding_cli/test/at_register_cli_test.dart @@ -15,14 +15,14 @@ void main() { test('validate behaviour of GetFreeAtsign - encounters exception', () async { - when(() => accessorInstance.getFreeAtSigns()) + when(() => accessorInstance.getFreeAtSign()) .thenThrow(Exception('random exception')); RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; - GetFreeAtsign getFreeAtsign = GetFreeAtsign(params, - registrarApiAccessorInstance: accessorInstance, allowRetry: true); + GetFreeAtsign getFreeAtsign = GetFreeAtsign( + apiAccessorInstance: accessorInstance, allowRetry: true); try { - await RegistrationFlow().add(getFreeAtsign).start(); + await RegistrationFlow(params).add(getFreeAtsign).start(); } on Exception catch (e) { expect(e.runtimeType, AtRegisterException); expect(e.toString().contains('random exception'), true); @@ -45,35 +45,16 @@ void main() { RegisterParams params = RegisterParams() ..atsign = atsign ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: accessorInstance, allowRetry: true); - - await RegistrationFlow().add(registerAtsignTask).start(); - expect(registerAtsignTask.retryCount, registerAtsignTask.maximumRetries); - expect(registerAtsignTask.shouldRetry(), false); - }); - - test('verify behaviour of RegisterAtsign processing an exception', - () async { - String atsign = '@bobby'; - String email = 'second-group@email'; - when(() => accessorInstance.registerAtSign(atsign, email)) - .thenThrow(Exception('another random exception')); - - RegisterParams params = RegisterParams() - ..atsign = atsign - ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: accessorInstance, allowRetry: true); - - expect(registerAtsignTask.retryCount, registerAtsignTask.maximumRetries); - expect(registerAtsignTask.shouldRetry(), false); + RegisterAtsign registerAtsignTask = RegisterAtsign( + apiAccessorInstance: accessorInstance, allowRetry: true); try { - await RegistrationFlow().add(registerAtsignTask).start(); - } on Exception catch (e) { - assert(e is AtRegisterException && - e.message.contains('another random exception')); + await RegistrationFlow(params).add(registerAtsignTask).start(); + } on Exception catch(e){ + expect(e.runtimeType, AtRegisterException); + assert(e.toString().contains('Could not complete the task')); } + expect(registerAtsignTask.retryCount, registerAtsignTask.maximumRetries); + expect(registerAtsignTask.shouldRetry(), false); }); test( @@ -87,16 +68,18 @@ void main() { RegisterParams params = RegisterParams() ..atsign = atsign ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: accessorInstance, allowRetry: true); - + RegisterAtsign registerAtsignTask = RegisterAtsign( + apiAccessorInstance: accessorInstance, allowRetry: true); + bool exceptionFlag = false; try { - await RegistrationFlow().add(registerAtsignTask).start(); + await RegistrationFlow(params).add(registerAtsignTask).start(); } on Exception catch (e) { - print(e.toString()); assert(e.toString().contains('another new random exception')); + exceptionFlag = true; } - expect(registerAtsignTask.retryCount, 3); + expect(exceptionFlag, true); + expect( + registerAtsignTask.retryCount, registerAtsignTask.maximumRetries); expect(registerAtsignTask.shouldRetry(), false); }); }); @@ -141,10 +124,10 @@ void main() { ..email = email ..otp = otp; - ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: accessorInstance, allowRetry: true); + ValidateOtp validateOtpTask = + ValidateOtp(apiAccessorInstance: accessorInstance, allowRetry: true); RegisterTaskResult result = - await RegistrationFlow().add(validateOtpTask).start(); + await RegistrationFlow(params).add(validateOtpTask).start(); expect(result.data[RegistrarConstants.cramKeyName], cram); expect(params.confirmation, true); @@ -176,11 +159,18 @@ void main() { ..email = email ..otp = otp; - ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: accessorInstance, allowRetry: true); - expect(() async => await RegistrationFlow().add(validateOtpTask).start(), - throwsA(ExhaustedVerificationCodeRetriesException)); - + ValidateOtp validateOtpTask = + ValidateOtp(apiAccessorInstance: accessorInstance, allowRetry: true); + bool exceptionCaughtFlag = false; + try { + await RegistrationFlow(params).add(validateOtpTask).start(); + } on Exception catch (e) { + print(e.runtimeType); + print(e.toString()); + assert(e is ExhaustedVerificationCodeRetriesException); + exceptionCaughtFlag = true; + } + expect(exceptionCaughtFlag, true); expect(validateOtpTask.retryCount, validateOtpTask.maximumRetries); expect(validateOtpTask.shouldRetry(), false); }); @@ -194,8 +184,9 @@ void main() { String email = 'lewis44@gmail.com'; String cram = 'craaaaaaaaaaaaam'; String otp = 'Agbr'; + // mock for get-free-atsign - when(() => accessorInstance.getFreeAtSigns()) + when(() => accessorInstance.getFreeAtSign()) .thenAnswer((invocation) => Future.value(atsign)); // mock for register-atsign when(() => accessorInstance.registerAtSign(atsign, email)) @@ -226,23 +217,24 @@ void main() { ..otp = otp ..confirmation = false; - GetFreeAtsign getFreeAtsignTask = GetFreeAtsign(params, - registrarApiAccessorInstance: accessorInstance, allowRetry: true); + GetFreeAtsign getFreeAtsignTask = GetFreeAtsign( + apiAccessorInstance: accessorInstance, allowRetry: true); - RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: accessorInstance, allowRetry: true); + RegisterAtsign registerAtsignTask = RegisterAtsign( + apiAccessorInstance: accessorInstance, allowRetry: true); - ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: accessorInstance, allowRetry: true); + ValidateOtp validateOtpTask = + ValidateOtp(apiAccessorInstance: accessorInstance, allowRetry: true); - await RegistrationFlow() + print(await accessorInstance.getFreeAtSign()); + RegisterTaskResult result = await RegistrationFlow(params) .add(getFreeAtsignTask) .add(registerAtsignTask) .add(validateOtpTask) .start(); expect(params.atsign, atsign); - expect(params.cram, cram); + expect(result.data[RegistrarConstants.cramKeyName], cram); expect(params.confirmation, true); }); }); diff --git a/packages/at_register/README.md b/packages/at_register/README.md index f6cc65d9..56a961c8 100644 --- a/packages/at_register/README.md +++ b/packages/at_register/README.md @@ -21,7 +21,7 @@ please feel free to take a look at `at_onboarding_cli/register_cli` To add this package as the dependency, add it to your pubspec.yaml -```dart +```yaml dependencies: at_register: ^1.0.0 ``` @@ -51,27 +51,27 @@ RegisterParams params = RegisterParams()..email = 'email@email.com'; ### 1) To fetch a free atsign ```dart -GetFreeAtsign getFreeAtsignTask = GetFreeAtsign(params); -RegisterTaskResult result = await getFreeAtsignTask.run(); -print(result.data['atsign']); +GetFreeAtsign getFreeAtsignTask = GetFreeAtsign(); +RegisterTaskResult result = await getFreeAtsignTask.run(params); +print(result.data[RegistrarConstants.atsignName]); ``` ### 2) To register the free atsign fetched in (1) ```dart params.atsign = '@alice'; // preferably use the one fetched in (1) -RegisterAtsign registerAtsignTask = RegisterAtsign(params); -RegisterTaskResult result = await registerAtsignTask.run(); -print(result.data['otpSent']); // contains true/false if verification code was delivered to email +RegisterAtsign registerAtsignTask = RegisterAtsign(); +RegisterTaskResult result = await registerAtsignTask.run(params); +print(result.data[RegistrarConstants.otpSentName]); // contains true/false if verification code was delivered to email ``` ### 3) To validate the verification code ```dart params.otp = 'AB1C'; // to be fetched from user -ValidateOtp validateOtpTask = ValidateOtp(params); -RegisterTaskResult result = await validateOtp.run(); -print(result.data['cram']); +ValidateOtp validateOtpTask = ValidateOtp(); +RegisterTaskResult result = await validateOtp.run(params); +print(result.data[RegistrarConstants.cramKeyName]); ``` Please refer to [examples](https://github.com/atsign-foundation/at_libraries/blob/doc_at_lookup/at_lookup/example/bin/example.dart) for more details. diff --git a/packages/at_register/example/at_register_example.dart b/packages/at_register/example/at_register_example.dart index eb40b25f..8b8ee720 100644 --- a/packages/at_register/example/at_register_example.dart +++ b/packages/at_register/example/at_register_example.dart @@ -4,19 +4,19 @@ void main() async { String email = ''; RegisterParams params = RegisterParams()..email = email; - GetFreeAtsign getFreeAtsignTask = GetFreeAtsign(params); - RegisterTaskResult result = await getFreeAtsignTask.run(); + GetFreeAtsign getFreeAtsignTask = GetFreeAtsign(); + RegisterTaskResult result = await getFreeAtsignTask.run(params); params.addFromJson(result.data); - RegisterAtsign registerAtsignTask = RegisterAtsign(params); - result = await registerAtsignTask.run(); + RegisterAtsign registerAtsignTask = RegisterAtsign(); + result = await registerAtsignTask.run(params); params.addFromJson(result.data); // verification code sent to email provided in the beginning // check the same email and enter that verification code through terminal/stdin params.otp = ApiUtil.readCliVerificationCode(); - ValidateOtp validateOtpTask = ValidateOtp(params); - result = await validateOtpTask.run(); + ValidateOtp validateOtpTask = ValidateOtp(); + result = await validateOtpTask.run(params); if(result.apiCallStatus == ApiCallStatus.success){ print(result.data[RegistrarConstants.cramKeyName]); } else { @@ -24,9 +24,9 @@ void main() async { // set task.params.confirmation to true, select an atsign (existing/new) from // the String newAtsign = result.data[RegistrarConstants.newAtsignName]; - validateOtpTask.registerParams.atsign = newAtsign; - validateOtpTask.registerParams.confirmation = true; - result = await validateOtpTask.run(); + params.atsign = newAtsign; + params.confirmation = true; + result = await validateOtpTask.run(params); print(result.data[RegistrarConstants.cramKeyName]); } } \ No newline at end of file diff --git a/packages/at_register/example/at_register_usage_explained.dart b/packages/at_register/example/at_register_usage_explained.dart index 10fdea84..aeb85ac5 100644 --- a/packages/at_register/example/at_register_usage_explained.dart +++ b/packages/at_register/example/at_register_usage_explained.dart @@ -6,8 +6,8 @@ Future main() async { /// Example for GetFreeAtsign task GetFreeAtsign getFreeAtsignTask = - GetFreeAtsign(params, registrarApiAccessorInstance: accessorInstance); - RegisterTaskResult getFreeAtsignResult = await getFreeAtsignTask.run(); + GetFreeAtsign(apiAccessorInstance: accessorInstance); + RegisterTaskResult getFreeAtsignResult = await getFreeAtsignTask.run(params); // api call status present in result.apiCallStatus print(getFreeAtsignResult.apiCallStatus); // all relevant data present in result.data which is a Map @@ -20,8 +20,8 @@ Future main() async { /// Example for RegisterAtsign task RegisterAtsign registerAtsignTask = - RegisterAtsign(params, registrarApiAccessorInstance: accessorInstance); - RegisterTaskResult registerAtsignResult = await registerAtsignTask.run(); + RegisterAtsign(apiAccessorInstance: accessorInstance); + RegisterTaskResult registerAtsignResult = await registerAtsignTask.run(params); // registerAtsignResult.data should have a key named 'otpSent' which contains // true/false reg the status of verificationCodeSent to provided email print(registerAtsignResult.data[RegistrarConstants.otpSentName]); @@ -31,9 +31,9 @@ Future main() async { /// Example for ValidateOtp task ValidateOtp validateOtpTask = - ValidateOtp(params, registrarApiAccessorInstance: accessorInstance); + ValidateOtp(apiAccessorInstance: accessorInstance); // Note: confirmation is set to false by default - RegisterTaskResult validateOtpResult = await validateOtpTask.run(); + RegisterTaskResult validateOtpResult = await validateOtpTask.run(params); // CASE 1: if this is the first atsign for the provided email, CRAM should be // present in validateOtpResult.data with key RegistrarConstants.cramKeyName @@ -49,12 +49,12 @@ Future main() async { // now this will return a result with the cram key in result.data List fetchedAtsignList = validateOtpResult.data[RegistrarConstants.fetchedAtsignListName]; - validateOtpTask.registerParams.atsign = fetchedAtsignList[0]; - validateOtpResult = await validateOtpTask.run(); + params.atsign = fetchedAtsignList[0]; + validateOtpResult = await validateOtpTask.run(params); print(validateOtpResult.data[RegistrarConstants.cramKeyName]); // CASE 3: if the otp is incorrect, fetch the correct otp from user and re-run // the validateOtpTask - validateOtpTask.registerParams.otp = 'AB14'; // correct otp - validateOtpResult = await validateOtpTask.run(); + params.otp = 'AB14'; // correct otp + validateOtpResult = await validateOtpTask.run(params); } diff --git a/packages/at_register/lib/at_register.dart b/packages/at_register/lib/at_register.dart index 2e41b162..2d8f64d0 100644 --- a/packages/at_register/lib/at_register.dart +++ b/packages/at_register/lib/at_register.dart @@ -1,4 +1,4 @@ -library; +library at_register; export 'src/api-interactions/registrar_api_accessor.dart'; export 'src/api-interactions/get_free_atsign.dart'; diff --git a/packages/at_register/lib/src/api-interactions/get_free_atsign.dart b/packages/at_register/lib/src/api-interactions/get_free_atsign.dart index fe9c1ccb..710da806 100644 --- a/packages/at_register/lib/src/api-interactions/get_free_atsign.dart +++ b/packages/at_register/lib/src/api-interactions/get_free_atsign.dart @@ -10,41 +10,44 @@ import '../../at_register.dart'; /// Example usage: /// ```dart /// GetFreeAtsign getFreeAtsignInstance = GetFreeAtsign(); -/// await getFreeAtsignInstance.init(RegisterParams(), RegistrarApiAccessor()); -/// RegisterTaskResult result = await getFreeAtsignInstance.run(); +/// RegisterTaskResult result = await getFreeAtsignInstance.run(registerParams); /// ``` -/// The fetched atsign will be stored in result.data['atsign']. -/// ToDo: write down what will be the structure inside result.data{} +/// The fetched atsign will be stored in result.data[[RegistrarConstants.atsignName]] class GetFreeAtsign extends RegisterTask { - GetFreeAtsign(super.registerParams, - {super.registrarApiAccessorInstance, super.allowRetry}); + GetFreeAtsign( + {RegistrarApiAccessor? apiAccessorInstance, bool allowRetry = false}) + : super( + registrarApiAccessorInstance: apiAccessorInstance, + allowRetry: allowRetry); @override String get name => 'GetFreeAtsignTask'; @override - Future run() async { + Future run(RegisterParams params) async { + validateInputParams(params); logger.info('Getting a randomly generated free atSign...'); RegisterTaskResult result = RegisterTaskResult(); try { - String atsign = await registrarApiAccessor.getFreeAtSigns( + String atsign = await registrarApiAccessor.getFreeAtSign( authority: RegistrarConstants.authority); logger.info('Fetched free atsign: $atsign'); result.data['atsign'] = atsign; + result.apiCallStatus = ApiCallStatus.success; } on Exception catch (e) { if (canThrowException()) { throw AtRegisterException(e.toString()); } - populateApiCallStatus(result, e); + ApiUtil.handleException(result, e, shouldRetry()); } - result.apiCallStatus = ApiCallStatus.success; return result; } @override - void validateInputParams() { - if(registerParams.email.isNullOrEmpty){ - throw IllegalArgumentException('email cannot be null for get-free-atsign-task'); + void validateInputParams(params) { + if (params.email.isNullOrEmpty) { + throw IllegalArgumentException( + 'email cannot be null for get-free-atsign-task'); } } } diff --git a/packages/at_register/lib/src/api-interactions/register_atsign.dart b/packages/at_register/lib/src/api-interactions/register_atsign.dart index da6d8f26..9ddcd2cb 100644 --- a/packages/at_register/lib/src/api-interactions/register_atsign.dart +++ b/packages/at_register/lib/src/api-interactions/register_atsign.dart @@ -6,41 +6,62 @@ import '../../at_register.dart'; /// /// Registers the selected atsign to the email provided through [RegisterParams]. /// -/// Sets [RegisterTaskResult.apiCallStatus] if the HTTP GET/POST request gets any response other than STATUS_OK. +/// Sets [RegisterTaskResult.apiCallStatus] if the HTTP GET/POST request gets +/// any response other than STATUS_OK. +/// +/// Example usage: +/// ```dart +/// RegisterAtsign registerAtsignInstance = RegisterAtsign(); +/// RegisterTaskResult result = await registerAtsignInstance.run(registerParams); +/// ``` +/// If verification code has been successfully delivered to email, +/// result.data[[RegistrarConstants.otpSentName]] will be set to true, otherwise false class RegisterAtsign extends RegisterTask { - RegisterAtsign(super.registerParams, - {super.registrarApiAccessorInstance, super.allowRetry}); + RegisterAtsign( + {RegistrarApiAccessor? apiAccessorInstance, bool allowRetry = false}) + : super( + registrarApiAccessorInstance: apiAccessorInstance, + allowRetry: allowRetry); @override String get name => 'RegisterAtsignTask'; @override - Future run() async { - logger.info('Sending verification code to: ${registerParams.email}'); + Future run(RegisterParams params) async { + validateInputParams(params); + logger.info('Sending verification code to: ${params.email}'); RegisterTaskResult result = RegisterTaskResult(); try { - result.data[RegistrarConstants.otpSentName] = (await registrarApiAccessor - .registerAtSign(registerParams.atsign!, registerParams.email!, - authority: RegistrarConstants.authority)) - .toString(); - logger.info('Verification code sent to: ${registerParams.email}'); + bool otpSent = await registrarApiAccessor.registerAtSign( + params.atsign!, params.email!, + authority: RegistrarConstants.authority); + result.data[RegistrarConstants.otpSentName] = otpSent; + if(otpSent) { + logger.info('Verification code sent to: ${params.email}'); + result.apiCallStatus = ApiCallStatus.success; + } else { + logger.severe('Could NOT Verification code sent to: ${params.email}.' + ' Please try again'); + result.apiCallStatus = ApiCallStatus.retry; + } } on Exception catch (e) { if (canThrowException()) { throw AtRegisterException(e.toString()); } - populateApiCallStatus(result, e); + ApiUtil.handleException(result, e, shouldRetry()); } - result.apiCallStatus = ApiCallStatus.success; return result; } @override - void validateInputParams() { - if(registerParams.atsign.isNullOrEmpty){ - throw IllegalArgumentException('Atsign cannot be null for register-atsign-task'); + void validateInputParams(RegisterParams params) { + if (params.atsign.isNullOrEmpty) { + throw IllegalArgumentException( + 'Atsign cannot be null for register-atsign-task'); } - if(registerParams.email.isNullOrEmpty){ - throw IllegalArgumentException('e-mail cannot be null for register-atsign-task'); + if (params.email.isNullOrEmpty) { + throw IllegalArgumentException( + 'e-mail cannot be null for register-atsign-task'); } } } diff --git a/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart b/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart index dbdcf178..c1d5b231 100644 --- a/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart +++ b/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart @@ -14,7 +14,7 @@ class RegistrarApiAccessor { /// Returns a Future> containing free available atSigns /// based on [count] provided as input. - Future getFreeAtSigns( + Future getFreeAtSign( {String authority = RegistrarConstants.apiHostProd}) async { http.Response response = await ApiUtil.getRequest( authority, RegistrarConstants.getFreeAtSignApiPath); @@ -95,10 +95,6 @@ class RegistrarApiAccessor { if (response.statusCode == 200) { ValidateOtpResult validateOtpResult = ValidateOtpResult(); final jsonDecodedResponse = jsonDecode(response.body); - // if (jsonDecodedResponse.containsKey('data')) { - // validateOtpResult.data - // .addAll(jsonDecodedResponse['data'] as Map); - // } _processValidateOtpApiResponse(jsonDecodedResponse, validateOtpResult); return validateOtpResult; } else { @@ -110,32 +106,29 @@ class RegistrarApiAccessor { /// processes API response for [validateOtp] call and populates [result] void _processValidateOtpApiResponse( Map apiResponse, ValidateOtpResult result) { - if ((apiResponse.containsKey('message') && - (apiResponse['message'].toString().toLowerCase()) == - ValidateOtpStatus.verified.name) && + String? message = apiResponse['message'].toString().toLowerCase(); + if (apiResponse.containsKey('data')) { + result.data.addAll(apiResponse['data'] as Map); + } + + if (message == ValidateOtpStatus.verified.name && apiResponse.containsKey(RegistrarConstants.cramKeyName)) { result.taskStatus = ValidateOtpStatus.verified; - result.data[RegistrarConstants.cramKeyName] = - apiResponse[RegistrarConstants.cramKeyName]; } else if (apiResponse.containsKey('data') && - result.data.containsKey(RegistrarConstants.newAtsignName)) { + apiResponse.containsKey(RegistrarConstants.newAtsignName)) { result.data[RegistrarConstants.fetchedAtsignListName] = apiResponse[RegistrarConstants.atsignName]; - result.data[RegistrarConstants.newAtsignName] = - apiResponse[RegistrarConstants.newAtsignName]; result.taskStatus = ValidateOtpStatus.followUp; - } else if (apiResponse.containsKey('message') && - apiResponse['message'] == - 'The code you have entered is invalid or expired. Please try again?') { + } else if (message == + 'The code you have entered is invalid or expired. Please try again?') { result.taskStatus = ValidateOtpStatus.retry; - result.exceptionMessage = apiResponse['message']; - } else if (apiResponse.containsKey('message') && - (apiResponse['message'] == - 'Oops! You already have the maximum number of free atSigns. Please select one of your existing atSigns.')) { + result.exception = apiResponse['message']; + } else if (message == + 'Oops! You already have the maximum number of free atSigns. Please select one of your existing atSigns.') { throw MaximumAtsignQuotaException( 'Maximum free atsign limit reached for current email'); } else { - throw AtRegisterException('${apiResponse['message']}'); + throw AtRegisterException(message); } } diff --git a/packages/at_register/lib/src/api-interactions/validate_otp.dart b/packages/at_register/lib/src/api-interactions/validate_otp.dart index 3912c72b..fabb2d01 100644 --- a/packages/at_register/lib/src/api-interactions/validate_otp.dart +++ b/packages/at_register/lib/src/api-interactions/validate_otp.dart @@ -4,24 +4,42 @@ import 'package:at_utils/at_utils.dart'; import '../../at_register.dart'; /// Task for validating the verification_code sent as part of the registration process. +/// +/// Example usage: +/// ```dart +/// ValidateOtp validateOtpInstance = ValidateOtp(); +/// RegisterTaskResult result = await validateOtpInstance.run(registerParams); +/// ``` +/// CASE 1: 'If the email provided through registerParams does NOT have any existing atsigns': +/// cramKey will be present in result.data[[RegistrarConstants.cramKeyName]] +/// +/// CASE 2: 'If the email provided through registerParams has existing atsigns': +/// list of existingAtsigns will be present in result.data[[RegistrarConstants.fetchedAtsignListName]] +/// and the new atsign in result.data[[RegistrarConstants.newAtsignName]]; +/// Now, to fetch the cram key select one atsign (existing/new); populate this atsign +/// in registerParams and retry this task. Output will be as described in 'CASE 1' class ValidateOtp extends RegisterTask { - ValidateOtp(super.registerParams, - {super.registrarApiAccessorInstance, super.allowRetry}); + ValidateOtp( + {RegistrarApiAccessor? apiAccessorInstance, bool allowRetry = false}) + : super( + registrarApiAccessorInstance: apiAccessorInstance, + allowRetry: allowRetry); @override String get name => 'ValidateOtpTask'; @override - Future run() async { + Future run(RegisterParams params) async { + validateInputParams(params); RegisterTaskResult result = RegisterTaskResult(); try { - logger.info('Validating code with ${registerParams.atsign}...'); - registerParams.atsign = AtUtils.fixAtSign(registerParams.atsign!); + logger.info('Validating code with ${params.atsign}...'); + params.atsign = AtUtils.fixAtSign(params.atsign!); final validateOtpApiResult = await registrarApiAccessor.validateOtp( - registerParams.atsign!, - registerParams.email!, - registerParams.otp!, - confirmation: registerParams.confirmation, + params.atsign!, + params.email!, + params.otp!, + confirmation: params.confirmation, authority: RegistrarConstants.authority, ); @@ -30,23 +48,25 @@ class ValidateOtp extends RegisterTask { if (canThrowException()) { throw InvalidVerificationCodeException( 'Verification Failed: Incorrect verification code provided'); + } + logger.warning('Invalid or expired verification code. Retrying...'); + params.otp ??= ApiUtil + .readCliVerificationCode(); // retry reading otp from user through stdin + if (shouldRetry()) { + result.apiCallStatus = ApiCallStatus.retry; + result.exception = InvalidVerificationCodeException( + 'Verification Failed: Incorrect verification code provided. Please retry the process again'); } else { - logger.warning('Invalid or expired verification code. Retrying...'); - registerParams.otp ??= ApiUtil - .readCliVerificationCode(); // retry reading otp from user through stdin - result.apiCallStatus = - shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; - result.exceptionMessage = - 'Verification Failed: Incorrect verification code provided. Please retry the process again'; + result.apiCallStatus = ApiCallStatus.failure; + result.exception = ExhaustedVerificationCodeRetriesException( + 'Exhausted verification code retry attempts. Please restart the process'); } break; case ValidateOtpStatus.followUp: - registerParams.confirmation = true; - result.data['otp'] = registerParams.otp; - result.data[RegistrarConstants.fetchedAtsignListName] = - validateOtpApiResult - .data[RegistrarConstants.fetchedAtsignListName]; + params.confirmation = true; + result.data['otp'] = params.otp; + result.fetchedAtsignList = validateOtpApiResult.data['atsign']; result.data[RegistrarConstants.newAtsignName] = validateOtpApiResult.data[RegistrarConstants.newAtsignName]; result.apiCallStatus = ApiCallStatus.retry; @@ -59,18 +79,18 @@ class ValidateOtp extends RegisterTask { .data[RegistrarConstants.cramKeyName] .split(":")[1]; logger.info('Cram secret verified'); - logger.info('Successful registration for ${registerParams.email}'); + logger.info('Successful registration for ${params.email}'); result.apiCallStatus = ApiCallStatus.success; break; case ValidateOtpStatus.failure: result.apiCallStatus = ApiCallStatus.failure; - result.exceptionMessage = validateOtpApiResult.exceptionMessage; + result.exception = validateOtpApiResult.exception; break; case null: result.apiCallStatus = ApiCallStatus.failure; - result.exceptionMessage = validateOtpApiResult.exceptionMessage; + result.exception = validateOtpApiResult.exception; break; } } on MaximumAtsignQuotaException { @@ -83,25 +103,24 @@ class ValidateOtp extends RegisterTask { if (canThrowException()) { throw AtRegisterException(e.toString()); } - populateApiCallStatus(result, e); + ApiUtil.handleException(result, e, shouldRetry()); } return result; } @override - void validateInputParams() { - if (registerParams.atsign.isNullOrEmpty) { + void validateInputParams(RegisterParams params) { + if (params.atsign.isNullOrEmpty) { throw IllegalArgumentException( 'Atsign cannot be null for register-atsign-task'); } - if (registerParams.email.isNullOrEmpty) { + if (params.email.isNullOrEmpty) { throw IllegalArgumentException( 'e-mail cannot be null for register-atsign-task'); } - if (registerParams.otp.isNullOrEmpty) { + if (params.otp.isNullOrEmpty) { throw InvalidVerificationCodeException( 'Verification code cannot be null/empty'); } - } } diff --git a/packages/at_register/lib/src/util/api_util.dart b/packages/at_register/lib/src/util/api_util.dart index 5c531453..a812d1d9 100644 --- a/packages/at_register/lib/src/util/api_util.dart +++ b/packages/at_register/lib/src/util/api_util.dart @@ -89,6 +89,18 @@ class ApiUtil { return otp; } + /// Populates [RegisterTaskResult.apiCallStatus] based on [_retryCount] + /// Formats and populates [RegisterTaskResult.exception] based on Exception [e] + static void handleException( + RegisterTaskResult result, Exception e, bool shouldRetry) { + result.apiCallStatus = + shouldRetry ? ApiCallStatus.retry : ApiCallStatus.failure; + + String formattedExceptionMessage = + ApiUtil.formatExceptionMessage(e.toString()); + result.exception = AtRegisterException(formattedExceptionMessage); + } + static String readUserAtsignChoice(List? atsigns) { if (atsigns == null) { throw AtRegisterException('Fetched atsigns list is null'); @@ -107,11 +119,4 @@ class ApiUtil { return atsigns[choice]; } } - - static String formatException(String message) { - if (message.contains('Exception: ')) { - return message.replaceAll('Exception: ', ''); - } - return message; - } } diff --git a/packages/at_register/lib/src/util/at_register_exception.dart b/packages/at_register/lib/src/util/at_register_exception.dart index d7894520..5a6b0f72 100644 --- a/packages/at_register/lib/src/util/at_register_exception.dart +++ b/packages/at_register/lib/src/util/at_register_exception.dart @@ -1,20 +1,25 @@ import 'package:at_commons/at_commons.dart'; class AtRegisterException extends AtException { - AtRegisterException(super.message, {super.intent, super.exceptionScenario}); + AtRegisterException(String message, + {Intent? intent, ExceptionScenario? exceptionScenario}) + : super(message, intent: intent, exceptionScenario: exceptionScenario); } class MaximumAtsignQuotaException extends AtException { - MaximumAtsignQuotaException(super.message, - {super.intent, super.exceptionScenario}); + MaximumAtsignQuotaException(String message, + {Intent? intent, ExceptionScenario? exceptionScenario}) + : super(message, intent: intent, exceptionScenario: exceptionScenario); } class InvalidVerificationCodeException extends AtException { - InvalidVerificationCodeException(super.message, - {super.intent, super.exceptionScenario}); + InvalidVerificationCodeException(String message, + {Intent? intent, ExceptionScenario? exceptionScenario}) + : super(message, intent: intent, exceptionScenario: exceptionScenario); } class ExhaustedVerificationCodeRetriesException extends AtException { - ExhaustedVerificationCodeRetriesException(super.message, - {super.intent, super.exceptionScenario}); + ExhaustedVerificationCodeRetriesException(String message, + {Intent? intent, ExceptionScenario? exceptionScenario}) + : super(message, intent: intent, exceptionScenario: exceptionScenario); } diff --git a/packages/at_register/lib/src/util/register_task.dart b/packages/at_register/lib/src/util/register_task.dart index cec41d78..c86906a7 100644 --- a/packages/at_register/lib/src/util/register_task.dart +++ b/packages/at_register/lib/src/util/register_task.dart @@ -5,7 +5,7 @@ import 'package:at_utils/at_logger.dart'; abstract class RegisterTask { late String name; - final maximumRetries = 3; + final maximumRetries = 4; // 1 run attempt + 3 retries = 4 int _retryCount = 1; @@ -13,20 +13,17 @@ abstract class RegisterTask { late bool _allowRetry = false; - late RegisterParams registerParams; - late RegistrarApiAccessor _registrarApiAccessor; RegistrarApiAccessor get registrarApiAccessor => _registrarApiAccessor; late AtSignLogger logger; - RegisterTask(this.registerParams, + RegisterTask( {RegistrarApiAccessor? registrarApiAccessorInstance, bool allowRetry = false}) { _registrarApiAccessor = registrarApiAccessorInstance ?? RegistrarApiAccessor(); _allowRetry = allowRetry; - validateInputParams(); logger = AtSignLogger(name); } @@ -35,30 +32,24 @@ abstract class RegisterTask { /// /// If [allowRetry] is set to true, the task will rethrow all exceptions /// otherwise will catch the exception and store the exception message in - /// [RegisterTaskResult.exceptionMessage] - Future run(); + /// [RegisterTaskResult.exception] + Future run(RegisterParams params); - Future retry() async { + Future retry(RegisterParams params) async { increaseRetryCount(); - return await run(); + return await run(params); } + /// Each task implementation will have a set of params that are required to + /// complete the respective task. This method will ensure all required params + /// are provided/populated + void validateInputParams(RegisterParams params); + bool canThrowException() { // cannot throw exception if retries allowed return !_allowRetry; } - void populateApiCallStatus(RegisterTaskResult result, Exception e) { - result.apiCallStatus = - shouldRetry() ? ApiCallStatus.retry : ApiCallStatus.failure; - result.exceptionMessage = e.toString(); - } - - /// Each task implementation will have a set of params that are required to - /// complete the respective task. This method will ensure all required params - /// are provided/populated - void validateInputParams(); - /// Increases retry count by 1 /// /// This method is to ensure that retryCount cannot be reduced @@ -69,6 +60,6 @@ abstract class RegisterTask { /// In case the task has returned a [RegisterTaskResult] with status retry, /// this method checks and returns if the task can be retried bool shouldRetry() { - return _retryCount <= maximumRetries; + return _retryCount < maximumRetries; } } diff --git a/packages/at_register/lib/src/util/register_task_result.dart b/packages/at_register/lib/src/util/register_task_result.dart index 08fc338d..2dc1d068 100644 --- a/packages/at_register/lib/src/util/register_task_result.dart +++ b/packages/at_register/lib/src/util/register_task_result.dart @@ -7,12 +7,14 @@ class RegisterTaskResult { late ApiCallStatus apiCallStatus; - String? exceptionMessage; + List? fetchedAtsignList; + + Exception? exception; @override String toString() { return 'Data: $data | ' 'ApiCallStatus: ${apiCallStatus.name} | ' - 'exception(if encountered): $exceptionMessage'; + 'exception(if encountered): $exception'; } } diff --git a/packages/at_register/pubspec.yaml b/packages/at_register/pubspec.yaml index ea6db1ca..622ba975 100644 --- a/packages/at_register/pubspec.yaml +++ b/packages/at_register/pubspec.yaml @@ -4,7 +4,7 @@ version: 1.0.0 repository: https://github.com/atsign-foundation/at_libraries environment: - sdk: ^3.2.4 + sdk: '>=2.15.1 <4.0.0' dependencies: at_commons: ^4.0.3 diff --git a/packages/at_register/test/at_register_allow_retry_false_test.dart b/packages/at_register/test/at_register_allow_retry_false_test.dart index c2ebb5d8..a3427720 100644 --- a/packages/at_register/test/at_register_allow_retry_false_test.dart +++ b/packages/at_register/test/at_register_allow_retry_false_test.dart @@ -9,13 +9,12 @@ void main() { MockRegistrarApiAccessor mockRegistrarApiAccessor = MockRegistrarApiAccessor(); - group('Validate individual tasks behaviour with invalid params', () { + group('Validate individual task behaviour with invalid params', () { test('validate behaviour of GetFreeAtsign', () async { RegisterParams params = RegisterParams()..email = ''; - - expect( - () => GetFreeAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor), + var getFreeAtsignTask = GetFreeAtsign( + apiAccessorInstance: mockRegistrarApiAccessor); + expect(() => getFreeAtsignTask.run(params), throwsA(predicate((e) => e is IllegalArgumentException))); }); @@ -24,9 +23,9 @@ void main() { ..email = 'email@email' ..atsign = null; - expect( - () => RegisterAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor), + var registerAtsignTask = RegisterAtsign( + apiAccessorInstance: mockRegistrarApiAccessor); + expect(() => registerAtsignTask.run(params), throwsA(predicate((e) => e is IllegalArgumentException))); }); @@ -35,9 +34,9 @@ void main() { ..email = 'email@email' ..otp = null; - expect( - () => ValidateOtp(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor), + var validateOtpTask = ValidateOtp( + apiAccessorInstance: mockRegistrarApiAccessor); + expect(() => validateOtpTask.run(params), throwsA(predicate((e) => e is IllegalArgumentException))); }); @@ -47,10 +46,9 @@ void main() { ..email = 'email@email' ..atsign = '@abcd' ..otp = null; - - expect( - () => ValidateOtp(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor), + var validateOtpTask = ValidateOtp( + apiAccessorInstance: mockRegistrarApiAccessor); + expect(() => validateOtpTask.run(params), throwsA(predicate((e) => e is InvalidVerificationCodeException))); }); }); @@ -59,28 +57,28 @@ void main() { setUp(() => resetMocktailState()); test('validate behaviour of GetFreeAtsign', () async { - when(() => mockRegistrarApiAccessor.getFreeAtSigns()) + when(() => mockRegistrarApiAccessor.getFreeAtSign()) .thenAnswer((invocation) => Future.value('@alice')); RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; - GetFreeAtsign getFreeAtsign = GetFreeAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); - final result = await getFreeAtsign.run(); + GetFreeAtsign getFreeAtsign = GetFreeAtsign( + apiAccessorInstance: mockRegistrarApiAccessor); + final result = await getFreeAtsign.run(params); expect(result.data[RegistrarConstants.atsignName], '@alice'); }); test('validate behaviour of GetFreeAtsign - encounters exception', () async { - when(() => mockRegistrarApiAccessor.getFreeAtSigns()) + when(() => mockRegistrarApiAccessor.getFreeAtSign()) .thenThrow(Exception('random exception')); RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; - GetFreeAtsign getFreeAtsign = GetFreeAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); + GetFreeAtsign getFreeAtsign = GetFreeAtsign( + apiAccessorInstance: mockRegistrarApiAccessor); bool exceptionFlag = false; try { - await getFreeAtsign.run(); + await getFreeAtsign.run(params); } on Exception catch (e) { expect(e.runtimeType, AtRegisterException); expect(e.toString().contains('random exception'), true); @@ -104,12 +102,12 @@ void main() { RegisterParams params = RegisterParams() ..atsign = atsign ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); - RegisterTaskResult result = await registerAtsignTask.run(); + RegisterAtsign registerAtsignTask = RegisterAtsign( + apiAccessorInstance: mockRegistrarApiAccessor); + RegisterTaskResult result = await registerAtsignTask.run(params); expect(result.apiCallStatus, ApiCallStatus.success); - expect(result.data['otpSent'], 'true'); + expect(result.data['otpSent'], true); }); test('RegisterAtsign params reading and updating - negative case', @@ -122,12 +120,12 @@ void main() { RegisterParams params = RegisterParams() ..atsign = atsign ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); - RegisterTaskResult result = await registerAtsignTask.run(); + RegisterAtsign registerAtsignTask = RegisterAtsign( + apiAccessorInstance: mockRegistrarApiAccessor); + RegisterTaskResult result = await registerAtsignTask.run(params); - expect(result.apiCallStatus, ApiCallStatus.success); - expect(result.data['otpSent'], 'false'); + expect(result.apiCallStatus, ApiCallStatus.retry); + expect(result.data['otpSent'], false); }); test('verify behaviour of RegisterAtsign processing an exception', @@ -141,11 +139,11 @@ void main() { ..atsign = atsign ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); + RegisterAtsign registerAtsignTask = RegisterAtsign( + apiAccessorInstance: mockRegistrarApiAccessor); bool exceptionFlag = false; try { - await registerAtsignTask.run(); + await registerAtsignTask.run(params); } on Exception catch (e) { expect(e.runtimeType, AtRegisterException); expect(e.toString().contains('random exception'), true); @@ -166,11 +164,11 @@ void main() { RegisterParams params = RegisterParams() ..atsign = atsign ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); + RegisterAtsign registerAtsignTask = RegisterAtsign( + apiAccessorInstance: mockRegistrarApiAccessor); bool exceptionFlag = false; try { - await registerAtsignTask.run(); + await registerAtsignTask.run(params); } on Exception catch (e) { assert(e.toString().contains('another new random exception')); exceptionFlag = true; @@ -205,9 +203,9 @@ void main() { ..email = email ..otp = otp; - ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); - RegisterTaskResult result = await validateOtpTask.run(); + ValidateOtp validateOtpTask = ValidateOtp( + apiAccessorInstance: mockRegistrarApiAccessor); + RegisterTaskResult result = await validateOtpTask.run(params); expect(result.data[RegistrarConstants.cramKeyName], cram); }); @@ -228,7 +226,7 @@ void main() { ValidateOtpResult validateOtpResult = ValidateOtpResult() ..taskStatus = ValidateOtpStatus.followUp ..apiCallStatus = ApiCallStatus.success - ..data = {'data': mockApiRespData}; + ..data = mockApiRespData; when(() => mockRegistrarApiAccessor.validateOtp(atsign, email, otp, confirmation: false)) .thenAnswer((invocation) => Future.value(validateOtpResult)); @@ -247,20 +245,19 @@ void main() { ..email = email ..otp = otp; - ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); - RegisterTaskResult result = await validateOtpTask.run(); + ValidateOtp validateOtpTask = ValidateOtp( + apiAccessorInstance: mockRegistrarApiAccessor); + RegisterTaskResult result = await validateOtpTask.run(params); expect(params.confirmation, true); // confirmation set to true by the Task expect(result.apiCallStatus, ApiCallStatus.retry); - // expect(result.data[RegistrarConstants.fetchedAtsignListName], - // ['@old-atsign']); + expect(result.fetchedAtsignList, ['@old-atsign']); expect(result.data[RegistrarConstants.newAtsignName], atsign); // The above case is when an email has already existing atsigns, select an atsign // from the list and retry the task with confirmation set to 'true' // mimic-ing a user selecting an atsign and proceeding ahead params.atsign = atsign2; - result = await validateOtpTask.run(); + result = await validateOtpTask.run(params); expect(result.apiCallStatus, ApiCallStatus.success); expect(result.data[RegistrarConstants.cramKeyName], cram); }); @@ -283,11 +280,11 @@ void main() { confirmation: false)) .thenAnswer((invocation) => Future.value(validateOtpResult)); - ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); + ValidateOtp validateOtpTask = + ValidateOtp(apiAccessorInstance: mockRegistrarApiAccessor); expect( - () async => await validateOtpTask.run(), + () async => await validateOtpTask.run(params), throwsA(predicate((e) => e is InvalidVerificationCodeException && e.message.contains('Incorrect verification code provided')))); @@ -311,10 +308,10 @@ void main() { .thenThrow( MaximumAtsignQuotaException('maximum free atsign limit reached')); - ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor); + ValidateOtp validateOtpTask = ValidateOtp( + apiAccessorInstance: mockRegistrarApiAccessor); - expect(() async => await validateOtpTask.run(), + expect(() async => await validateOtpTask.run(params), throwsA(predicate((e) => e is MaximumAtsignQuotaException))); }); }); diff --git a/packages/at_register/test/at_register_allow_retry_true_test.dart b/packages/at_register/test/at_register_allow_retry_true_test.dart index d058b563..60b865a4 100644 --- a/packages/at_register/test/at_register_allow_retry_true_test.dart +++ b/packages/at_register/test/at_register_allow_retry_true_test.dart @@ -17,31 +17,29 @@ void main() { setUp(() => resetMocktailState()); test('validate behaviour of GetFreeAtsign', () async { - when(() => mockRegistrarApiAccessor.getFreeAtSigns()) + when(() => mockRegistrarApiAccessor.getFreeAtSign()) .thenAnswer((invocation) => Future.value('@alice')); RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; - GetFreeAtsign getFreeAtsign = GetFreeAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor, - allowRetry: true); - final result = await getFreeAtsign.run(); + GetFreeAtsign getFreeAtsign = GetFreeAtsign( + apiAccessorInstance: mockRegistrarApiAccessor, allowRetry: true); + final result = await getFreeAtsign.run(params); expect(result.data[RegistrarConstants.atsignName], '@alice'); }); test('validate behaviour of GetFreeAtsign - encounters exception', () async { String testExceptionMessage = 'random exception'; - when(() => mockRegistrarApiAccessor.getFreeAtSigns()) + when(() => mockRegistrarApiAccessor.getFreeAtSign()) .thenThrow(Exception(testExceptionMessage)); RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; - GetFreeAtsign getFreeAtsign = GetFreeAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor, - allowRetry: true); - RegisterTaskResult? result = await getFreeAtsign.run(); + GetFreeAtsign getFreeAtsignTask = GetFreeAtsign( + apiAccessorInstance: mockRegistrarApiAccessor, allowRetry: true); + RegisterTaskResult? result = await getFreeAtsignTask.run(params); expect(result.apiCallStatus, ApiCallStatus.retry); - assert(result.exceptionMessage!.contains(testExceptionMessage)); + expect(result.exception!.toString().contains(testExceptionMessage), true); }); }); @@ -58,12 +56,11 @@ void main() { RegisterParams params = RegisterParams() ..atsign = atsign ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor, - allowRetry: true); - RegisterTaskResult result = await registerAtsignTask.run(); + RegisterAtsign registerAtsignTask = RegisterAtsign( + apiAccessorInstance: mockRegistrarApiAccessor, allowRetry: true); + RegisterTaskResult result = await registerAtsignTask.run(params); expect(result.apiCallStatus, ApiCallStatus.success); - expect(result.data['otpSent'], 'true'); + expect(result.data['otpSent'], true); }); test('RegisterAtsign params reading and updating - negative case', @@ -76,13 +73,12 @@ void main() { RegisterParams params = RegisterParams() ..atsign = atsign ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor, - allowRetry: true); - RegisterTaskResult result = await registerAtsignTask.run(); + RegisterAtsign registerAtsignTask = RegisterAtsign( + apiAccessorInstance: mockRegistrarApiAccessor, allowRetry: true); + RegisterTaskResult result = await registerAtsignTask.run(params); - expect(result.apiCallStatus, ApiCallStatus.success); - expect(result.data['otpSent'], 'false'); + expect(result.apiCallStatus, ApiCallStatus.retry); + expect(result.data['otpSent'], false); /// ToDo: discuss }); test('verify behaviour of RegisterAtsign processing an exception', @@ -97,13 +93,12 @@ void main() { ..atsign = atsign ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor, - allowRetry: true); - RegisterTaskResult? result = await registerAtsignTask.run(); + RegisterAtsign registerAtsignTask = RegisterAtsign( + apiAccessorInstance: mockRegistrarApiAccessor, allowRetry: true); + RegisterTaskResult? result = await registerAtsignTask.run(params); expect(registerAtsignTask.shouldRetry(), true); - assert(result.exceptionMessage!.contains(testException)); + expect(result.exception!.toString().contains(testException), true); }); test( @@ -118,12 +113,11 @@ void main() { RegisterParams params = RegisterParams() ..atsign = atsign ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor, - allowRetry: true); + RegisterAtsign registerAtsignTask = RegisterAtsign( + apiAccessorInstance: mockRegistrarApiAccessor, allowRetry: true); - var result = await registerAtsignTask.run(); - assert(result.exceptionMessage!.contains(testExceptionMessage)); + var result = await registerAtsignTask.run(params); + expect(result.exception!.toString().contains(testExceptionMessage), true); expect(registerAtsignTask.retryCount, 1); }); }); @@ -154,10 +148,9 @@ void main() { ..email = email ..otp = otp; - ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor, - allowRetry: true); - RegisterTaskResult result = await validateOtpTask.run(); + ValidateOtp validateOtpTask = ValidateOtp( + apiAccessorInstance: mockRegistrarApiAccessor, allowRetry: true); + RegisterTaskResult result = await validateOtpTask.run(params); expect(result.data[RegistrarConstants.cramKeyName], cram); }); @@ -197,10 +190,9 @@ void main() { ..email = email ..otp = otp; - ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor, - allowRetry: true); - RegisterTaskResult result = await validateOtpTask.run(); + ValidateOtp validateOtpTask = ValidateOtp( + apiAccessorInstance: mockRegistrarApiAccessor, allowRetry: true); + RegisterTaskResult result = await validateOtpTask.run(params); expect(params.confirmation, true); // confirmation set to true by RegisterTask expect(result.apiCallStatus, ApiCallStatus.retry); @@ -209,7 +201,7 @@ void main() { // The above case is when an email has already existing atsigns, select an atsign // from the list and retry the task with confirmation set to 'true' params.atsign = atsign2; - result = await validateOtpTask.run(); + result = await validateOtpTask.run(params); expect(result.apiCallStatus, ApiCallStatus.success); expect(result.data[RegistrarConstants.cramKeyName], cram); }); @@ -235,11 +227,10 @@ void main() { ..confirmation = false ..email = email ..otp = otp; - ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor, - allowRetry: true); + ValidateOtp validateOtpTask = ValidateOtp( + apiAccessorInstance: mockRegistrarApiAccessor, allowRetry: true); - RegisterTaskResult result = await validateOtpTask.run(); + RegisterTaskResult result = await validateOtpTask.run(params); expect(result.apiCallStatus, ApiCallStatus.retry); }); @@ -261,11 +252,10 @@ void main() { .thenThrow( MaximumAtsignQuotaException('maximum free atsign limit reached')); - ValidateOtp validateOtpTask = ValidateOtp(params, - registrarApiAccessorInstance: mockRegistrarApiAccessor, - allowRetry: true); + ValidateOtp validateOtpTask = ValidateOtp( + apiAccessorInstance: mockRegistrarApiAccessor, allowRetry: true); - expect(() async => await validateOtpTask.run(), + expect(() async => await validateOtpTask.run(params), throwsA(predicate((e) => e is MaximumAtsignQuotaException))); }); }); From 03b7b18dcb01ab5a95382a5ed47be4e6183de53a Mon Sep 17 00:00:00 2001 From: Srie Teja Date: Thu, 7 Mar 2024 04:32:52 +0530 Subject: [PATCH 14/16] build: add dep_override for at_register in onboarding_cli_func_tests --- tests/at_onboarding_cli_functional_tests/pubspec.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/at_onboarding_cli_functional_tests/pubspec.yaml b/tests/at_onboarding_cli_functional_tests/pubspec.yaml index 077ad73f..bea5480a 100644 --- a/tests/at_onboarding_cli_functional_tests/pubspec.yaml +++ b/tests/at_onboarding_cli_functional_tests/pubspec.yaml @@ -9,6 +9,8 @@ environment: dependencies: at_onboarding_cli: path: ../../packages/at_onboarding_cli + at_register: + path: ../../packages/at_register dev_dependencies: lints: ^1.0.0 From d51c8c5053667765fb012468f0aa340ff5b52b4f Mon Sep 17 00:00:00 2001 From: Sri Teja T Date: Thu, 7 Mar 2024 04:43:23 +0530 Subject: [PATCH 15/16] Delete packages/at_onboarding_cli/bin/delete_test.dart --- packages/at_onboarding_cli/bin/delete_test.dart | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/at_onboarding_cli/bin/delete_test.dart diff --git a/packages/at_onboarding_cli/bin/delete_test.dart b/packages/at_onboarding_cli/bin/delete_test.dart deleted file mode 100644 index e69de29b..00000000 From 930e372cf34236b611e5ce13a7d98d06ed62a762 Mon Sep 17 00:00:00 2001 From: Srie Teja Date: Thu, 28 Mar 2024 02:26:58 +0530 Subject: [PATCH 16/16] reformat: minor formatting changes --- .../lib/src/register_cli/register.dart | 7 ++- .../src/register_cli/registration_flow.dart | 2 - .../test/at_register_cli_test.dart | 7 ++- .../example/at_register_example.dart | 28 +++++----- .../example/at_register_usage_explained.dart | 8 +-- .../src/api-interactions/get_free_atsign.dart | 6 +-- .../src/api-interactions/register_atsign.dart | 4 +- .../registrar_api_accessor.dart | 23 ++++---- .../src/api-interactions/validate_otp.dart | 5 +- .../at_register_allow_retry_false_test.dart | 52 +++++++++---------- 10 files changed, 70 insertions(+), 72 deletions(-) diff --git a/packages/at_onboarding_cli/lib/src/register_cli/register.dart b/packages/at_onboarding_cli/lib/src/register_cli/register.dart index 9a2a4f01..da595ca4 100644 --- a/packages/at_onboarding_cli/lib/src/register_cli/register.dart +++ b/packages/at_onboarding_cli/lib/src/register_cli/register.dart @@ -47,14 +47,13 @@ class Register { } GetFreeAtsign getFreeAtsignTask = GetFreeAtsign( - apiAccessorInstance: registrarApiAccessor, - ); + apiAccessorInstance: registrarApiAccessor, allowRetry: true); RegisterAtsign registerAtsignTask = - RegisterAtsign(apiAccessorInstance: registrarApiAccessor); + RegisterAtsign(apiAccessorInstance: registrarApiAccessor, allowRetry: true); ValidateOtp validateOtpTask = - ValidateOtp(apiAccessorInstance: registrarApiAccessor); + ValidateOtp(apiAccessorInstance: registrarApiAccessor, allowRetry: true); // create a queue of tasks each of type [RegisterTask] and then // call start on the RegistrationFlow object diff --git a/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart b/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart index 33c8aada..6b87ebe7 100644 --- a/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart +++ b/packages/at_onboarding_cli/lib/src/register_cli/registration_flow.dart @@ -19,8 +19,6 @@ class RegistrationFlow { Future start() async { for (RegisterTask task in processQueue) { - // setting allowRetry to true as this method has logic to retry each - // failed task 3-times and then throw an exception if still failing try { _result = await task.run(params); task.logger.finer('Attempt: ${task.retryCount} | params[$params]'); diff --git a/packages/at_onboarding_cli/test/at_register_cli_test.dart b/packages/at_onboarding_cli/test/at_register_cli_test.dart index ca704c05..395c71fe 100644 --- a/packages/at_onboarding_cli/test/at_register_cli_test.dart +++ b/packages/at_onboarding_cli/test/at_register_cli_test.dart @@ -49,9 +49,9 @@ void main() { apiAccessorInstance: accessorInstance, allowRetry: true); try { await RegistrationFlow(params).add(registerAtsignTask).start(); - } on Exception catch(e){ + } on Exception catch (e) { expect(e.runtimeType, AtRegisterException); - assert(e.toString().contains('Could not complete the task')); + assert(e.toString().contains('Could not complete the task')); } expect(registerAtsignTask.retryCount, registerAtsignTask.maximumRetries); expect(registerAtsignTask.shouldRetry(), false); @@ -78,8 +78,7 @@ void main() { exceptionFlag = true; } expect(exceptionFlag, true); - expect( - registerAtsignTask.retryCount, registerAtsignTask.maximumRetries); + expect(registerAtsignTask.retryCount, registerAtsignTask.maximumRetries); expect(registerAtsignTask.shouldRetry(), false); }); }); diff --git a/packages/at_register/example/at_register_example.dart b/packages/at_register/example/at_register_example.dart index 8b8ee720..5f7cf0e1 100644 --- a/packages/at_register/example/at_register_example.dart +++ b/packages/at_register/example/at_register_example.dart @@ -2,31 +2,31 @@ import 'package:at_register/at_register.dart'; void main() async { String email = ''; - RegisterParams params = RegisterParams()..email = email; + RegisterParams registerParams = RegisterParams()..email = email; GetFreeAtsign getFreeAtsignTask = GetFreeAtsign(); - RegisterTaskResult result = await getFreeAtsignTask.run(params); - params.addFromJson(result.data); + RegisterTaskResult result = await getFreeAtsignTask.run(registerParams); + registerParams.addFromJson(result.data); RegisterAtsign registerAtsignTask = RegisterAtsign(); - result = await registerAtsignTask.run(params); - params.addFromJson(result.data); + result = await registerAtsignTask.run(registerParams); + registerParams.addFromJson(result.data); // verification code sent to email provided in the beginning // check the same email and enter that verification code through terminal/stdin - params.otp = ApiUtil.readCliVerificationCode(); + registerParams.otp = ApiUtil.readCliVerificationCode(); ValidateOtp validateOtpTask = ValidateOtp(); - result = await validateOtpTask.run(params); - if(result.apiCallStatus == ApiCallStatus.success){ + result = await validateOtpTask.run(registerParams); + if (result.apiCallStatus == ApiCallStatus.success) { print(result.data[RegistrarConstants.cramKeyName]); } else { // this is the case where the email has existing atsigns - // set task.params.confirmation to true, select an atsign (existing/new) from - // the + // set task.params.confirmation to true, select an atsign (existing/new) + // from the list of atsigns returned in the previous call(ValidateOtp with confirmation set to false) String newAtsign = result.data[RegistrarConstants.newAtsignName]; - params.atsign = newAtsign; - params.confirmation = true; - result = await validateOtpTask.run(params); + registerParams.atsign = newAtsign; + registerParams.confirmation = true; + result = await validateOtpTask.run(registerParams); print(result.data[RegistrarConstants.cramKeyName]); } -} \ No newline at end of file +} diff --git a/packages/at_register/example/at_register_usage_explained.dart b/packages/at_register/example/at_register_usage_explained.dart index aeb85ac5..da1bb828 100644 --- a/packages/at_register/example/at_register_usage_explained.dart +++ b/packages/at_register/example/at_register_usage_explained.dart @@ -14,14 +14,15 @@ Future main() async { print(getFreeAtsignResult.data); // this step is optional - // Can be used to propagates the data received in the current task to the next + // Can be used to propagate the data received in the current task to the next params.addFromJson(getFreeAtsignResult.data); // ---------------------------------------------------- /// Example for RegisterAtsign task RegisterAtsign registerAtsignTask = RegisterAtsign(apiAccessorInstance: accessorInstance); - RegisterTaskResult registerAtsignResult = await registerAtsignTask.run(params); + RegisterTaskResult registerAtsignResult = + await registerAtsignTask.run(params); // registerAtsignResult.data should have a key named 'otpSent' which contains // true/false reg the status of verificationCodeSent to provided email print(registerAtsignResult.data[RegistrarConstants.otpSentName]); @@ -49,6 +50,7 @@ Future main() async { // now this will return a result with the cram key in result.data List fetchedAtsignList = validateOtpResult.data[RegistrarConstants.fetchedAtsignListName]; + // selecting the first atsign from the fetchedAtsignList for demonstration params.atsign = fetchedAtsignList[0]; validateOtpResult = await validateOtpTask.run(params); print(validateOtpResult.data[RegistrarConstants.cramKeyName]); @@ -56,5 +58,5 @@ Future main() async { // CASE 3: if the otp is incorrect, fetch the correct otp from user and re-run // the validateOtpTask params.otp = 'AB14'; // correct otp - validateOtpResult = await validateOtpTask.run(params); + validateOtpResult = await validateOtpTask.retry(params); } diff --git a/packages/at_register/lib/src/api-interactions/get_free_atsign.dart b/packages/at_register/lib/src/api-interactions/get_free_atsign.dart index 710da806..6dbcdd4d 100644 --- a/packages/at_register/lib/src/api-interactions/get_free_atsign.dart +++ b/packages/at_register/lib/src/api-interactions/get_free_atsign.dart @@ -1,8 +1,8 @@ import 'package:at_commons/at_commons.dart'; -import '../../at_register.dart'; +import 'package:at_register/at_register.dart'; -/// A [RegisterTask] that fetches a list of free atsigns. +/// A [RegisterTask] that fetches a free atsign /// /// Throws an [AtException] with the concerned message encountered in the /// HTTP GET/POST request. @@ -12,7 +12,7 @@ import '../../at_register.dart'; /// GetFreeAtsign getFreeAtsignInstance = GetFreeAtsign(); /// RegisterTaskResult result = await getFreeAtsignInstance.run(registerParams); /// ``` -/// The fetched atsign will be stored in result.data[[RegistrarConstants.atsignName]] +/// The fetched atsign will be stored in RegisterTaskResult.data[[RegistrarConstants.atsignName]] class GetFreeAtsign extends RegisterTask { GetFreeAtsign( {RegistrarApiAccessor? apiAccessorInstance, bool allowRetry = false}) diff --git a/packages/at_register/lib/src/api-interactions/register_atsign.dart b/packages/at_register/lib/src/api-interactions/register_atsign.dart index 9ddcd2cb..708cd128 100644 --- a/packages/at_register/lib/src/api-interactions/register_atsign.dart +++ b/packages/at_register/lib/src/api-interactions/register_atsign.dart @@ -1,6 +1,6 @@ import 'package:at_commons/at_commons.dart'; -import '../../at_register.dart'; +import 'package:at_register/at_register.dart'; /// User selects an atsign from the list fetched in [GetFreeAtsign]. /// @@ -36,7 +36,7 @@ class RegisterAtsign extends RegisterTask { params.atsign!, params.email!, authority: RegistrarConstants.authority); result.data[RegistrarConstants.otpSentName] = otpSent; - if(otpSent) { + if (otpSent) { logger.info('Verification code sent to: ${params.email}'); result.apiCallStatus = ApiCallStatus.success; } else { diff --git a/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart b/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart index c1d5b231..61aaecd2 100644 --- a/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart +++ b/packages/at_register/lib/src/api-interactions/registrar_api_accessor.dart @@ -5,7 +5,7 @@ import 'package:at_commons/at_commons.dart'; import 'package:at_utils/at_logger.dart'; import 'package:http/http.dart' as http; -import '../../at_register.dart'; +import 'package:at_register/at_register.dart'; /// Contains methods that actually perform the RegistrarAPI calls /// and handle/process the response @@ -48,6 +48,7 @@ class RegistrarApiAccessor { }); if (response.statusCode == 200) { final jsonDecoded = jsonDecode(response.body) as Map; + // will be set to true if API response contains message with 'success' bool sentSuccessfully = jsonDecoded['message'].toLowerCase().contains('success'); return sentSuccessfully; @@ -143,16 +144,14 @@ class RegistrarApiAccessor { final response = await ApiUtil.postRequest(authority, RegistrarConstants.requestAuthenticationOtpPath, {'atsign': atsign}); final apiResponseMessage = jsonDecode(response.body)['message']; - if (response.statusCode == 200) { - if (apiResponseMessage.contains('Sent Successfully')) { - logger.info( - 'Successfully sent verification code to your registered e-mail'); - return; - } - throw AtRegisterException( - 'Unable to send verification code for authentication. | Cause: $apiResponseMessage'); + if (response.statusCode == 200 && + apiResponseMessage.contains('Sent Successfully')) { + logger.info( + 'Successfully sent verification code to your registered e-mail'); + return; } - throw AtRegisterException(apiResponseMessage); + throw AtRegisterException( + 'Unable to send verification code for authentication. | Cause: $apiResponseMessage'); } /// Returns the cram key for an atsign by fetching it from the registrar API @@ -175,7 +174,9 @@ class RegistrarApiAccessor { logger.info('CRAM Key fetched successfully'); return cram; } - throw InvalidDataException( + // If API call status is HTTP.OK / 200, but the response message does not + // contain 'Verified', that indicates incorrect verification provided by user + throw InvalidVerificationCodeException( 'Invalid verification code. Please enter a valid verification code'); } throw InvalidDataException(jsonDecodedBody['message']); diff --git a/packages/at_register/lib/src/api-interactions/validate_otp.dart b/packages/at_register/lib/src/api-interactions/validate_otp.dart index fabb2d01..baf37463 100644 --- a/packages/at_register/lib/src/api-interactions/validate_otp.dart +++ b/packages/at_register/lib/src/api-interactions/validate_otp.dart @@ -84,11 +84,10 @@ class ValidateOtp extends RegisterTask { break; case ValidateOtpStatus.failure: - result.apiCallStatus = ApiCallStatus.failure; - result.exception = validateOtpApiResult.exception; - break; case null: + + default: result.apiCallStatus = ApiCallStatus.failure; result.exception = validateOtpApiResult.exception; break; diff --git a/packages/at_register/test/at_register_allow_retry_false_test.dart b/packages/at_register/test/at_register_allow_retry_false_test.dart index a3427720..4d565b3c 100644 --- a/packages/at_register/test/at_register_allow_retry_false_test.dart +++ b/packages/at_register/test/at_register_allow_retry_false_test.dart @@ -12,8 +12,8 @@ void main() { group('Validate individual task behaviour with invalid params', () { test('validate behaviour of GetFreeAtsign', () async { RegisterParams params = RegisterParams()..email = ''; - var getFreeAtsignTask = GetFreeAtsign( - apiAccessorInstance: mockRegistrarApiAccessor); + var getFreeAtsignTask = + GetFreeAtsign(apiAccessorInstance: mockRegistrarApiAccessor); expect(() => getFreeAtsignTask.run(params), throwsA(predicate((e) => e is IllegalArgumentException))); }); @@ -23,8 +23,8 @@ void main() { ..email = 'email@email' ..atsign = null; - var registerAtsignTask = RegisterAtsign( - apiAccessorInstance: mockRegistrarApiAccessor); + var registerAtsignTask = + RegisterAtsign(apiAccessorInstance: mockRegistrarApiAccessor); expect(() => registerAtsignTask.run(params), throwsA(predicate((e) => e is IllegalArgumentException))); }); @@ -34,8 +34,8 @@ void main() { ..email = 'email@email' ..otp = null; - var validateOtpTask = ValidateOtp( - apiAccessorInstance: mockRegistrarApiAccessor); + var validateOtpTask = + ValidateOtp(apiAccessorInstance: mockRegistrarApiAccessor); expect(() => validateOtpTask.run(params), throwsA(predicate((e) => e is IllegalArgumentException))); }); @@ -46,8 +46,8 @@ void main() { ..email = 'email@email' ..atsign = '@abcd' ..otp = null; - var validateOtpTask = ValidateOtp( - apiAccessorInstance: mockRegistrarApiAccessor); + var validateOtpTask = + ValidateOtp(apiAccessorInstance: mockRegistrarApiAccessor); expect(() => validateOtpTask.run(params), throwsA(predicate((e) => e is InvalidVerificationCodeException))); }); @@ -61,8 +61,8 @@ void main() { .thenAnswer((invocation) => Future.value('@alice')); RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; - GetFreeAtsign getFreeAtsign = GetFreeAtsign( - apiAccessorInstance: mockRegistrarApiAccessor); + GetFreeAtsign getFreeAtsign = + GetFreeAtsign(apiAccessorInstance: mockRegistrarApiAccessor); final result = await getFreeAtsign.run(params); expect(result.data[RegistrarConstants.atsignName], '@alice'); @@ -74,8 +74,8 @@ void main() { .thenThrow(Exception('random exception')); RegisterParams params = RegisterParams()..email = 'abcd@gmail.com'; - GetFreeAtsign getFreeAtsign = GetFreeAtsign( - apiAccessorInstance: mockRegistrarApiAccessor); + GetFreeAtsign getFreeAtsign = + GetFreeAtsign(apiAccessorInstance: mockRegistrarApiAccessor); bool exceptionFlag = false; try { await getFreeAtsign.run(params); @@ -102,8 +102,8 @@ void main() { RegisterParams params = RegisterParams() ..atsign = atsign ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign( - apiAccessorInstance: mockRegistrarApiAccessor); + RegisterAtsign registerAtsignTask = + RegisterAtsign(apiAccessorInstance: mockRegistrarApiAccessor); RegisterTaskResult result = await registerAtsignTask.run(params); expect(result.apiCallStatus, ApiCallStatus.success); @@ -120,8 +120,8 @@ void main() { RegisterParams params = RegisterParams() ..atsign = atsign ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign( - apiAccessorInstance: mockRegistrarApiAccessor); + RegisterAtsign registerAtsignTask = + RegisterAtsign(apiAccessorInstance: mockRegistrarApiAccessor); RegisterTaskResult result = await registerAtsignTask.run(params); expect(result.apiCallStatus, ApiCallStatus.retry); @@ -139,8 +139,8 @@ void main() { ..atsign = atsign ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign( - apiAccessorInstance: mockRegistrarApiAccessor); + RegisterAtsign registerAtsignTask = + RegisterAtsign(apiAccessorInstance: mockRegistrarApiAccessor); bool exceptionFlag = false; try { await registerAtsignTask.run(params); @@ -164,8 +164,8 @@ void main() { RegisterParams params = RegisterParams() ..atsign = atsign ..email = email; - RegisterAtsign registerAtsignTask = RegisterAtsign( - apiAccessorInstance: mockRegistrarApiAccessor); + RegisterAtsign registerAtsignTask = + RegisterAtsign(apiAccessorInstance: mockRegistrarApiAccessor); bool exceptionFlag = false; try { await registerAtsignTask.run(params); @@ -203,8 +203,8 @@ void main() { ..email = email ..otp = otp; - ValidateOtp validateOtpTask = ValidateOtp( - apiAccessorInstance: mockRegistrarApiAccessor); + ValidateOtp validateOtpTask = + ValidateOtp(apiAccessorInstance: mockRegistrarApiAccessor); RegisterTaskResult result = await validateOtpTask.run(params); expect(result.data[RegistrarConstants.cramKeyName], cram); @@ -245,8 +245,8 @@ void main() { ..email = email ..otp = otp; - ValidateOtp validateOtpTask = ValidateOtp( - apiAccessorInstance: mockRegistrarApiAccessor); + ValidateOtp validateOtpTask = + ValidateOtp(apiAccessorInstance: mockRegistrarApiAccessor); RegisterTaskResult result = await validateOtpTask.run(params); expect(params.confirmation, true); // confirmation set to true by the Task expect(result.apiCallStatus, ApiCallStatus.retry); @@ -308,8 +308,8 @@ void main() { .thenThrow( MaximumAtsignQuotaException('maximum free atsign limit reached')); - ValidateOtp validateOtpTask = ValidateOtp( - apiAccessorInstance: mockRegistrarApiAccessor); + ValidateOtp validateOtpTask = + ValidateOtp(apiAccessorInstance: mockRegistrarApiAccessor); expect(() async => await validateOtpTask.run(params), throwsA(predicate((e) => e is MaximumAtsignQuotaException)));