diff --git a/packages/rtn-passkeys/ios/AmplifyRtnPasskeys-Bridging-Header.h b/packages/rtn-passkeys/ios/AmplifyRtnPasskeys-Bridging-Header.h new file mode 100644 index 00000000000..ba3d60c08c9 --- /dev/null +++ b/packages/rtn-passkeys/ios/AmplifyRtnPasskeys-Bridging-Header.h @@ -0,0 +1,5 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#import +#import diff --git a/packages/rtn-passkeys/ios/AmplifyRtnPasskeys.mm b/packages/rtn-passkeys/ios/AmplifyRtnPasskeys.mm new file mode 100644 index 00000000000..773496380a1 --- /dev/null +++ b/packages/rtn-passkeys/ios/AmplifyRtnPasskeys.mm @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#import "AmplifyRtnPasskeys.h" +#import "AmplifyRtnPasskeys-Swift.h" + +@implementation AmplifyRtnPasskeys + +- (void)createPasskey: + (JS::NativeAmplifyRtnPasskeys::PasskeyCreateOptionsJson &)input + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + + NSMutableArray *excludeCredentials = [@[] mutableCopy]; + + if (input.excludeCredentials().has_value()) { + auto credentials = input.excludeCredentials().value(); + for (const auto &credential : credentials) { + [excludeCredentials addObject:credential.id_()]; + } + } + + return [[AmplifyRtnPasskeysSwift alloc] createPasskey:input.rp().id_() + userId:input.user().id_() + userName:input.user().name() + challenge:input.challenge() + excludeCredentials:excludeCredentials + resolve:resolve + reject:reject]; +} + +- (void)getPasskey:(JS::NativeAmplifyRtnPasskeys::PasskeyGetOptionsJson &)input + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + + NSMutableArray *allowCredentials = [@[] mutableCopy]; + + if (input.allowCredentials().has_value()) { + auto credentials = input.allowCredentials().value(); + for (const auto &credential : credentials) { + [allowCredentials addObject:credential.id_()]; + } + } + + return [[AmplifyRtnPasskeysSwift alloc] getPasskey:input.rpId() + challenge:input.challenge() + userVerification:input.userVerification() + allowCredentials:allowCredentials + resolve:resolve + reject:reject]; +} + +- (nonnull NSNumber *)getIsPasskeySupported { + return [[AmplifyRtnPasskeysSwift alloc] getIsPasskeySupported]; +} + ++ (NSString *)moduleName { + return @"AmplifyRtnPasskeys"; +} + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared( + params); +} + +@end diff --git a/packages/rtn-passkeys/ios/AmplifyRtnPasskeys.swift b/packages/rtn-passkeys/ios/AmplifyRtnPasskeys.swift new file mode 100644 index 00000000000..92c17d22d56 --- /dev/null +++ b/packages/rtn-passkeys/ios/AmplifyRtnPasskeys.swift @@ -0,0 +1,154 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import AuthenticationServices +import Foundation + +@objc(AmplifyRtnPasskeysSwift) +public class AmplifyRtnPasskeys: NSObject, AmplifyRtnPasskeysResultHandler { + private var _passkeyDelegate: AmplifyRtnPasskeysDelegate? + private var _promiseHandler: AmplifyRtnPasskeysPromiseHandler? + + @objc + @available(iOS 15.0, *) + public func createPasskey( + _ rpId: String, + userId: String, + userName: String, + challenge: String, + excludeCredentials: [String], + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + + _promiseHandler = initializePromiseHandler(resolve, reject) + + guard self.getIsPasskeySupported() == true else { + handleError(errorName: "NOT_SUPPORTED", errorMessage: nil, error: nil) + return + } + + let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider( + relyingPartyIdentifier: rpId) + + let platformKeyRequest = + platformProvider.createCredentialRegistrationRequest( + challenge: challenge.toBase64UrlDecodedData(), + name: userName, + userID: userId.toBase64UrlDecodedData() + ) + + if #available(iOS 17.4, *) { + let excludedCredentials: + [ASAuthorizationPlatformPublicKeyCredentialDescriptor] = + excludeCredentials.compactMap { credentialId in + return .init(credentialID: credentialId.toBase64UrlDecodedData()) + } + + platformKeyRequest.excludedCredentials = excludedCredentials + } + + let authController = initializeAuthController( + platformKeyRequest: platformKeyRequest) + + let passkeyDelegate = initializePasskeyDelegate(resultHandler: self) + + _passkeyDelegate = passkeyDelegate + + passkeyDelegate.performAuthForController(authController) + } + + @objc + @available(iOS 15.0, *) + public func getPasskey( + _ rpId: String, + challenge: String, + userVerification: String, + allowCredentials: [String], + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + _promiseHandler = initializePromiseHandler(resolve, reject) + + guard self.getIsPasskeySupported() == true else { + handleError(errorName: "NOT_SUPPORTED") + return + } + + let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider( + relyingPartyIdentifier: rpId) + + let platformKeyRequest = platformProvider.createCredentialAssertionRequest( + challenge: challenge.toBase64UrlDecodedData() + ) + + let allowedCredentials: + [ASAuthorizationPlatformPublicKeyCredentialDescriptor] = + allowCredentials.compactMap { credentialId in + return .init(credentialID: credentialId.toBase64UrlDecodedData()) + } + + platformKeyRequest.allowedCredentials = allowedCredentials + + platformKeyRequest.userVerificationPreference = + ASAuthorizationPublicKeyCredentialUserVerificationPreference( + userVerification) + + let authController = initializeAuthController( + platformKeyRequest: platformKeyRequest) + + let passkeyDelegate = initializePasskeyDelegate(resultHandler: self) + + _passkeyDelegate = passkeyDelegate + + passkeyDelegate.performAuthForController(authController) + } + + func handleSuccess(_ data: NSDictionary) { + guard let handler = _promiseHandler else { + return + } + handler.resolve(data) + _promiseHandler = nil + _passkeyDelegate = nil + } + + func handleError( + errorName: String, errorMessage: String? = nil, error: (any Error)? = nil + ) { + guard let handler = _promiseHandler else { + return + } + handler.reject(errorName, errorMessage, error) + _promiseHandler = nil + _passkeyDelegate = nil + } + + func initializePromiseHandler( + _ resolve: @escaping RCTPromiseResolveBlock, + _ reject: @escaping RCTPromiseRejectBlock + ) -> AmplifyRtnPasskeysPromiseHandler { + return AmplifyRtnPasskeysPromiseHandler(resolve, reject) + } + + func initializePasskeyDelegate(resultHandler: AmplifyRtnPasskeysResultHandler) + -> AmplifyRtnPasskeysDelegate + { + return AmplifyRtnPasskeysDelegate(resultHandler: resultHandler) + } + + func initializeAuthController(platformKeyRequest: ASAuthorizationRequest) + -> ASAuthorizationController + { + return ASAuthorizationController(authorizationRequests: [platformKeyRequest] + ) + } + + @objc + public func getIsPasskeySupported() -> NSNumber { + if #available(iOS 17.4, *) { + return true + } + return false + } +} diff --git a/packages/rtn-passkeys/ios/AmplifyRtnPasskeysDelegate.swift b/packages/rtn-passkeys/ios/AmplifyRtnPasskeysDelegate.swift new file mode 100644 index 00000000000..0bca1870523 --- /dev/null +++ b/packages/rtn-passkeys/ios/AmplifyRtnPasskeysDelegate.swift @@ -0,0 +1,119 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import AuthenticationServices +import Foundation + +class AmplifyRtnPasskeysDelegate: NSObject, + ASAuthorizationControllerDelegate, + ASAuthorizationControllerPresentationContextProviding +{ + private static let PUBLIC_KEY_TYPE = "public-key" + private static let PLATFORM_ATTACHMENT = "platform" + private static let INTERNAL_TRANSPORT = "internal" + + private static let ERROR_MAP: [Int: String] = [ + 1000: "UNKNOWN", + 1001: "CANCELED", + 1002: "INVALID_RESPONSE", + 1003: "NOT_HANDLED", + 1004: "FAILED", + 1005: "NOT_INTERACTIVE", + 1006: "DUPLICATE", + ] + + let _resultHandler: AmplifyRtnPasskeysResultHandler + + init(resultHandler: AmplifyRtnPasskeysResultHandler) { + _resultHandler = resultHandler + } + + func performAuthForController(_ authController: ASAuthorizationController) { + authController.delegate = self + authController.presentationContextProvider = self + authController.performRequests() + } + + func authorizationController( + controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization + ) { + + switch authorization.credential { + case let assertionCredential + as ASAuthorizationPlatformPublicKeyCredentialAssertion: + + let assertionResult: NSDictionary = [ + "id": assertionCredential.credentialID.toBase64UrlEncodedString(), + "rawId": assertionCredential.credentialID.toBase64UrlEncodedString(), + "authenticatorAttachment": AmplifyRtnPasskeysDelegate + .PLATFORM_ATTACHMENT, + "type": AmplifyRtnPasskeysDelegate.PUBLIC_KEY_TYPE, + "response": [ + "authenticatorData": assertionCredential.rawAuthenticatorData + .toBase64UrlEncodedString(), + "clientDataJSON": assertionCredential.rawClientDataJSON + .toBase64UrlEncodedString(), + "signature": assertionCredential.signature.toBase64UrlEncodedString(), + "userHandle": assertionCredential.userID.toBase64UrlEncodedString(), + ], + ] + + _resultHandler.handleSuccess(assertionResult) + + case let registrationCredential + as ASAuthorizationPlatformPublicKeyCredentialRegistration: + let registrationResult: NSDictionary = [ + "id": registrationCredential.credentialID.toBase64UrlEncodedString(), + "rawId": registrationCredential.credentialID.toBase64UrlEncodedString(), + "authenticatorAttachment": AmplifyRtnPasskeysDelegate + .PLATFORM_ATTACHMENT, + "type": AmplifyRtnPasskeysDelegate.PUBLIC_KEY_TYPE, + "response": [ + "attestationObject": registrationCredential.rawAttestationObject! + .toBase64UrlEncodedString(), + "clientDataJSON": registrationCredential.rawClientDataJSON + .toBase64UrlEncodedString(), + "transports": [AmplifyRtnPasskeysDelegate.INTERNAL_TRANSPORT], + ], + ] + + _resultHandler.handleSuccess(registrationResult) + + default: + _resultHandler.handleError( + errorName: "FAILED", errorMessage: nil, error: nil) + } + } + + func authorizationController( + controller: ASAuthorizationController, + didCompleteWithError error: any Error + ) { + let errorMessage = error.localizedDescription + + var errorName = + AmplifyRtnPasskeysDelegate.ERROR_MAP[(error as NSError).code] ?? "UNKNOWN" + + // pre-iOS 18 does not through explicit error for duplicate + if errorMessage.contains( + "credential matches an entry of the excludeCredentials list") + { + errorName = "DUPLICATE" + } + + // no explicit error with for SecurityError + if errorMessage.contains("not associated with domain") { + errorName = "RELYING_PARTY_MISMATCH" + } + + _resultHandler.handleError( + errorName: errorName, errorMessage: errorMessage, error: error) + } + + func presentationAnchor(for controller: ASAuthorizationController) + -> ASPresentationAnchor + { + return ASPresentationAnchor() + } +} diff --git a/packages/rtn-passkeys/ios/AmplifyRtnPasskeysHelpers.swift b/packages/rtn-passkeys/ios/AmplifyRtnPasskeysHelpers.swift new file mode 100644 index 00000000000..d03434eb4a2 --- /dev/null +++ b/packages/rtn-passkeys/ios/AmplifyRtnPasskeysHelpers.swift @@ -0,0 +1,45 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +extension String { + // Converts base64Url encoded string to base64 Data + func toBase64UrlDecodedData() -> Data { + var base64String = self.replacingOccurrences(of: "_", with: "/") + .replacingOccurrences(of: "-", with: "+") + + while base64String.count % 4 != 0 { + base64String.append("=") + } + + return Data(base64Encoded: base64String) ?? Data() + } +} + +extension Data { + // Converts base64 Data to base64url String + func toBase64UrlEncodedString() -> String { + return self.base64EncodedString() + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "=", with: "") + } +} + +struct AmplifyRtnPasskeysPromiseHandler { + let resolve: RCTPromiseResolveBlock + let reject: RCTPromiseRejectBlock + + init( + _ resolve: @escaping RCTPromiseResolveBlock, + _ reject: @escaping RCTPromiseRejectBlock + ) { + self.resolve = resolve + self.reject = reject + } +} + +protocol AmplifyRtnPasskeysResultHandler { + func handleSuccess(_ data: NSDictionary) + func handleError( + errorName: String, errorMessage: String?, error: (any Error)?) +} diff --git a/packages/rtn-passkeys/ios/tests/AmplifyRtnPasskeysDelegateTests.swift b/packages/rtn-passkeys/ios/tests/AmplifyRtnPasskeysDelegateTests.swift new file mode 100644 index 00000000000..90bd6c4ec5b --- /dev/null +++ b/packages/rtn-passkeys/ios/tests/AmplifyRtnPasskeysDelegateTests.swift @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import XCTest + +@testable import AmplifyRtnPasskeys + +class MockASAuthorizationController: ASAuthorizationController {} +class MockASAuthorization: ASAuthorization, @unchecked Sendable {} + +class AmplifyRtnPasskeysDelegateTests: XCTestCase, + AmplifyRtnPasskeysResultHandler +{ + private var mockResolve: RCTPromiseResolveBlock! + private var mockReject: RCTPromiseRejectBlock! + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func handleSuccess(_ data: NSDictionary) { + mockResolve(data) + } + + func handleError( + errorName: String, errorMessage: String?, error: (any Error)? + ) { + mockReject(errorName, errorMessage, error) + } + + func testInitializedWithResultHandler() { + let passkeyDelegate = AmplifyRtnPasskeysDelegate(resultHandler: self) + + XCTAssertEqual( + self, + passkeyDelegate._resultHandler as! AmplifyRtnPasskeysDelegateTests) + } + +} diff --git a/packages/rtn-passkeys/ios/tests/AmplifyRtnPasskeysHelpersTests.swift b/packages/rtn-passkeys/ios/tests/AmplifyRtnPasskeysHelpersTests.swift new file mode 100644 index 00000000000..a97657c1a67 --- /dev/null +++ b/packages/rtn-passkeys/ios/tests/AmplifyRtnPasskeysHelpersTests.swift @@ -0,0 +1,41 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import XCTest + +@testable import AmplifyRtnPasskeys + +class AmplifyRtnPasskeysHelpersTests: XCTestCase { + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testToBase64UrlDecodedData() { + XCTAssertEqual( + TestFixtures.base64UrlString.toBase64UrlDecodedData(), + Data(base64Encoded: TestFixtures.base64String), + "Base64Url decoding should work correctly") + } + + func testToBase64UrlEncodedString() { + XCTAssertEqual( + Data(base64Encoded: TestFixtures.base64String)?.toBase64UrlEncodedString(), + TestFixtures.base64UrlString, + "Base64Url encoding should work correctly") + } + + func testBase64UrlTranscodeRoundTrip() { + XCTAssertEqual( + TestFixtures.base64UrlString, + TestFixtures.base64UrlString.toBase64UrlDecodedData().toBase64UrlEncodedString(), + "Base64Url encoding and decoding should work correctly") + } + + + +} diff --git a/packages/rtn-passkeys/ios/tests/AmplifyRtnPasskeysTestFixtures.swift b/packages/rtn-passkeys/ios/tests/AmplifyRtnPasskeysTestFixtures.swift new file mode 100644 index 00000000000..0e0e75958ad --- /dev/null +++ b/packages/rtn-passkeys/ios/tests/AmplifyRtnPasskeysTestFixtures.swift @@ -0,0 +1,65 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import AuthenticationServices +import Foundation + +struct TestFixtures { + // AmplifyRtnPasskeysHelpersTests + static let base64String = "PDw/Pz8+Pg==" + static let base64UrlString = "PDw_Pz8-Pg" + + // AmplifyRtnPasskeysTests + static let validRpId = "example.com" + static let validChallenge = "Y2hhbGxlbmdlLXZhbHVl" + static let invalidChallenge = "invalid-challenge" + + static let validUserId = "dXNlci1pZC12YWx1ZQ==" + static let validUserName = "testuser@example.com" + + static let validExcludeCredentials = [ + "Y3JlZGVudGlhbC1pZC0x", + "Y3JlZGVudGlhbC1pZC0y", + ] + + static let validUserVerificationRequired = "required" + + static let validAllowCredentials = [ + "Y3JlZGVudGlhbC1pZC0x", + "Y3JlZGVudGlhbC1pZC0y", + ] + + static func createPasskeySuccess() -> NSDictionary { + return [ + "id": "Y3JlZGVudGlhbC1pZC1uZXc=", + "rawId": "Y3JlZGVudGlhbC1pZC1uZXc=", + "type": "public-key", + "authenticatorAttachment": "platform", + "response": [ + "clientDataJSON": + "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiWTJoaGJHeGxibWRsTFhaaGJIVmwiLCJvcmlnaW4iOiJodHRwczovL2V4YW1wbGUuY29tIn0=", + "attestationObject": "bzJoZWxsb3dvcmxkZw==", + "transports": ["internal"], + ], + ] + } + + static func getPasskeySuccess() -> NSDictionary { + return [ + "id": "Y3JlZGVudGlhbC1pZC0x", + "rawId": "Y3JlZGVudGlhbC1pZC0x", + "type": "public-key", + "authenticatorAttachment": "platform", + "response": [ + "authenticatorData": + "SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MBAAAACA==", + "clientDataJSON": + "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiWTJoaGJHeGxibWRsTFhaaGJIVmwiLCJvcmlnaW4iOiJodHRwczovL2V4YW1wbGUuY29tIn0=", + "signature": + "MEUCIQDxvUz+tIFPrWWRtJJbYHFHNmWdRYPi0EvCEiN+aXiOQQIgEfXxDHbH0q8+htk7qacVFniQKz85KYQMz3HEPDC9hok=", + "userHandle": "dXNlci1pZC12YWx1ZQ==", + ], + ] + + } +} diff --git a/packages/rtn-passkeys/ios/tests/AmplifyRtnPasskeysTests.swift b/packages/rtn-passkeys/ios/tests/AmplifyRtnPasskeysTests.swift new file mode 100644 index 00000000000..1007bde3b94 --- /dev/null +++ b/packages/rtn-passkeys/ios/tests/AmplifyRtnPasskeysTests.swift @@ -0,0 +1,236 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import XCTest + +@testable import AmplifyRtnPasskeys + +class AmplifyRtnPasskeysTests: XCTestCase { + private var mockResolve: RCTPromiseResolveBlock! + private var mockReject: RCTPromiseRejectBlock! + + private var resolveCalled = false + private var resolveValue: Any? + + private var rejectCalled = false + private var rejectErrorName: String? + private var rejectErrorMessage: String? + + override func setUp() { + super.setUp() + + mockResolve = { value in + self.resolveCalled = true + self.resolveValue = value + } + + mockReject = { errorName, errorMessage, error in + self.rejectCalled = true + self.rejectErrorName = errorName + self.rejectErrorMessage = errorMessage + } + } + + override func tearDown() { + super.tearDown() + + resolveCalled = false + resolveValue = nil + + rejectCalled = false + rejectErrorName = nil + rejectErrorMessage = nil + } + + func testGetPasskeyShouldRejectWhenNotSupported() { + class MockAmplifyRtnPasskeys: AmplifyRtnPasskeys { + override func getIsPasskeySupported() -> NSNumber { + return false + } + } + + let mockInstance = MockAmplifyRtnPasskeys() + + mockInstance.getPasskey( + TestFixtures.validRpId, + challenge: TestFixtures.validChallenge, + userVerification: TestFixtures + .validUserVerificationRequired, + allowCredentials: TestFixtures + .validAllowCredentials, resolve: mockResolve, reject: mockReject) + + XCTAssertTrue(rejectCalled) + XCTAssertTrue(!resolveCalled) + XCTAssertEqual(rejectErrorName, "NOT_SUPPORTED") + XCTAssertEqual(rejectErrorMessage, nil) + } + + func testCreatePasskeyShouldRejectWhenNotSupported() { + class MockAmplifyRtnPasskeys: AmplifyRtnPasskeys { + override func getIsPasskeySupported() -> NSNumber { + return false + } + } + + let mockInstance = MockAmplifyRtnPasskeys() + + mockInstance.createPasskey( + TestFixtures.validRpId, + userId: TestFixtures.validUserId, + userName: TestFixtures.validUserName, + challenge: TestFixtures.validChallenge, + excludeCredentials: TestFixtures + .validExcludeCredentials, resolve: mockResolve, reject: mockReject) + + XCTAssertTrue(rejectCalled) + XCTAssertTrue(!resolveCalled) + XCTAssertEqual(rejectErrorName, "NOT_SUPPORTED") + XCTAssertEqual(rejectErrorMessage, nil) + } + + func testGetPasskeyShouldResolveWithAssertionResultFromDelegate() { + class MockAmplifyRtnPasskeys: AmplifyRtnPasskeys { + override func getIsPasskeySupported() -> NSNumber { + return true + } + override func initializePasskeyDelegate( + resultHandler: any AmplifyRtnPasskeysResultHandler + ) -> AmplifyRtnPasskeysDelegate { + class MockAmplifyRtnPasskeysDelegate: AmplifyRtnPasskeysDelegate { + override func performAuthForController( + _ authController: ASAuthorizationController + ) { + self._resultHandler.handleSuccess(TestFixtures.getPasskeySuccess()) + } + } + return MockAmplifyRtnPasskeysDelegate(resultHandler: self) + } + } + + let mockInstance = MockAmplifyRtnPasskeys() + + mockInstance.getPasskey( + TestFixtures.validRpId, + challenge: TestFixtures.validChallenge, + userVerification: TestFixtures + .validUserVerificationRequired, + allowCredentials: TestFixtures + .validAllowCredentials, resolve: mockResolve, reject: mockReject) + + XCTAssertTrue(resolveCalled) + XCTAssertTrue(!rejectCalled) + XCTAssertEqual( + resolveValue as! NSDictionary, TestFixtures.getPasskeySuccess()) + + } + + func testCreatePasskeyShouldResolveWithRegistrationResultFromDelegate() { + class MockAmplifyRtnPasskeys: AmplifyRtnPasskeys { + override func getIsPasskeySupported() -> NSNumber { + return true + } + override func initializePasskeyDelegate( + resultHandler: any AmplifyRtnPasskeysResultHandler + ) -> AmplifyRtnPasskeysDelegate { + class MockAmplifyRtnPasskeysDelegate: AmplifyRtnPasskeysDelegate { + override func performAuthForController( + _ authController: ASAuthorizationController + ) { + self._resultHandler.handleSuccess( + TestFixtures.createPasskeySuccess()) + } + } + return MockAmplifyRtnPasskeysDelegate(resultHandler: self) + } + } + + let mockInstance = MockAmplifyRtnPasskeys() + + mockInstance.createPasskey( + TestFixtures.validRpId, + userId: TestFixtures.validUserId, + userName: TestFixtures.validUserName, + challenge: TestFixtures.validChallenge, + excludeCredentials: TestFixtures + .validExcludeCredentials, resolve: mockResolve, reject: mockReject) + + XCTAssertTrue(resolveCalled) + XCTAssertTrue(!rejectCalled) + XCTAssertEqual( + resolveValue as! NSDictionary, TestFixtures.createPasskeySuccess()) + } + + func testGetPasskeyShouldRejectWithErrorFromDelegate() { + class MockAmplifyRtnPasskeys: AmplifyRtnPasskeys { + override func getIsPasskeySupported() -> NSNumber { + return true + } + override func initializePasskeyDelegate( + resultHandler: any AmplifyRtnPasskeysResultHandler + ) -> AmplifyRtnPasskeysDelegate { + class MockAmplifyRtnPasskeysDelegate: AmplifyRtnPasskeysDelegate { + override func performAuthForController( + _ authController: ASAuthorizationController + ) { + self._resultHandler.handleError( + errorName: "SOME_ERROR", errorMessage: "some error message", + error: nil) + } + } + return MockAmplifyRtnPasskeysDelegate(resultHandler: self) + } + } + + let mockInstance = MockAmplifyRtnPasskeys() + + mockInstance.getPasskey( + TestFixtures.validRpId, + challenge: TestFixtures.validChallenge, + userVerification: TestFixtures + .validUserVerificationRequired, + allowCredentials: TestFixtures + .validAllowCredentials, resolve: mockResolve, reject: mockReject) + + XCTAssertTrue(rejectCalled) + XCTAssertTrue(!resolveCalled) + XCTAssertEqual(rejectErrorName, "SOME_ERROR") + XCTAssertEqual(rejectErrorMessage, "some error message") + } + + func testCreatePasskeyShouldRejectWithErrorFromDelegate() { + class MockAmplifyRtnPasskeys: AmplifyRtnPasskeys { + override func getIsPasskeySupported() -> NSNumber { + return true + } + override func initializePasskeyDelegate( + resultHandler: any AmplifyRtnPasskeysResultHandler + ) -> AmplifyRtnPasskeysDelegate { + class MockAmplifyRtnPasskeysDelegate: AmplifyRtnPasskeysDelegate { + override func performAuthForController( + _ authController: ASAuthorizationController + ) { + self._resultHandler.handleError( + errorName: "SOME_ERROR", errorMessage: "some error message", + error: nil) + } + } + return MockAmplifyRtnPasskeysDelegate(resultHandler: self) + } + } + + let mockInstance = MockAmplifyRtnPasskeys() + + mockInstance.createPasskey( + TestFixtures.validRpId, + userId: TestFixtures.validUserId, + userName: TestFixtures.validUserName, + challenge: TestFixtures.validChallenge, + excludeCredentials: TestFixtures + .validExcludeCredentials, resolve: mockResolve, reject: mockReject) + + XCTAssertTrue(rejectCalled) + XCTAssertTrue(!resolveCalled) + XCTAssertEqual(rejectErrorName, "SOME_ERROR") + XCTAssertEqual(rejectErrorMessage, "some error message") + } +}