| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091 |
- // Copyright 2023 Google LLC
- //
- // Licensed under the Apache License, Version 2.0 (the "License")
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- #if os(iOS)
- import Foundation
- import XCTest
- @testable import FirebaseAuth
- import FirebaseCore
- import SafariServices
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- class PhoneAuthProviderTests: RPCBaseTests {
- static let kFakeAuthorizedDomain = "test.firebaseapp.com"
- static let kFakeAPIKey = "asdfghjkl"
- static let kFakeEmulatorHost = "emulatorhost"
- static let kFakeEmulatorPort = 12345
- static let kFakeClientID = "123456.apps.googleusercontent.com"
- static let kFakeFirebaseAppID = "1:123456789:ios:123abc456def"
- static let kFakeEncodedFirebaseAppID = "app-1-123456789-ios-123abc456def"
- static let kFakeTenantID = "tenantID"
- static let kFakeReverseClientID = "com.googleusercontent.apps.123456"
- private let kTestVerificationID = "verificationID"
- private let kTestVerificationCode = "verificationCode"
- private let kTestReceipt = "receipt"
- private let kTestTimeout = "1"
- private let kTestSecret = "secret"
- private let kVerificationIDKey = "sessionInfo"
- private let kFakeEncodedFirebaseAppID = "app-1-123456789-ios-123abc456def"
- private let kFakeReCAPTCHAToken = "fakeReCAPTCHAToken"
- private let kCaptchaResponse: String = "captchaResponse"
- private let kRecaptchaVersion: String = "RECAPTCHA_ENTERPRISE"
- static var auth: Auth?
- // static var authRecaptchaVerifier: AuthRecaptchaVerifier
- /** @fn testCredentialWithVerificationID
- @brief Tests the @c credentialWithToken method to make sure that it returns a valid AuthCredential instance.
- */
- func testCredentialWithVerificationID() throws {
- initApp(#function)
- let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
- let provider = PhoneAuthProvider.provider(auth: auth)
- let credential = provider.credential(withVerificationID: kTestVerificationID,
- verificationCode: kTestVerificationCode)
- switch credential.credentialKind {
- case .phoneNumber: XCTFail("Should be verification case")
- case let .verification(id, code):
- XCTAssertEqual(id, kTestVerificationID)
- XCTAssertEqual(code, kTestVerificationCode)
- }
- }
- /** @fn testVerifyEmptyPhoneNumber
- @brief Tests a failed invocation verifyPhoneNumber because an empty phone
- number was provided.
- */
- func testVerifyEmptyPhoneNumber() async throws {
- initApp(#function)
- let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
- let provider = PhoneAuthProvider.provider(auth: auth)
- do {
- _ = try await provider.verifyPhoneNumber("")
- XCTFail("Expected an error, but verification succeeded.")
- } catch {
- XCTAssertEqual((error as NSError).code, AuthErrorCode.missingPhoneNumber.rawValue)
- }
- }
- /** @fn testVerifyInvalidPhoneNumber
- @brief Tests a failed invocation @c verifyPhoneNumber:completion: because an invalid phone
- number was provided.
- */
- func testVerifyInvalidPhoneNumber() async throws {
- try await internalTestVerify(errorString: "INVALID_PHONE_NUMBER",
- errorCode: AuthErrorCode.invalidPhoneNumber.rawValue,
- function: #function)
- }
- /** @fn testVerifyPhoneNumber
- @brief Tests a successful invocation of @c verifyPhoneNumber:completion:.
- */
- func testVerifyPhoneNumber() async throws {
- try await internalTestVerify(function: #function)
- }
- /**
- @fn testVerifyPhoneNumberWithRceEnforce
- @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
- */
- func testVerifyPhoneNumberWithRceEnforceSuccess() async throws {
- initApp(#function)
- let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
- // TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response
- let provider = PhoneAuthProvider.provider(auth: auth)
- let mockVerifier = FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse)
- AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
- rpcIssuer.rceMode = "ENFORCE"
- let requestExpectation = expectation(description: "verifyRequester")
- rpcIssuer?.verifyRequester = { request in
- XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
- XCTAssertEqual(request.captchaResponse, self.kCaptchaResponse)
- XCTAssertEqual(request.recaptchaVersion, "RECAPTCHA_ENTERPRISE")
- XCTAssertEqual(request.codeIdentity, CodeIdentity.empty)
- requestExpectation.fulfill()
- do {
- try self.rpcIssuer?
- .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
- } catch {
- XCTFail("Failure sending response: \(error)")
- }
- }
- do {
- let result = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
- toPhoneNumber: kTestPhoneNumber,
- retryOnInvalidAppCredential: false,
- uiDelegate: nil,
- recaptchaVerifier: mockVerifier
- )
- XCTAssertEqual(result, kTestVerificationID)
- } catch {
- XCTFail("Unexpected error")
- }
- await fulfillment(of: [requestExpectation], timeout: 5.0)
- }
- /**
- @fn testVerifyPhoneNumberWithRceEnforceInvalidRecaptcha
- @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
- */
- func testVerifyPhoneNumberWithRceEnforceInvalidRecaptcha() async throws {
- initApp(#function)
- let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
- // TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response
- let provider = PhoneAuthProvider.provider(auth: auth)
- let mockVerifier = FakeAuthRecaptchaVerifier()
- AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
- rpcIssuer.rceMode = "ENFORCE"
- let requestExpectation = expectation(description: "verifyRequester")
- rpcIssuer?.verifyRequester = { request in
- XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
- XCTAssertEqual(request.captchaResponse, "NO_RECAPTCHA")
- XCTAssertEqual(request.recaptchaVersion, "RECAPTCHA_ENTERPRISE")
- XCTAssertEqual(request.codeIdentity, CodeIdentity.empty)
- requestExpectation.fulfill()
- do {
- try self.rpcIssuer?
- .respond(
- serverErrorMessage: "INVALID_RECAPTCHA_TOKEN",
- error: AuthErrorUtils.invalidRecaptchaTokenError() as NSError
- )
- } catch {
- XCTFail("Failure sending response: \(error)")
- }
- }
- do {
- _ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
- toPhoneNumber: kTestPhoneNumber,
- retryOnInvalidAppCredential: false,
- uiDelegate: nil,
- recaptchaVerifier: mockVerifier
- )
- // XCTAssertEqual(result, kTestVerificationID)
- } catch {
- // Traverse the nested error to find the root cause
- let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError
- let rootError = underlyingError?.userInfo[NSUnderlyingErrorKey] as? NSError
- // Compare the root error code to the expected error code
- XCTAssertEqual(rootError?.code, AuthErrorCode.invalidRecaptchaToken.code.rawValue)
- }
- await fulfillment(of: [requestExpectation], timeout: 5.0)
- }
- /**
- @fn testVerifyPhoneNumberWithRceEnforceSDKNotLinked
- @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
- */
- func testVerifyPhoneNumberWithRceEnforceRecaptchaSDKNotLinked() async throws {
- return try await testRecaptchaFlowError(
- function: #function,
- rceError: AuthErrorUtils.recaptchaSDKNotLinkedError()
- )
- }
- /**
- @fn testVerifyPhoneNumberWithRceEnforceSDKNotLinked
- @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
- */
- func testVerifyPhoneNumberWithRceEnforceRecaptchaActionCreationFailed() async throws {
- return try await testRecaptchaFlowError(
- function: #function,
- rceError: AuthErrorUtils.recaptchaActionCreationFailed()
- )
- }
- /// @fn testVerifyPhoneNumberWithRceAudit
- /// @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise in
- /// audit mode
- func testVerifyPhoneNumberWithRceAuditSuccess() async throws {
- initApp(#function)
- let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
- let provider = PhoneAuthProvider.provider(auth: auth)
- let mockVerifier = FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse)
- AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
- rpcIssuer.rceMode = "AUDIT"
- let requestExpectation = expectation(description: "verifyRequester")
- rpcIssuer?.verifyRequester = { request in
- XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
- XCTAssertEqual(request.captchaResponse, self.kCaptchaResponse)
- XCTAssertEqual(request.recaptchaVersion, "RECAPTCHA_ENTERPRISE")
- XCTAssertEqual(request.codeIdentity, CodeIdentity.empty)
- requestExpectation.fulfill()
- do {
- try self.rpcIssuer?
- .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
- } catch {
- XCTFail("Failure sending response: \(error)")
- }
- }
- do {
- let result = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
- toPhoneNumber: kTestPhoneNumber,
- retryOnInvalidAppCredential: false,
- uiDelegate: nil,
- recaptchaVerifier: mockVerifier
- )
- XCTAssertEqual(result, kTestVerificationID)
- } catch {
- XCTFail("Unexpected error")
- }
- await fulfillment(of: [requestExpectation], timeout: 5.0)
- }
- /// @fn testVerifyPhoneNumberWithRceAuditInvalidRecaptcha
- /// @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise in
- /// audit mode
- func testVerifyPhoneNumberWithRceAuditInvalidRecaptcha() async throws {
- initApp(#function)
- let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
- let provider = PhoneAuthProvider.provider(auth: auth)
- let mockVerifier = FakeAuthRecaptchaVerifier()
- AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
- rpcIssuer.rceMode = "AUDIT"
- let requestExpectation = expectation(description: "verifyRequester")
- rpcIssuer?.verifyRequester = { request in
- XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
- XCTAssertEqual(request.captchaResponse, "NO_RECAPTCHA")
- XCTAssertEqual(request.recaptchaVersion, "RECAPTCHA_ENTERPRISE")
- XCTAssertEqual(request.codeIdentity, CodeIdentity.empty)
- requestExpectation.fulfill()
- do {
- try self.rpcIssuer?
- .respond(
- serverErrorMessage: "INVALID_RECAPTCHA_TOKEN",
- error: AuthErrorUtils.invalidRecaptchaTokenError() as NSError
- )
- } catch {
- XCTFail("Failure sending response: \(error)")
- }
- }
- do {
- _ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
- toPhoneNumber: kTestPhoneNumber,
- retryOnInvalidAppCredential: false,
- uiDelegate: nil,
- recaptchaVerifier: mockVerifier
- )
- } catch {
- let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError
- let rootError = underlyingError?.userInfo[NSUnderlyingErrorKey] as? NSError
- XCTAssertEqual(rootError?.code, AuthErrorCode.invalidRecaptchaToken.code.rawValue)
- }
- await fulfillment(of: [requestExpectation], timeout: 5.0)
- }
- /** @fn testVerifyPhoneNumberInTestMode
- @brief Tests a successful invocation of @c verifyPhoneNumber:completion: when app verification
- is disabled.
- */
- func testVerifyPhoneNumberInTestMode() async throws {
- try await internalTestVerify(function: #function, testMode: true)
- }
- /** @fn testVerifyPhoneNumberInTestModeFailure
- @brief Tests a failed invocation of @c verifyPhoneNumber:completion: when app verification
- is disabled.
- */
- func testVerifyPhoneNumberInTestModeFailure() async throws {
- try await internalTestVerify(errorString: "INVALID_PHONE_NUMBER",
- errorCode: AuthErrorCode.invalidPhoneNumber.rawValue,
- function: #function, testMode: true)
- }
- /** @fn testVerifyPhoneNumberUIDelegateFirebaseAppIdFlow
- @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion:.
- */
- func testVerifyPhoneNumberUIDelegateFirebaseAppIdFlow() async throws {
- try await internalTestVerify(function: #function, reCAPTCHAfallback: true)
- }
- /** @fn testVerifyPhoneNumberUIDelegateFirebaseAppIdWhileClientIdPresentFlow
- @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion: when the
- client ID is present in the plist file, but the encoded app ID is the registered custom URL scheme.
- */
- func testVerifyPhoneNumberUIDelegateFirebaseAppIdWhileClientIdPresentFlow() async throws {
- try await internalTestVerify(function: #function, useClientID: true,
- bothClientAndAppID: true, reCAPTCHAfallback: true)
- }
- /** @fn testVerifyPhoneNumberUIDelegateClientIdFlow
- @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion:.
- */
- func testVerifyPhoneNumberUIDelegateClientIdFlow() async throws {
- try await internalTestVerify(function: #function, useClientID: true, reCAPTCHAfallback: true)
- }
- /** @fn testVerifyPhoneNumberUIDelegateInvalidClientID
- @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
- invalid client ID error.
- */
- func testVerifyPhoneNumberUIDelegateInvalidClientID() async throws {
- try await internalTestVerify(
- errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringInvalidClientID,
- errorCode: AuthErrorCode.invalidClientID.rawValue,
- function: #function,
- useClientID: true,
- reCAPTCHAfallback: true
- )
- }
- /** @fn testVerifyPhoneNumberUIDelegateWebNetworkRequestFailed
- @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in a web
- network request failed error.
- */
- func testVerifyPhoneNumberUIDelegateWebNetworkRequestFailed() async throws {
- try await internalTestVerify(
- errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringWebNetworkRequestFailed,
- errorCode: AuthErrorCode.webNetworkRequestFailed.rawValue,
- function: #function,
- useClientID: true,
- reCAPTCHAfallback: true
- )
- }
- /** @fn testVerifyPhoneNumberUIDelegateWebInternalError
- @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in a web
- internal error.
- */
- func testVerifyPhoneNumberUIDelegateWebInternalError() async throws {
- try await internalTestVerify(
- errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringWebInternalError,
- errorCode: AuthErrorCode.webInternalError.rawValue,
- function: #function,
- useClientID: true,
- reCAPTCHAfallback: true
- )
- }
- /** @fn testVerifyPhoneNumberUIDelegateUnexpectedError
- @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
- invalid client ID.
- */
- func testVerifyPhoneNumberUIDelegateUnexpectedError() async throws {
- try await internalTestVerify(
- errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringUnknownError,
- errorCode: AuthErrorCode.webSignInUserInteractionFailure.rawValue,
- function: #function,
- useClientID: true,
- reCAPTCHAfallback: true
- )
- }
- /** @fn testVerifyPhoneNumberUIDelegateUnstructuredError
- @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
- error being surfaced with a default NSLocalizedFailureReasonErrorKey due to an unexpected
- structure of the error response.
- */
- func testVerifyPhoneNumberUIDelegateUnstructuredError() async throws {
- try await internalTestVerify(
- errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringUnstructuredError,
- errorCode: AuthErrorCode.appVerificationUserInteractionFailure.rawValue,
- function: #function,
- useClientID: true,
- reCAPTCHAfallback: true
- )
- }
- // TODO: This test is skipped. What was formerly an Objective-C exception is now a Swift fatal_error.
- // The test runs correctly, but it's not clear how to automate fatal_error testing. Switching to
- // Swift exceptions would break the API.
- /** @fn testVerifyPhoneNumberUIDelegateRaiseException
- @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
- exception.
- */
- func SKIPtestVerifyPhoneNumberUIDelegateRaiseException() async throws {
- initApp(#function)
- let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
- auth.mainBundleUrlTypes = [["CFBundleURLSchemes": ["fail"]]]
- let provider = PhoneAuthProvider.provider(auth: auth)
- provider.verifyPhoneNumber(kTestPhoneNumber, uiDelegate: nil) { verificationID, error in
- XCTFail("Should not call completion")
- }
- }
- /** @fn testNotForwardingNotification
- @brief Tests returning an error for the app failing to forward notification.
- */
- func testNotForwardingNotification() throws {
- func testVerifyPhoneNumberUIDelegateUnstructuredError() async throws {
- try await internalTestVerify(
- errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringUnstructuredError,
- errorCode: AuthErrorCode.appVerificationUserInteractionFailure.rawValue,
- function: #function,
- useClientID: true,
- reCAPTCHAfallback: true,
- forwardingNotification: false
- )
- }
- }
- /** @fn testMissingAPNSToken
- @brief Tests returning an error for the app failing to provide an APNS device token.
- */
- func testMissingAPNSToken() async throws {
- try await internalTestVerify(
- errorCode: AuthErrorCode.missingAppToken.rawValue,
- function: #function,
- useClientID: true,
- reCAPTCHAfallback: true,
- presenterError: NSError(
- domain: AuthErrors.domain,
- code: AuthErrorCode.missingAppToken.rawValue
- )
- )
- }
- /** @fn testVerifyPhoneNumberUIDelegateiOSSecretMissingFlow
- @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion: that falls
- back to the reCAPTCHA flow when the push notification is not received before the timeout.
- */
- func testVerifyPhoneNumberUIDelegateiOSSecretMissingFlow() throws {
- try internalFlow(function: #function, useClientID: false, reCAPTCHAfallback: true)
- }
- /** @fn testVerifyClient
- @brief Tests verifying client before sending verification code.
- */
- func testVerifyClient() throws {
- try internalFlow(function: #function, useClientID: true, reCAPTCHAfallback: false)
- }
- /** @fn testSendVerificationCodeFailedRetry
- @brief Tests failed retry after failing to send verification code.
- */
- func testSendVerificationCodeFailedRetry() throws {
- try internalFlowRetry(function: #function)
- }
- /** @fn testSendVerificationCodeSuccessfulRetry
- @brief Tests successful retry after failing to send verification code.
- */
- func testSendVerificationCodeSuccessfulRetry() throws {
- try internalFlowRetry(function: #function, goodRetry: true)
- }
- /** @fn testPhoneAuthCredentialCoding
- @brief Tests successful archiving and unarchiving of @c PhoneAuthCredential.
- */
- func testPhoneAuthCredentialCoding() throws {
- let kVerificationID = "My verificationID"
- let kVerificationCode = "1234"
- let credential = PhoneAuthCredential(withProviderID: PhoneAuthProvider.id,
- verificationID: kVerificationID,
- verificationCode: kVerificationCode)
- XCTAssertTrue(PhoneAuthCredential.supportsSecureCoding)
- let data = try NSKeyedArchiver.archivedData(
- withRootObject: credential,
- requiringSecureCoding: true
- )
- let unarchivedCredential = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
- ofClass: PhoneAuthCredential.self, from: data
- ))
- switch unarchivedCredential.credentialKind {
- case .phoneNumber: XCTFail("Should be verification case")
- case let .verification(id, code):
- XCTAssertEqual(id, kVerificationID)
- XCTAssertEqual(code, kVerificationCode)
- }
- XCTAssertEqual(unarchivedCredential.provider, PhoneAuthProvider.id)
- }
- /** @fn testPhoneAuthCredentialCodingPhone
- @brief Tests successful archiving and unarchiving of @c PhoneAuthCredential after other constructor.
- */
- func testPhoneAuthCredentialCodingPhone() throws {
- let kTemporaryProof = "Proof"
- let kPhoneNumber = "123457"
- let credential = PhoneAuthCredential(withTemporaryProof: kTemporaryProof,
- phoneNumber: kPhoneNumber,
- providerID: PhoneAuthProvider.id)
- XCTAssertTrue(PhoneAuthCredential.supportsSecureCoding)
- let data = try NSKeyedArchiver.archivedData(
- withRootObject: credential,
- requiringSecureCoding: true
- )
- let unarchivedCredential = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
- ofClasses: [PhoneAuthCredential.self, NSString.self], from: data
- ) as? PhoneAuthCredential)
- switch unarchivedCredential.credentialKind {
- case let .phoneNumber(phoneNumber, temporaryProof):
- XCTAssertEqual(temporaryProof, kTemporaryProof)
- XCTAssertEqual(phoneNumber, kPhoneNumber)
- case .verification: XCTFail("Should be phoneNumber case")
- }
- XCTAssertEqual(unarchivedCredential.provider, PhoneAuthProvider.id)
- }
- private func testRecaptchaFlowError(function: String, rceError: Error) async throws {
- initApp(function)
- let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
- // TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response
- // Mocking the output of verify() method
- let provider = PhoneAuthProvider.provider(auth: auth)
- let mockVerifier = FakeAuthRecaptchaVerifier(error: rceError)
- AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
- rpcIssuer.rceMode = "ENFORCE"
- do {
- let _ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
- toPhoneNumber: kTestPhoneNumber,
- retryOnInvalidAppCredential: false,
- uiDelegate: nil,
- recaptchaVerifier: mockVerifier
- )
- } catch {
- XCTAssertEqual((error as NSError).code, (rceError as NSError).code)
- }
- }
- private func internalFlowRetry(function: String, goodRetry: Bool = false) throws {
- let function = function
- initApp(function, useClientID: true, fakeToken: true)
- let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
- let provider = PhoneAuthProvider.provider(auth: auth)
- let expectation = self.expectation(description: function)
- // Fake push notification.
- auth.appCredentialManager?.fakeCredential = AuthAppCredential(
- receipt: kTestReceipt,
- secret: kTestSecret
- )
- // 1. Intercept, handle, and test three RPC calls.
- let verifyClientRequestExpectation = self.expectation(description: "verifyClientRequest")
- verifyClientRequestExpectation.expectedFulfillmentCount = 2
- rpcIssuer?.verifyClientRequester = { request in
- XCTAssertEqual(request.appToken, "21402324255E")
- XCTAssertFalse(request.isSandbox)
- verifyClientRequestExpectation.fulfill()
- do {
- // Response for the underlying VerifyClientRequest RPC call.
- try self.rpcIssuer?.respond(withJSON: [
- "receipt": self.kTestReceipt,
- "suggestedTimeout": self.kTestTimeout,
- ])
- } catch {
- XCTFail("Failure sending response: \(error)")
- }
- }
- let verifyRequesterExpectation = self.expectation(description: "verifyRequester")
- verifyRequesterExpectation.expectedFulfillmentCount = 2
- var visited = false
- rpcIssuer?.verifyRequester = { request in
- XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
- switch request.codeIdentity {
- case let .credential(credential):
- XCTAssertEqual(credential.receipt, self.kTestReceipt)
- XCTAssertEqual(credential.secret, self.kTestSecret)
- default:
- XCTFail("Should be credential")
- }
- verifyRequesterExpectation.fulfill()
- do {
- if visited == false || goodRetry == false {
- // First Response for the underlying SendVerificationCode RPC call.
- try self.rpcIssuer?.respond(serverErrorMessage: "INVALID_APP_CREDENTIAL")
- visited = true
- } else {
- // Second Response for the underlying SendVerificationCode RPC call.
- try self.rpcIssuer?
- .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
- }
- } catch {
- XCTFail("Failure sending response: \(error)")
- }
- }
- // Use fake authURLPresenter so we can test the parameters that get sent to it.
- PhoneAuthProviderTests.auth?.authURLPresenter =
- FakePresenter(
- urlString: PhoneAuthProviderTests.kFakeRedirectURLStringWithReCAPTCHAToken,
- clientID: PhoneAuthProviderTests.kFakeClientID,
- firebaseAppID: nil,
- errorTest: false,
- presenterError: nil
- )
- // 2. After setting up the fakes and parameters, call `verifyPhoneNumber`.
- provider
- .verifyPhoneNumber(kTestPhoneNumber, uiDelegate: nil) { verificationID, error in
- // 8. After the response triggers the callback in the FakePresenter, verify the callback.
- XCTAssertTrue(Thread.isMainThread)
- if goodRetry {
- XCTAssertNil(error)
- XCTAssertEqual(verificationID, self.kTestVerificationID)
- } else {
- XCTAssertNil(verificationID)
- XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.internalError.rawValue)
- }
- expectation.fulfill()
- }
- waitForExpectations(timeout: 5)
- }
- private func internalFlow(function: String,
- useClientID: Bool = false,
- reCAPTCHAfallback: Bool = false) throws {
- let function = function
- initApp(function, useClientID: useClientID, fakeToken: true)
- let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
- let provider = PhoneAuthProvider.provider(auth: auth)
- let expectation = self.expectation(description: function)
- // Fake push notification.
- auth.appCredentialManager?.fakeCredential = AuthAppCredential(
- receipt: kTestReceipt,
- secret: reCAPTCHAfallback ? nil : kTestSecret
- )
- // 1. Intercept, handle, and test three RPC calls.
- let verifyClientRequestExpectation = self.expectation(description: "verifyClientRequest")
- rpcIssuer?.verifyClientRequester = { request in
- XCTAssertEqual(request.appToken, "21402324255E")
- XCTAssertFalse(request.isSandbox)
- verifyClientRequestExpectation.fulfill()
- do {
- // Response for the underlying VerifyClientRequest RPC call.
- try self.rpcIssuer?.respond(withJSON: [
- "receipt": self.kTestReceipt,
- "suggestedTimeout": self.kTestTimeout,
- ])
- } catch {
- XCTFail("Failure sending response: \(error)")
- }
- }
- if reCAPTCHAfallback {
- let projectConfigExpectation = self.expectation(description: "projectConfiguration")
- rpcIssuer?.projectConfigRequester = { request in
- XCTAssertEqual(request.apiKey, PhoneAuthProviderTests.kFakeAPIKey)
- projectConfigExpectation.fulfill()
- kAuthGlobalWorkQueue.async {
- do {
- // Response for the underlying VerifyClientRequest RPC call.
- try self.rpcIssuer?.respond(
- withJSON: ["projectId": "kFakeProjectID",
- "authorizedDomains": [PhoneAuthProviderTests.kFakeAuthorizedDomain]]
- )
- } catch {
- XCTFail("Failure sending response: \(error)")
- }
- }
- }
- }
- let verifyRequesterExpectation = self.expectation(description: "verifyRequester")
- rpcIssuer?.verifyRequester = { request in
- XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
- if reCAPTCHAfallback {
- switch request.codeIdentity {
- case let .recaptcha(token):
- XCTAssertEqual(token, self.kFakeReCAPTCHAToken)
- default:
- XCTFail("Should be recaptcha")
- }
- } else {
- switch request.codeIdentity {
- case let .credential(credential):
- XCTAssertEqual(credential.receipt, self.kTestReceipt)
- XCTAssertEqual(credential.secret, self.kTestSecret)
- default:
- XCTFail("Should be credential")
- }
- }
- verifyRequesterExpectation.fulfill()
- do {
- // Response for the underlying SendVerificationCode RPC call.
- try self.rpcIssuer?
- .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
- } catch {
- XCTFail("Failure sending response: \(error)")
- }
- }
- // Use fake authURLPresenter so we can test the parameters that get sent to it.
- PhoneAuthProviderTests.auth?.authURLPresenter =
- FakePresenter(
- urlString: PhoneAuthProviderTests.kFakeRedirectURLStringWithReCAPTCHAToken,
- clientID: useClientID ? PhoneAuthProviderTests.kFakeClientID : nil,
- firebaseAppID: useClientID ? nil : PhoneAuthProviderTests.kFakeFirebaseAppID,
- errorTest: false,
- presenterError: nil
- )
- let uiDelegate = reCAPTCHAfallback ? FakeUIDelegate() : nil
- // 2. After setting up the fakes and parameters, call `verifyPhoneNumber`.
- provider
- .verifyPhoneNumber(kTestPhoneNumber, uiDelegate: uiDelegate) { verificationID, error in
- // 8. After the response triggers the callback in the FakePresenter, verify the callback.
- XCTAssertTrue(Thread.isMainThread)
- XCTAssertNil(error)
- XCTAssertEqual(verificationID, self.kTestVerificationID)
- expectation.fulfill()
- }
- waitForExpectations(timeout: 5)
- }
- /** @fn testVerifyClient
- @brief Tests verifying client before sending verification code.
- */
- private func internalTestVerify(errorString: String? = nil,
- errorURLString: String? = nil,
- errorCode: Int = 0,
- function: String,
- testMode: Bool = false,
- useClientID: Bool = false,
- bothClientAndAppID: Bool = false,
- reCAPTCHAfallback: Bool = false,
- forwardingNotification: Bool = true,
- presenterError: Error? = nil) async throws {
- initApp(function, useClientID: useClientID, bothClientAndAppID: bothClientAndAppID,
- testMode: testMode,
- forwardingNotification: forwardingNotification)
- let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
- let provider = PhoneAuthProvider.provider(auth: auth)
- var expectations: [XCTestExpectation] = []
- if !reCAPTCHAfallback {
- // Fake out appCredentialManager flow.
- auth.appCredentialManager?.credential = AuthAppCredential(receipt: kTestReceipt,
- secret: kTestSecret)
- } else {
- // 1. Intercept, handle, and test the projectConfiguration RPC calls.
- let projectConfigExpectation = expectation(description: "projectConfiguration")
- expectations.append(projectConfigExpectation)
- rpcIssuer?.projectConfigRequester = { request in
- XCTAssertEqual(request.apiKey, PhoneAuthProviderTests.kFakeAPIKey)
- projectConfigExpectation.fulfill()
- do {
- // Response for the underlying VerifyClientRequest RPC call.
- try self.rpcIssuer?.respond(
- withJSON: ["projectId": "kFakeProjectID",
- "authorizedDomains": [PhoneAuthProviderTests.kFakeAuthorizedDomain]]
- )
- } catch {
- XCTFail("Failure sending response: \(error)")
- }
- }
- }
- if reCAPTCHAfallback {
- // Use fake authURLPresenter so we can test the parameters that get sent to it.
- let urlString = errorURLString ??
- PhoneAuthProviderTests.kFakeRedirectURLStringWithReCAPTCHAToken
- let errorTest = errorURLString != nil
- PhoneAuthProviderTests.auth?.authURLPresenter =
- FakePresenter(
- urlString: urlString,
- clientID: useClientID ? PhoneAuthProviderTests.kFakeClientID : nil,
- firebaseAppID: useClientID ? nil : PhoneAuthProviderTests.kFakeFirebaseAppID,
- errorTest: errorTest,
- presenterError: presenterError
- )
- }
- if errorURLString == nil, presenterError == nil {
- let requestExpectation = expectation(description: "verifyRequester")
- expectations.append(requestExpectation)
- rpcIssuer?.verifyRequester = { request in
- XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
- switch request.codeIdentity {
- case let .credential(credential):
- XCTAssertFalse(reCAPTCHAfallback)
- XCTAssertEqual(credential.receipt, self.kTestReceipt)
- XCTAssertEqual(credential.secret, self.kTestSecret)
- case let .recaptcha(token):
- XCTAssertTrue(reCAPTCHAfallback)
- XCTAssertEqual(token, self.kFakeReCAPTCHAToken)
- case .empty:
- XCTAssertTrue(testMode)
- }
- requestExpectation.fulfill()
- do {
- // Response for the underlying SendVerificationCode RPC call.
- if let errorString {
- try self.rpcIssuer?.respond(serverErrorMessage: errorString)
- } else {
- try self.rpcIssuer?
- .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
- }
- } catch {
- XCTFail("Failure sending response: \(error)")
- }
- }
- }
- let uiDelegate = reCAPTCHAfallback ? FakeUIDelegate() : nil
- // 2. After setting up the parameters, call `verifyPhoneNumber`.
- do {
- // Call the async function to verify the phone number
- let verificationID = try await provider.verifyPhoneNumber(
- kTestPhoneNumber,
- uiDelegate: uiDelegate
- )
- // Assert that the verificationID matches the expected value
- XCTAssertEqual(verificationID, kTestVerificationID)
- } catch {
- // If an error occurs, assert that verificationID is nil and the error code matches the
- // expected value
- XCTAssertEqual((error as NSError).code, errorCode)
- }
- await fulfillment(of: expectations, timeout: 5.0)
- }
- private func initApp(_ functionName: String,
- useClientID: Bool = false,
- bothClientAndAppID: Bool = false,
- testMode: Bool = false,
- forwardingNotification: Bool = true,
- fakeToken: Bool = false) {
- let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000",
- gcmSenderID: "00000000000000000-00000000000-000000000")
- options.apiKey = PhoneAuthProviderTests.kFakeAPIKey
- options.projectID = "myProjectID"
- if useClientID {
- options.clientID = PhoneAuthProviderTests.kFakeClientID
- }
- if !useClientID || bothClientAndAppID {
- // Use the appID.
- options.googleAppID = PhoneAuthProviderTests.kFakeFirebaseAppID
- }
- let scheme = useClientID ? PhoneAuthProviderTests.kFakeReverseClientID :
- PhoneAuthProviderTests.kFakeEncodedFirebaseAppID
- let strippedName = functionName.replacingOccurrences(of: "(", with: "")
- .replacingOccurrences(of: ")", with: "")
- FirebaseApp.configure(name: strippedName, options: options)
- let auth = Auth.auth(app: FirebaseApp.app(name: strippedName)!)
- kAuthGlobalWorkQueue.sync {
- // Wait for Auth protectedDataInitialization to finish.
- PhoneAuthProviderTests.auth = auth
- if testMode {
- // Disable app verification.
- let settings = AuthSettings()
- settings.appVerificationDisabledForTesting = true
- auth.settings = settings
- }
- auth.notificationManager?.immediateCallbackForTestFaking = { forwardingNotification }
- auth.mainBundleUrlTypes = [["CFBundleURLSchemes": [scheme]]]
- if fakeToken {
- guard let data = "!@#$%^".data(using: .utf8) else {
- XCTFail("Failed to encode data for fake token")
- return
- }
- auth.tokenManager?.tokenStore = AuthAPNSToken(withData: data, type: .prod)
- } else {
- // Skip APNS token fetching.
- auth.tokenManager = FakeTokenManager(withApplication: UIApplication.shared)
- }
- }
- }
- class FakeAuthRecaptchaVerifier: AuthRecaptchaVerifier {
- var captchaResponse: String
- var error: Error?
- init(captchaResponse: String? = nil, error: Error? = nil) {
- self.captchaResponse = captchaResponse ?? "NO_RECAPTCHA"
- self.error = error
- super.init()
- }
- override func verify(forceRefresh: Bool, action: AuthRecaptchaAction) async throws -> String {
- if let error = error {
- throw error
- }
- return captchaResponse
- }
- }
- class FakeTokenManager: AuthAPNSTokenManager {
- override func getTokenInternal(callback: @escaping (Result<AuthAPNSToken, Error>) -> Void) {
- let error = NSError(domain: "dummy domain", code: AuthErrorCode.missingAppToken.rawValue)
- callback(.failure(error))
- }
- }
- class FakePresenter: NSObject, AuthWebViewControllerDelegate {
- func webViewController(_ webViewController: AuthWebViewController,
- canHandle URL: URL) -> Bool {
- XCTFail("Do not call")
- return false
- }
- func webViewControllerDidCancel(_ webViewController: AuthWebViewController) {
- XCTFail("Do not call")
- }
- func webViewController(_ webViewController: AuthWebViewController,
- didFailWithError error: Error) {
- XCTFail("Do not call")
- }
- func present(_ presentURL: URL,
- uiDelegate UIDelegate: AuthUIDelegate?,
- callbackMatcher: @escaping (URL?) -> Bool,
- completion: @escaping (URL?, Error?) -> Void) {
- // 5. Verify flow triggers present in the FakePresenter class with the right parameters.
- XCTAssertEqual(presentURL.scheme, "https")
- XCTAssertEqual(presentURL.host, kFakeAuthorizedDomain)
- XCTAssertEqual(presentURL.path, "/__/auth/handler")
- let actualURLComponents = URLComponents(url: presentURL, resolvingAgainstBaseURL: false)
- guard let _ = actualURLComponents?.queryItems else {
- XCTFail("Failed to get queryItems")
- return
- }
- let params = AuthWebUtils.dictionary(withHttpArgumentsString: presentURL.query)
- XCTAssertEqual(params["ibi"], Bundle.main.bundleIdentifier)
- XCTAssertEqual(params["apiKey"], PhoneAuthProviderTests.kFakeAPIKey)
- XCTAssertEqual(params["authType"], "verifyApp")
- XCTAssertNotNil(params["v"])
- if OAuthProviderTests.testTenantID {
- XCTAssertEqual(params["tid"], OAuthProviderTests.kFakeTenantID)
- } else {
- XCTAssertNil(params["tid"])
- }
- let appCheckToken = presentURL.fragment
- let verifyAppCheckToken = OAuthProviderTests.testAppCheck ? "fac=fakeAppCheckToken" : nil
- XCTAssertEqual(appCheckToken, verifyAppCheckToken)
- var redirectURL = ""
- if let clientID {
- XCTAssertEqual(params["clientId"], clientID)
- redirectURL = "\(kFakeReverseClientID)\(urlString)"
- }
- if let firebaseAppID {
- XCTAssertEqual(params["appId"], firebaseAppID)
- redirectURL = "\(kFakeEncodedFirebaseAppID)\(urlString)"
- }
- // 6. Test callbackMatcher
- // Verify that the URL is rejected by the callback matcher without the event ID.
- XCTAssertFalse(callbackMatcher(URL(string: "\(redirectURL)")))
- // Verify that the URL is accepted by the callback matcher with the matching event ID.
- guard let eventID = params["eventId"] else {
- XCTFail("Failed to get eventID")
- return
- }
- let redirectWithEventID = "\(redirectURL)%26eventId%3D\(eventID)"
- let originalComponents = URLComponents(string: redirectWithEventID)!
- XCTAssertEqual(callbackMatcher(originalComponents.url), !errorTest)
- var components = originalComponents
- components.query = "https"
- XCTAssertFalse(callbackMatcher(components.url))
- components = originalComponents
- components.host = "badhost"
- XCTAssertFalse(callbackMatcher(components.url))
- components = originalComponents
- components.path = "badpath"
- XCTAssertFalse(callbackMatcher(components.url))
- components = originalComponents
- components.query = "badquery"
- XCTAssertFalse(callbackMatcher(components.url))
- // 7. Do the callback to the original call.
- kAuthGlobalWorkQueue.async {
- if let presenterError = self.presenterError {
- completion(nil, presenterError)
- } else {
- completion(URL(string: "\(kFakeEncodedFirebaseAppID)\(self.urlString)") ?? nil, nil)
- }
- }
- }
- let urlString: String
- let clientID: String?
- let firebaseAppID: String?
- let errorTest: Bool
- let presenterError: Error?
- init(urlString: String, clientID: String?, firebaseAppID: String?, errorTest: Bool,
- presenterError: Error?) {
- self.urlString = urlString
- self.clientID = clientID
- self.firebaseAppID = firebaseAppID
- self.errorTest = errorTest
- self.presenterError = presenterError
- }
- }
- private class FakeUIDelegate: NSObject, AuthUIDelegate {
- func present(_ viewControllerToPresent: UIViewController, animated flag: Bool,
- completion: (() -> Void)? = nil) {
- guard let safariController = viewControllerToPresent as? SFSafariViewController,
- let delegate = safariController.delegate as? AuthURLPresenter,
- let uiDelegate = delegate.uiDelegate as? FakeUIDelegate else {
- XCTFail("Failed to get presentURL from controller")
- return
- }
- XCTAssertEqual(self, uiDelegate)
- }
- func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
- XCTFail("Implement me")
- }
- }
- private static let kFakeRedirectURLStringInvalidClientID =
- "//firebaseauth/" +
- "link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
- "lback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Finvalid-oauth-client-id%2522%252" +
- "C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%2520either%252" +
- "0invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%26" +
- "authType%3DverifyApp"
- private static let kFakeRedirectURLStringWebNetworkRequestFailed =
- "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fc" +
- "allback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Fnetwork-request-failed%2522%" +
- "252C%2522message%2522%253A%2522The%2520network%2520request%2520failed%2520.%2522%257D%" +
- "26authType%3DverifyApp"
- private static let kFakeRedirectURLStringWebInternalError =
- "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
- "lback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Finternal-error%2522%252C%" +
- "2522message%2522%253A%2522Internal%2520error%2520.%2522%257D%26authType%3DverifyApp"
- private static let kFakeRedirectURLStringUnknownError =
- "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
- "lback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Funknown-error-id%2522%252" +
- "C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%2520either%252" +
- "0invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%26" +
- "authType%3DverifyApp"
- private static let kFakeRedirectURLStringUnstructuredError =
- "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
- "lback%3FfirebaseError%3D%257B%2522unstructuredcode%2522%253A%2522auth%252Funknown-error-id%" +
- "2522%252" +
- "C%2522unstructuredmessage%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%" +
- "2520either%252" +
- "0invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%" +
- "26authType%3DverifyApp"
- private static let kFakeRedirectURLStringWithReCAPTCHAToken =
- "://firebaseauth/" +
- "link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcallback%3FauthType%" +
- "3DverifyApp%26recaptchaToken%3DfakeReCAPTCHAToken"
- }
- #endif
|