Files
firebase-ios-sdk/FirebaseAuth/Tests/Unit/PhoneAuthProviderTests.swift
T

866 lines
36 KiB
Swift

// 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"
static var auth: Auth?
/** @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 @c verifyPhoneNumber:completion: because an empty phone
number was provided.
*/
func testVerifyEmptyPhoneNumber() throws {
initApp(#function)
let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
let provider = PhoneAuthProvider.provider(auth: auth)
let expectation = self.expectation(description: #function)
// Empty phone number is checked on the client side so no backend RPC is faked.
provider.verifyPhoneNumber("", uiDelegate: nil) { verificationID, error in
XCTAssertNotNil(error)
XCTAssertNil(verificationID)
XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.missingPhoneNumber.rawValue)
expectation.fulfill()
}
waitForExpectations(timeout: 5)
}
/** @fn testVerifyInvalidPhoneNumber
@brief Tests a failed invocation @c verifyPhoneNumber:completion: because an invalid phone
number was provided.
*/
func testVerifyInvalidPhoneNumber() throws {
try internalTestVerify(errorString: "INVALID_PHONE_NUMBER",
errorCode: AuthErrorCode.invalidPhoneNumber.rawValue,
function: #function)
}
/** @fn testVerifyPhoneNumber
@brief Tests a successful invocation of @c verifyPhoneNumber:completion:.
*/
func testVerifyPhoneNumber() throws {
try internalTestVerify(function: #function)
}
/** @fn testVerifyPhoneNumberInTestMode
@brief Tests a successful invocation of @c verifyPhoneNumber:completion: when app verification
is disabled.
*/
func testVerifyPhoneNumberInTestMode() throws {
try internalTestVerify(function: #function, testMode: true)
}
/** @fn testVerifyPhoneNumberInTestModeFailure
@brief Tests a failed invocation of @c verifyPhoneNumber:completion: when app verification
is disabled.
*/
func testVerifyPhoneNumberInTestModeFailure() throws {
try 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() throws {
try 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() throws {
try internalTestVerify(function: #function, useClientID: true,
bothClientAndAppID: true, reCAPTCHAfallback: true)
}
/** @fn testVerifyPhoneNumberUIDelegateClientIdFlow
@brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion:.
*/
func testVerifyPhoneNumberUIDelegateClientIdFlow() throws {
try 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() throws {
try 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() throws {
try 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() throws {
try 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() throws {
try 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() throws {
try 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() 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() throws {
try 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() throws {
try 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 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) throws {
initApp(function, useClientID: useClientID, bothClientAndAppID: bothClientAndAppID,
testMode: testMode,
forwardingNotification: forwardingNotification)
let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
let provider = PhoneAuthProvider.provider(auth: auth)
let expectation = self.expectation(description: function)
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 = self.expectation(description: "projectConfiguration")
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 errorURLString == nil, presenterError == nil {
let requestExpectation = self.expectation(description: "verifyRequester")
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)")
}
}
}
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
)
}
let uiDelegate = reCAPTCHAfallback ? FakeUIDelegate() : nil
// 2. After setting up the 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)
if errorCode != 0 {
XCTAssertNil(verificationID)
XCTAssertEqual((error as? NSError)?.code, errorCode)
} else {
XCTAssertNil(error)
XCTAssertEqual(verificationID, self.kTestVerificationID)
}
expectation.fulfill()
}
waitForExpectations(timeout: 5)
}
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(app: FirebaseApp.app(name: strippedName)!, backend: authBackend)
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 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