Files
firebase-ios-sdk/FirebaseAuth/Tests/Unit/UserTests.swift
Paul Beusterienandgemini-code-assist[bot] aa10b07f9f fix(auth): check the session before signing out on a rejected ID token (#16857)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-10-14 10:45:22 -07:00

2253 lines
97 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.
import Foundation
import XCTest
@testable import FirebaseAuth
import FirebaseCore
class UserTests: RPCBaseTests {
static let kFakeAPIKey = "FAKE_API_KEY"
let kFacebookAccessToken = "FACEBOOK_ACCESS_TOKEN"
let kFacebookID = "FACEBOOK_ID"
let kFacebookEmail = "user@facebook.com"
let kFacebookDisplayName = "Facebook Doe"
let kFacebookIDToken: String? = nil // Facebook id Token is always nil.
let kNewEmail = "newuser@company.com"
let kNewPassword = "newpassword"
let kNewDisplayName = "New User Doe"
let kVerificationCode = "12345678"
let kVerificationID = "55432"
let kPhoneNumber = "555-1234"
let kRefreshedAccessToken = "REFRESHED_ACCESS_TOKEN"
var auth: Auth?
override func setUp() {
super.setUp()
let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000",
gcmSenderID: "00000000000000000-00000000000-000000000")
options.apiKey = Self.kFakeAPIKey
options.projectID = "myUserProjectID"
FirebaseApp.configure(name: "test-UserTests", options: options)
#if (os(macOS) && !FIREBASE_AUTH_TESTING_USE_MACOS_KEYCHAIN) || SWIFT_PACKAGE
let keychainStorageProvider = FakeAuthKeychainStorage()
#else
let keychainStorageProvider = AuthKeychainStorageReal.shared
#endif // (os(macOS) && !FIREBASE_AUTH_TESTING_USE_MACOS_KEYCHAIN) || SWIFT_PACKAGE
auth = Auth(
app: FirebaseApp.app(name: "test-UserTests")!,
keychainStorageProvider: keychainStorageProvider,
backend: authBackend
)
}
override func tearDown() {
// Verifies that no tasks are left suspended on the AuthSerialTaskQueue.
try? auth?.signOut()
auth = nil
FirebaseApp.resetApps()
super.tearDown()
}
/** @fn testUserWithProviderUserInfoMissingProviderIDIgnoresEntry
@brief Tests that a getAccountInfo provider entry lacking a `providerId`
is skipped rather than crashing @c User construction.
*/
func testUserWithProviderUserInfoMissingProviderIDIgnoresEntry() throws {
let providerUserInfos: [[String: String]] = [
[
"providerId": GoogleAuthProvider.id,
"federatedId": "GOOGLE_ID",
"email": kEmail,
],
[
// A malformed backend response entry with no `providerId`.
"federatedId": "NO_PROVIDER_ID",
"email": kEmail,
],
]
rpcIssuer?.fakeGetAccountProviderJSON = [[
"providerUserInfo": providerUserInfos,
"localId": kLocalID,
"email": kEmail,
"emailVerified": true,
]]
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
// The entry without a providerId is dropped; the valid entry remains.
XCTAssertEqual(user.providerData.count, 1)
XCTAssertEqual(user.providerData.first?.providerID, GoogleAuthProvider.id)
expectation.fulfill()
}
waitForExpectations(timeout: 5)
}
/** @fn testUserPropertiesAndNSSecureCoding
@brief Tests properties of the @c User instance before and after being
serialized/deserialized.
*/
func testUserPropertiesAndNSSecureCoding() throws {
let kProviderUserInfoKey = "providerUserInfo"
let kPhotoUrlKey = "photoUrl"
let kProviderIDkey = "providerId"
let kDisplayNameKey = "displayName"
let kFederatedIDKey = "federatedId"
let kEmailKey = "email"
let kPasswordHashKey = "passwordHash"
let kTestPasswordHash = "testPasswordHash"
let kEmailVerifiedKey = "emailVerified"
let kLocalIDKey = "localId"
let kGoogleID = "GOOGLE_ID"
let kGoogleDisplayName = "Google Doe"
let kGoogleEmail = "user@gmail.com"
let kGooglePhotoURL = "https://googleusercontents.com/user/profile"
let kFacebookID = "FACEBOOK_ID"
let kFacebookEmail = "user@facebook.com"
let kEnrollmentID = "fakeEnrollment"
let kPhoneInfo = "+15555555555"
let kEnrolledAt = "2022-08-01T18:31:15.426458Z"
let kEnrolledAtMatch = "2022-08-01 18:31:15 +0000"
let kTwitterID = "TwitterID"
let kGitHubID = "GitHubID"
let kGameCenterID = "GameCenterID"
var providerUserInfos = [[
kProviderIDkey: EmailAuthProvider.id,
kFederatedIDKey: kEmail,
kEmailKey: kEmail,
],
[
kProviderIDkey: GoogleAuthProvider.id,
kDisplayNameKey: kGoogleDisplayName,
kPhotoUrlKey: kGooglePhotoURL,
kFederatedIDKey: kGoogleID,
kEmailKey: kGoogleEmail,
],
[
kProviderIDkey: FacebookAuthProvider.id,
kFederatedIDKey: kFacebookID,
kEmailKey: kFacebookEmail,
],
[
kProviderIDkey: GitHubAuthProvider.id,
kFederatedIDKey: kGitHubID,
kEmailKey: kGoogleEmail,
],
[
kProviderIDkey: TwitterAuthProvider.id,
kFederatedIDKey: kTwitterID,
kEmailKey: kFacebookEmail,
]]
#if !os(watchOS)
providerUserInfos.append([
kProviderIDkey: GameCenterAuthProvider.id,
kFederatedIDKey: kGameCenterID,
kEmailKey: kFacebookEmail,
])
#endif
#if os(iOS)
providerUserInfos.append([
kProviderIDkey: PhoneAuthProvider.id,
kFederatedIDKey: kPhoneNumber,
"phoneNumber": kPhoneNumber,
])
#endif
var mfaInfo: [[AnyHashable: AnyHashable]] = []
#if os(iOS)
mfaInfo.append([
"phoneInfo": kPhoneInfo,
"mfaEnrollmentId": kEnrollmentID,
"displayName": kDisplayName,
"enrolledAt": kEnrolledAt,
])
#endif
#if os(iOS) || os(macOS)
mfaInfo.append([
// In practice, this will be an empty dictionary.
"totpInfo": [AnyHashable: AnyHashable](),
"mfaEnrollmentId": kEnrollmentID,
"displayName": kDisplayName,
"enrolledAt": kEnrolledAt,
])
#endif
rpcIssuer?.fakeGetAccountProviderJSON = [[
kProviderUserInfoKey: providerUserInfos,
kLocalIDKey: kLocalID,
kDisplayNameKey: kDisplayName,
kEmailKey: kEmail,
kPhotoUrlKey: kTestPhotoURL,
kEmailVerifiedKey: true,
kPasswordHashKey: kTestPasswordHash,
"phoneNumber": kPhoneNumber,
"createdAt": String(Int(kCreationDateTimeIntervalInSeconds) * 1000), // to nanoseconds
"lastLoginAt": String(Int(kLastSignInDateTimeIntervalInSeconds) * 1000),
"mfaInfo": mfaInfo,
]]
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
do {
XCTAssertEqual(user.providerID, "Firebase")
XCTAssertEqual(user.uid, self.kLocalID)
XCTAssertEqual(user.displayName, self.kDisplayName)
XCTAssertEqual(user.photoURL, URL(string: self.kTestPhotoURL))
XCTAssertEqual(user.email, self.kEmail)
XCTAssertEqual(user.metadata.creationDate, Date(timeIntervalSince1970:
self.kCreationDateTimeIntervalInSeconds))
XCTAssertEqual(user.metadata.lastSignInDate,
Date(timeIntervalSince1970: self.kLastSignInDateTimeIntervalInSeconds))
// Verify FIRUser properties besides providerData contents.
XCTAssertFalse(user.isAnonymous)
XCTAssertTrue(user.isEmailVerified)
XCTAssertEqual(user.refreshToken, self.kRefreshToken)
XCTAssertEqual(user.providerData.count, providerUserInfos.count)
let providerMap = user.providerData.reduce(into: [String: UserInfo]()) {
$0[$1.providerID] = $1
}
// Verify FIRUserInfo properties from email/password.
let passwordUserInfo = try XCTUnwrap(providerMap[EmailAuthProvider.id])
XCTAssertEqual(passwordUserInfo.uid, self.kEmail)
XCTAssertNil(passwordUserInfo.displayName)
XCTAssertNil(passwordUserInfo.photoURL)
XCTAssertEqual(passwordUserInfo.email, self.kEmail)
// Verify FIRUserInfo properties from the Google auth provider.
let googleUserInfo = try XCTUnwrap(providerMap[GoogleAuthProvider.id])
XCTAssertEqual(googleUserInfo.uid, kGoogleID)
XCTAssertEqual(googleUserInfo.displayName, kGoogleDisplayName)
XCTAssertEqual(googleUserInfo.photoURL, URL(string: kGooglePhotoURL))
XCTAssertEqual(googleUserInfo.email, kGoogleEmail)
// Verify FIRUserInfo properties from the Facebook auth provider.
let facebookUserInfo = try XCTUnwrap(providerMap[FacebookAuthProvider.id])
XCTAssertEqual(facebookUserInfo.uid, kFacebookID)
XCTAssertNil(facebookUserInfo.displayName)
XCTAssertNil(facebookUserInfo.photoURL)
XCTAssertEqual(facebookUserInfo.email, kFacebookEmail)
// Verify FIRUserInfo properties from the GitHub auth provider.
let gitHubUserInfo = try XCTUnwrap(providerMap[GitHubAuthProvider.id])
XCTAssertEqual(gitHubUserInfo.uid, kGitHubID)
XCTAssertNil(gitHubUserInfo.displayName)
XCTAssertNil(gitHubUserInfo.photoURL)
XCTAssertEqual(gitHubUserInfo.email, kGoogleEmail)
// Verify FIRUserInfo properties from the Twitter auth provider.
let twitterUserInfo = try XCTUnwrap(providerMap[TwitterAuthProvider.id])
XCTAssertEqual(twitterUserInfo.uid, kTwitterID)
XCTAssertNil(twitterUserInfo.displayName)
XCTAssertNil(twitterUserInfo.photoURL)
XCTAssertEqual(twitterUserInfo.email, kFacebookEmail)
#if os(iOS)
// Verify UserInfo properties from the phone auth provider.
let phoneUserInfo = try XCTUnwrap(providerMap[PhoneAuthProvider.id])
XCTAssertEqual(phoneUserInfo.phoneNumber, self.kPhoneNumber)
#endif
#if !os(watchOS)
// Verify FIRUserInfo properties from the Game Center auth provider.
let gameCenterUserInfo = try XCTUnwrap(providerMap[GameCenterAuthProvider.id])
XCTAssertEqual(gameCenterUserInfo.uid, kGameCenterID)
XCTAssertNil(gameCenterUserInfo.displayName)
XCTAssertNil(gameCenterUserInfo.photoURL)
XCTAssertEqual(gameCenterUserInfo.email, kFacebookEmail)
#endif
// Test NSSecureCoding
XCTAssertTrue(User.supportsSecureCoding)
let data = try NSKeyedArchiver.archivedData(
withRootObject: user,
requiringSecureCoding: true
)
var encodedClasses = [User.self, NSDictionary.self, NSURL.self, SecureTokenService.self,
UserInfoImpl.self, NSDate.self, UserMetadata.self, NSString.self,
NSArray.self]
#if os(iOS) || os(macOS)
encodedClasses.append(MultiFactor.self)
encodedClasses.append(TOTPMultiFactorInfo.self)
#if os(iOS)
encodedClasses.append(PhoneMultiFactorInfo.self)
#endif
#endif
let unarchivedUser = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
ofClasses: encodedClasses, from: data
)
as? User)
// Verify NSSecureCoding for FIRUser
XCTAssertEqual(unarchivedUser.providerID, user.providerID)
XCTAssertEqual(unarchivedUser.uid, user.uid)
XCTAssertEqual(unarchivedUser.email, user.email)
XCTAssertEqual(unarchivedUser.photoURL, user.photoURL)
XCTAssertEqual(unarchivedUser.displayName, user.displayName)
// Verify NSSecureCoding properties besides providerData contents.
XCTAssertEqual(unarchivedUser.isAnonymous, user.isAnonymous)
XCTAssertEqual(unarchivedUser.isEmailVerified, user.isEmailVerified)
XCTAssertEqual(unarchivedUser.refreshToken, user.refreshToken)
XCTAssertEqual(unarchivedUser.metadata.creationDate, user.metadata.creationDate)
XCTAssertEqual(unarchivedUser.metadata.lastSignInDate, user.metadata.lastSignInDate)
XCTAssertEqual(unarchivedUser.providerData.count, user.providerData.count)
let unarchivedProviderMap = unarchivedUser.providerData.reduce(into: [String: UserInfo]()) {
$0[$1.providerID] = $1
}
// Verify NSSecureCoding properties for AuthDataResult
let kFakeProfile = ["email": "user@mail.com", "given_name": "User", "family_name": "Doe"]
let kUserName = "User Doe"
let kProviderID = "PROVIDER_ID"
let userInfo = AdditionalUserInfo(providerID: kProviderID,
profile: kFakeProfile,
username: kUserName,
isNewUser: true)
let authDataResult = AuthDataResult(withUser: user, additionalUserInfo: userInfo)
XCTAssertTrue(AuthDataResult.supportsSecureCoding)
let authDataResultData = try NSKeyedArchiver.archivedData(
withRootObject: authDataResult,
requiringSecureCoding: true
)
encodedClasses.append(AuthDataResult.self)
encodedClasses.append(AdditionalUserInfo.self)
let unarchivedDataResult = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
ofClasses: encodedClasses, from: authDataResultData
) as? AuthDataResult)
XCTAssertEqual(unarchivedDataResult.user.providerID, user.providerID)
XCTAssertEqual(unarchivedDataResult.user.uid, user.uid)
XCTAssertEqual(unarchivedDataResult.user.email, user.email)
XCTAssertEqual(unarchivedDataResult.user.photoURL, user.photoURL)
XCTAssertEqual(unarchivedDataResult.user.displayName, user.displayName)
XCTAssertEqual(unarchivedDataResult.additionalUserInfo?.providerID, kProviderID)
XCTAssertEqual(unarchivedDataResult.additionalUserInfo?.profile as? [String: String],
kFakeProfile)
XCTAssertEqual(unarchivedDataResult.additionalUserInfo?.username, kUserName)
// Verify NSSecureCoding properties from email/password.
let unarchivedPasswordUserInfo = try XCTUnwrap(unarchivedProviderMap[EmailAuthProvider.id])
XCTAssertEqual(unarchivedPasswordUserInfo.uid, passwordUserInfo.uid)
XCTAssertEqual(unarchivedPasswordUserInfo.displayName, passwordUserInfo.displayName)
XCTAssertEqual(unarchivedPasswordUserInfo.photoURL, passwordUserInfo.photoURL)
XCTAssertEqual(unarchivedPasswordUserInfo.email, passwordUserInfo.email)
// Verify NSSecureCoding properties from the Google auth provider.
let unarchivedGoogleUserInfo = try XCTUnwrap(unarchivedProviderMap[GoogleAuthProvider.id])
XCTAssertEqual(unarchivedGoogleUserInfo.uid, googleUserInfo.uid)
XCTAssertEqual(unarchivedGoogleUserInfo.displayName, googleUserInfo.displayName)
XCTAssertEqual(unarchivedGoogleUserInfo.photoURL, googleUserInfo.photoURL)
XCTAssertEqual(unarchivedGoogleUserInfo.email, googleUserInfo.email)
// Verify NSSecureCoding properties from the Facebook auth provider.
let unarchivedFacebookUserInfo =
try XCTUnwrap(unarchivedProviderMap[FacebookAuthProvider.id])
XCTAssertEqual(unarchivedFacebookUserInfo.uid, facebookUserInfo.uid)
XCTAssertEqual(unarchivedFacebookUserInfo.displayName, facebookUserInfo.displayName)
XCTAssertEqual(unarchivedFacebookUserInfo.photoURL, facebookUserInfo.photoURL)
XCTAssertEqual(unarchivedFacebookUserInfo.email, facebookUserInfo.email)
#if !os(watchOS)
// Verify NSSecureCoding properties from the GameCenter auth provider.
let unarchivedGameCenterUserInfo =
try XCTUnwrap(unarchivedProviderMap[GameCenterAuthProvider.id])
XCTAssertEqual(unarchivedGameCenterUserInfo.uid, gameCenterUserInfo.uid)
XCTAssertEqual(unarchivedGameCenterUserInfo.displayName, gameCenterUserInfo.displayName)
XCTAssertEqual(unarchivedGameCenterUserInfo.photoURL, gameCenterUserInfo.photoURL)
XCTAssertEqual(unarchivedGameCenterUserInfo.email, gameCenterUserInfo.email)
#endif
// Verify NSSecureCoding properties from the GitHub auth provider.
let unarchivedGitHubUserInfo =
try XCTUnwrap(unarchivedProviderMap[GitHubAuthProvider.id])
XCTAssertEqual(unarchivedGitHubUserInfo.uid, gitHubUserInfo.uid)
XCTAssertEqual(unarchivedGitHubUserInfo.displayName, gitHubUserInfo.displayName)
XCTAssertEqual(unarchivedGitHubUserInfo.photoURL, gitHubUserInfo.photoURL)
XCTAssertEqual(unarchivedGitHubUserInfo.email, gitHubUserInfo.email)
// Verify NSSecureCoding properties from the Twitter auth provider.
let unarchivedTwitterUserInfo =
try XCTUnwrap(unarchivedProviderMap[TwitterAuthProvider.id])
XCTAssertEqual(unarchivedTwitterUserInfo.uid, twitterUserInfo.uid)
XCTAssertEqual(unarchivedTwitterUserInfo.displayName, twitterUserInfo.displayName)
XCTAssertEqual(unarchivedTwitterUserInfo.photoURL, twitterUserInfo.photoURL)
XCTAssertEqual(unarchivedTwitterUserInfo.email, twitterUserInfo.email)
#if os(iOS)
// Verify NSSecureCoding properties from the phone auth provider.
let unarchivedPhoneUserInfo = try XCTUnwrap(unarchivedProviderMap[PhoneAuthProvider.id])
XCTAssertEqual(unarchivedPhoneUserInfo.phoneNumber, phoneUserInfo.phoneNumber)
// Verify MultiFactorInfo properties.
let enrolledFactors = try XCTUnwrap(user.multiFactor.enrolledFactors)
XCTAssertEqual(enrolledFactors.count, 2)
XCTAssertEqual(enrolledFactors[0].factorID, PhoneMultiFactorInfo.PhoneMultiFactorID)
XCTAssertEqual(enrolledFactors[1].factorID, PhoneMultiFactorInfo.TOTPMultiFactorID)
for enrolledFactor in enrolledFactors {
XCTAssertEqual(enrolledFactor.uid, kEnrollmentID)
XCTAssertEqual(enrolledFactor.displayName, self.kDisplayName)
let date = try XCTUnwrap(enrolledFactor.enrollmentDate)
XCTAssertEqual("\(date)", kEnrolledAtMatch)
}
#endif
#if os(macOS)
// Verify TOTP MultiFactorInfo properties.
let enrolledFactors = try XCTUnwrap(user.multiFactor.enrolledFactors)
XCTAssertEqual(enrolledFactors.count, 1)
XCTAssertEqual(enrolledFactors[0].factorID, PhoneMultiFactorInfo.TOTPMultiFactorID)
XCTAssertEqual(enrolledFactors[0].uid, kEnrollmentID)
XCTAssertEqual(enrolledFactors[0].displayName, self.kDisplayName)
let date = try XCTUnwrap(enrolledFactors[0].enrollmentDate)
XCTAssertEqual("\(date)", kEnrolledAtMatch)
#endif
} catch {
XCTFail("Caught an error in \(#function): \(error)")
}
expectation.fulfill()
}
waitForExpectations(timeout: 5)
}
/** @fn testUpdateEmailSuccess
@brief Tests the flow of a successful @c updateEmail:completion: call.
*/
func testUpdateEmailSuccess() {
setFakeGetAccountProvider(withPasswordHash: kFakePassword)
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
self.changeUserEmail(user: user, changeEmail: true, expectation: expectation)
}
waitForExpectations(timeout: 5)
}
/** @fn testUpdateEmailWithAuthLinkAccountSuccess
@brief Tests a successful @c updateEmail:completion: call updates provider info.
*/
func testUpdateEmailWithAuthLinkAccountSuccess() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUserLink { user in
self.changeUserEmail(user: user, expectation: expectation)
}
waitForExpectations(timeout: 5)
}
/** @fn testUpdateEmailFailure
@brief Tests the flow of a failed @c updateEmail:completion: call.
*/
func testUpdateEmailFailure() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(serverErrorMessage: "INVALID_EMAIL")
}
user.updateEmail(to: self.kNewEmail) { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.invalidEmail.rawValue)
// Email should not have changed on the client side.
XCTAssertEqual(user.email, self.kEmail)
// User is still signed in.
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testUpdateEmailAutoSignOut
@brief Tests the flow of a failed @c updateEmail:completion: call that automatically signs out
because the session has ended.
*/
func testUpdateEmailAutoSignOut() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
// The refresh that checks the session fails too.
self.rpcIssuer.secureTokenErrorString = "TOKEN_EXPIRED"
return try self.rpcIssuer.respond(serverErrorMessage: "INVALID_ID_TOKEN")
}
user.updateEmail(to: self.kNewEmail) { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.invalidUserToken.rawValue)
// Email should not have changed on the client side.
XCTAssertEqual(user.email, self.kEmail)
// User is no longer signed in..
XCTAssertNil(self.auth?.currentUser)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testUpdateEmailInvalidIDTokenWithValidSessionStaysSignedIn
@brief Tests that a failed @c updateEmail:completion: call with @c INVALID_ID_TOKEN refreshes
the token, and keeps the user signed in when the refresh succeeds.
*/
func testUpdateEmailInvalidIDTokenWithValidSessionStaysSignedIn() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
self.rpcIssuer.respondBlock = {
self.setFakeSecureTokenService(fakeAccessToken: self.kRefreshedAccessToken)
return try self.rpcIssuer.respond(serverErrorMessage: "INVALID_ID_TOKEN")
}
user.updateEmail(to: self.kNewEmail) { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.invalidUserToken.rawValue)
XCTAssertEqual(user.email, self.kEmail)
// The token was refreshed and the user is still signed in.
XCTAssertEqual(user.rawAccessToken(), self.kRefreshedAccessToken)
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
waitForExpectations(timeout: 5)
}
/** @fn testUpdateEmailInvalidIDTokenWhenUserNoLongerCurrentSkipsRefresh
@brief Tests that when @c updateEmail:completion: fails with @c INVALID_ID_TOKEN on a stale
@c User instance after the current user has changed, no token refresh is sent and the
current user stays signed in.
*/
func testUpdateEmailInvalidIDTokenWhenUserNoLongerCurrentSkipsRefresh() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { staleUser in
self.signInWithEmailPasswordReturnFakeUser { currentUser in
XCTAssertNotIdentical(staleUser, currentUser)
XCTAssertEqual(staleUser.uid, currentUser.uid)
self.rpcIssuer.respondBlock = {
self.rpcIssuer.secureTokenErrorString = "TOKEN_EXPIRED"
return try self.rpcIssuer.respond(serverErrorMessage: "INVALID_ID_TOKEN")
}
staleUser.updateEmail(to: self.kNewEmail) { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.invalidUserToken.rawValue)
// No refresh was sent and the new current user is still signed in.
XCTAssertTrue(self.rpcIssuer.request is SetAccountInfoRequest)
XCTAssertIdentical(self.auth?.currentUser, currentUser)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
#if os(iOS)
/** @fn testUpdatePhoneSuccess
@brief Tests the flow of a successful @c updatePhoneNumberCredential:completion: call.
*/
func testUpdatePhoneSuccess() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
let auth = try XCTUnwrap(self.auth)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(withJSON: ["idToken": RPCBaseTests.kFakeAccessToken,
"refreshToken": self.kRefreshToken])
}
self.expectVerifyPhoneNumberRequest()
self.rpcIssuer?.fakeGetAccountProviderJSON = [[
"phoneNumber": self.kPhoneNumber,
]]
let credential = PhoneAuthProvider.provider(auth: auth).credential(
withVerificationID: self.kVerificationID,
verificationCode: self.kVerificationCode
)
user.updatePhoneNumber(credential) { error in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(error)
XCTAssertEqual(auth.currentUser?.phoneNumber, self.kPhoneNumber)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testUpdatePhoneNumberFailure
@brief Tests the flow of a failed @c updatePhoneNumberCredential:completion: call.
*/
func testUpdatePhoneNumberFailure() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
let auth = try XCTUnwrap(self.auth)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(serverErrorMessage: "INVALID_PHONE_NUMBER")
}
self.expectVerifyPhoneNumberRequest()
let credential = PhoneAuthProvider.provider(auth: auth).credential(
withVerificationID: self.kVerificationID,
verificationCode: self.kVerificationCode
)
user.updatePhoneNumber(credential) { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.invalidPhoneNumber.rawValue)
XCTAssertEqual(auth.currentUser, user)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testUpdatePhoneNumberFailureAutoSignOut
@brief Tests the flow of a failed @c updatePhoneNumberCredential:completion: call that
automatically signs out because the session has ended.
*/
func testUpdatePhoneNumberFailureAutoSignOut() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
let auth = try XCTUnwrap(self.auth)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
// The refresh that checks the session fails too.
self.rpcIssuer.secureTokenErrorString = "TOKEN_EXPIRED"
return try self.rpcIssuer.respond(serverErrorMessage: "TOKEN_EXPIRED")
}
self.expectVerifyPhoneNumberRequest()
let credential = PhoneAuthProvider.provider(auth: auth).credential(
withVerificationID: self.kVerificationID,
verificationCode: self.kVerificationCode
)
user.updatePhoneNumber(credential) { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue)
// User is no longer signed in.
XCTAssertNil(self.auth?.currentUser)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
#endif
/** @fn testUpdatePasswordSuccess
@brief Tests the flow of a successful @c updatePassword:completion: call.
*/
func testUpdatePasswordSuccess() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
self.changeUserEmail(user: user, expectation: expectation)
}
waitForExpectations(timeout: 5)
}
/** @fn testUpdatePasswordFailure
@brief Tests the flow of a failed @c updatePassword:completion: call.
*/
func testUpdatePasswordFailure() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(serverErrorMessage: "CREDENTIAL_TOO_OLD_LOGIN_AGAIN")
}
user.updatePassword(to: self.kNewPassword) { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.requiresRecentLogin.rawValue)
// Email should not have changed on the client side.
XCTAssertEqual(user.email, self.kEmail)
// User is still signed in.
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testUpdateEmptyPasswordFailure
@brief Tests the flow of a failed @c updatePassword:completion: call due to an empty password.
*/
func testUpdateEmptyPasswordFailure() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(serverErrorMessage: "WEAK_PASSWORD")
}
user.updatePassword(to: self.kNewPassword) { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.weakPassword.rawValue)
// Email should not have changed on the client side.
XCTAssertEqual(user.email, self.kEmail)
// User is still signed in.
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testUpdatePasswordFailureAutoSignOut
@brief Tests the flow of a failed @c updatePassword:completion: call that automatically signs
out.
*/
func testUpdatePasswordFailureAutoSignOut() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(serverErrorMessage: "USER_DISABLED")
}
user.updatePassword(to: self.kNewPassword) { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userDisabled.rawValue)
// Email should not have changed on the client side.
XCTAssertEqual(user.email, self.kEmail)
// User is signed out.
XCTAssertNil(self.auth?.currentUser)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testChangeProfileSuccess
@brief Tests a successful user profile change flow.
*/
func testChangeProfileSuccess() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(withJSON: ["idToken": RPCBaseTests.kFakeAccessToken,
"refreshToken": self.kRefreshToken])
}
let profileChange = user.createProfileChangeRequest()
profileChange.photoURL = URL(string: self.kTestPhotoURL)
profileChange.displayName = self.kNewDisplayName
profileChange.commitChanges { error in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(error)
XCTAssertEqual(user.displayName, self.kNewDisplayName)
XCTAssertEqual(user.photoURL, URL(string: self.kTestPhotoURL))
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testChangeProfileFailure
@brief Tests a failed user profile change flow.
*/
func testChangeProfileFailure() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(serverErrorMessage: "TOO_MANY_ATTEMPTS_TRY_LATER")
}
let profileChange = user.createProfileChangeRequest()
profileChange.displayName = self.kNewDisplayName
profileChange.commitChanges { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.tooManyRequests.rawValue)
// Email should not have changed on the client side.
XCTAssertEqual(user.email, self.kEmail)
XCTAssertEqual(user.displayName, self.kDisplayName)
// User is still signed in.
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testChangeProfileFailureAutoSignOut
@brief Tests a failed user profile change flow that automatically signs out.
*/
func testChangeProfileFailureAutoSignOut() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(serverErrorMessage: "USER_NOT_FOUND")
}
let profileChange = user.createProfileChangeRequest()
profileChange.displayName = self.kNewDisplayName
profileChange.commitChanges { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userNotFound.rawValue)
// Email should not have changed on the client side.
XCTAssertEqual(user.email, self.kEmail)
// User is signed out.
XCTAssertNil(self.auth?.currentUser)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testGetIDTokenResultSuccess
@brief Tests the flow of a successful @c getIDTokenResultWithCompletion: call.
*/
func testGetIDTokenResultSuccess() {
internalGetIDTokenResult(token: RPCBaseTests.kFakeAccessToken, forceRefresh: false)
}
/** @fn testGetIDTokenResultForcingRefreshSameAccessTokenSuccess
@brief Tests the flow of a successful @c getIDTokenResultForcingRefresh:completion: call when
the returned access token is the same as the stored access token.
*/
func testGetIDTokenResultForcingRefreshSameAccessTokenSuccess() {
internalGetIDTokenResult(token: RPCBaseTests.kFakeAccessToken)
}
/** @fn testGetIDTokenResultForcingRefreshSuccess
@brief Tests the flow successful @c getIDTokenResultForcingRefresh:completion: calls.
*/
func testGetIDTokenResultForcingRefreshSuccess() {
internalGetIDTokenResult(token: RPCBaseTests.kFakeAccessTokenLength415)
internalGetIDTokenResult(token: RPCBaseTests.kFakeAccessTokenLength416)
internalGetIDTokenResult(token: RPCBaseTests.kFakeAccessTokenLength523,
emailMatch: "aunitestuser4@gmail.com")
}
/** @fn testGetIDTokenResultSuccessWithBase64EncodedURL
@brief Tests the flow of a successful @c getIDTokenResultWithCompletion: call using a base64 url
encoded string.
*/
func testGetIDTokenResultSuccessWithBase64EncodedURL() {
internalGetIDTokenResult(token: RPCBaseTests.kFakeAccessTokenWithBase64,
emailMatch: ">>>>>>>>????????@gmail.com",
audMatch: "??????????>>>>>>>>>>")
}
/** @fn testGetIDTokenResultForcingRefreshFailure
@brief Tests the flow of a failed @c getIDTokenResultForcingRefresh:completion: call.
*/
func testGetIDTokenResultForcingRefreshFailure() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser(fakeAccessToken: RPCBaseTests.kFakeAccessToken) { user in
let underlying = NSError(domain: "Test Error", code: 1)
self.rpcIssuer?.secureTokenNetworkError =
AuthErrorUtils.networkError(underlyingError: underlying) as NSError
user.getIDTokenResult(forcingRefresh: true) { tokenResult, rawError in
do {
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(tokenResult)
let error = try XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.networkError.rawValue)
} catch {
XCTFail("Caught an error in \(#function): \(error)")
}
expectation.fulfill()
}
}
waitForExpectations(timeout: 5)
}
/** @fn testReloadSuccess
@brief Tests the flow of a successful @c reloadWithCompletion: call.
*/
func testReloadSuccess() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
user.reload { error in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(error)
XCTAssertEqual(user.displayName, self.kDisplayName)
XCTAssertEqual(user.email, self.kEmail)
expectation.fulfill()
}
}
waitForExpectations(timeout: 5)
}
/** @fn testReloadFailure
@brief Tests the flow of a failed @c reloadWithCompletion: call.
*/
func testReloadFailure() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(serverErrorMessage: "QUOTA_EXCEEDED")
}
// Clear fake so we can inject error
self.rpcIssuer?.fakeGetAccountProviderJSON = nil
user.reload { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.quotaExceeded.rawValue)
// User is still signed in.
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testReloadFailureAutoSignOut
@brief Tests the flow of a failed @c reloadWithCompletion: call that automtatically signs out
because the session has ended.
*/
func testReloadFailureAutoSignOut() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
// The refresh that checks the session fails too.
self.rpcIssuer.secureTokenErrorString = "TOKEN_EXPIRED"
return try self.rpcIssuer.respond(serverErrorMessage: "TOKEN_EXPIRED")
}
// Clear fake so we can inject error
self.rpcIssuer?.fakeGetAccountProviderJSON = nil
user.reload { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue)
// User is no longer signed in.
XCTAssertNil(self.auth?.currentUser)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testReloadTokenExpiredWithValidSessionStaysSignedIn
@brief Tests that a failed @c reloadWithCompletion: call with @c TOKEN_EXPIRED refreshes the
token, and keeps the user signed in when the refresh succeeds.
*/
func testReloadTokenExpiredWithValidSessionStaysSignedIn() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
self.rpcIssuer.respondBlock = {
self.setFakeSecureTokenService(fakeAccessToken: self.kRefreshedAccessToken)
return try self.rpcIssuer.respond(serverErrorMessage: "TOKEN_EXPIRED")
}
// Clear fake so we can inject error
self.rpcIssuer?.fakeGetAccountProviderJSON = nil
user.reload { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue)
// The token was refreshed and the user is still signed in.
XCTAssertEqual(user.rawAccessToken(), self.kRefreshedAccessToken)
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
waitForExpectations(timeout: 5)
}
/** @fn testReloadTokenExpiredWithNetworkErrorDuringCheckStaysSignedIn
@brief Tests that the user stays signed in when the refresh that checks the session after
@c TOKEN_EXPIRED fails with a network error.
*/
func testReloadTokenExpiredWithNetworkErrorDuringCheckStaysSignedIn() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
self.rpcIssuer.respondBlock = {
let underlying = NSError(domain: "Test Error", code: 1)
self.rpcIssuer.secureTokenNetworkError =
AuthErrorUtils.networkError(underlyingError: underlying) as NSError
return try self.rpcIssuer.respond(serverErrorMessage: "TOKEN_EXPIRED")
}
// Clear fake so we can inject error
self.rpcIssuer?.fakeGetAccountProviderJSON = nil
user.reload { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue)
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
waitForExpectations(timeout: 5)
}
/** @fn testReloadTokenExpiredWithUnmappedRefreshErrorSignsOut
@brief Tests that the user is signed out when the refresh that checks the session after
@c TOKEN_EXPIRED fails with an error other than a network error, including one that isn't
mapped to a specific error code.
*/
func testReloadTokenExpiredWithUnmappedRefreshErrorSignsOut() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
self.rpcIssuer.respondBlock = {
// `INVALID_REFRESH_TOKEN` isn't mapped to a specific error code.
self.rpcIssuer.secureTokenErrorString = "INVALID_REFRESH_TOKEN"
return try self.rpcIssuer.respond(serverErrorMessage: "TOKEN_EXPIRED")
}
// Clear fake so we can inject error
self.rpcIssuer?.fakeGetAccountProviderJSON = nil
user.reload { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue)
// User is no longer signed in.
XCTAssertNil(self.auth?.currentUser)
expectation.fulfill()
}
}
waitForExpectations(timeout: 5)
}
/** @fn testReloadTokenExpiredWhenCredentialsChangeDuringCheckStaysSignedIn
@brief Tests that the user stays signed in when another operation installs new credentials
while the refresh that checks the session is in flight, and that refresh fails because
the old refresh token was revoked.
*/
func testReloadTokenExpiredWhenCredentialsChangeDuringCheckStaysSignedIn() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
let newAccessToken = "NEW_ACCESS_TOKEN"
signInWithEmailPasswordReturnFakeUser { user in
self.rpcIssuer.respondBlock = {
XCTAssertTrue(self.rpcIssuer.request is GetAccountInfoRequest)
// Send the refresh that checks the session to `nextRespondBlock`.
self.rpcIssuer.fakeSecureTokenServiceJSON = nil
return try self.rpcIssuer.respond(serverErrorMessage: "TOKEN_EXPIRED")
}
self.rpcIssuer.nextRespondBlock = {
XCTAssertTrue(self.rpcIssuer.request is SecureTokenRequest)
// Another operation, such as a password update, installs new credentials while the
// refresh of the old ones is in flight.
user.tokenService = SecureTokenService(
withRequestConfiguration: user.requestConfiguration,
accessToken: newAccessToken,
accessTokenExpirationDate: Date(timeIntervalSinceNow: 3600),
refreshToken: "NEW_REFRESH_TOKEN"
)
return try self.rpcIssuer.respond(serverErrorMessage: "TOKEN_EXPIRED")
}
// Clear fake so we can inject error
self.rpcIssuer?.fakeGetAccountProviderJSON = nil
user.reload { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue)
// The user is still signed in, with the new credentials.
XCTAssertEqual(user.rawAccessToken(), newAccessToken)
XCTAssertIdentical(self.auth?.currentUser, user)
expectation.fulfill()
}
}
waitForExpectations(timeout: 5)
}
/** @fn testReloadTokenExpiredWhenSameAccountSignsInDuringCheckKeepsNewSession
@brief Tests that when the same account signs in again while the refresh that checks the
session is in flight, the failed refresh doesn't sign out the new session.
*/
func testReloadTokenExpiredWhenSameAccountSignsInDuringCheckKeepsNewSession() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
let auth = try XCTUnwrap(self.auth)
// Stands in for the new session: another `User` instance for the same account.
signInWithEmailPasswordReturnFakeUser { newSessionUser in
self.signInWithEmailPasswordReturnFakeUser { user in
XCTAssertNotIdentical(newSessionUser, user)
XCTAssertEqual(newSessionUser.uid, user.uid)
self.rpcIssuer.respondBlock = {
XCTAssertTrue(self.rpcIssuer.request is GetAccountInfoRequest)
// Send the refresh that checks the session to `nextRespondBlock`.
self.rpcIssuer.fakeSecureTokenServiceJSON = nil
return try self.rpcIssuer.respond(serverErrorMessage: "TOKEN_EXPIRED")
}
self.rpcIssuer.nextRespondBlock = {
XCTAssertTrue(self.rpcIssuer.request is SecureTokenRequest)
// The same account signs in again while the refresh is in flight.
try kAuthGlobalWorkQueue.sync {
try auth.updateCurrentUser(newSessionUser, byForce: true, savingToDisk: false)
}
return try self.rpcIssuer.respond(serverErrorMessage: "TOKEN_EXPIRED")
}
// Clear fake so we can inject error
self.rpcIssuer?.fakeGetAccountProviderJSON = nil
user.reload { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue)
// The new session is still signed in.
XCTAssertIdentical(self.auth?.currentUser, newSessionUser)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testReloadTokenExpiredAfterTokenRefreshedSkipsCheck
@brief Tests that no refresh is sent to check the session when the user's token has been
refreshed since the rejected request was sent.
*/
func testReloadTokenExpiredAfterTokenRefreshedSkipsCheck() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
self.rpcIssuer.respondBlock = {
XCTAssertTrue(self.rpcIssuer.request is GetAccountInfoRequest)
// The token is refreshed while the request is in flight.
user.tokenService.accessToken = self.kRefreshedAccessToken
// Another refresh would fail and sign the user out.
self.rpcIssuer.secureTokenErrorString = "TOKEN_EXPIRED"
return try self.rpcIssuer.respond(serverErrorMessage: "TOKEN_EXPIRED")
}
// Clear fake so we can inject error
self.rpcIssuer?.fakeGetAccountProviderJSON = nil
user.reload { rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue)
// No refresh was sent, and the user is still signed in.
XCTAssertTrue(self.rpcIssuer.request is GetAccountInfoRequest)
XCTAssertEqual(user.rawAccessToken(), self.kRefreshedAccessToken)
XCTAssertIdentical(self.auth?.currentUser, user)
expectation.fulfill()
}
}
waitForExpectations(timeout: 5)
}
/** @fn testReauthenticateSuccess
@brief Tests the flow of a successful @c reauthenticateWithCredential:completion: call.
*/
func testReauthenticateSuccess() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(withJSON: ["idToken": RPCBaseTests.kFakeAccessToken,
"refreshToken": self.kRefreshToken])
}
let emailCredential = EmailAuthProvider.credential(withEmail: self.kEmail,
password: self.kFakePassword)
user.reauthenticate(with: emailCredential) { rawResult, error in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(error)
let result = try! XCTUnwrap(rawResult)
XCTAssertEqual(result.user.uid, user.uid)
XCTAssertEqual(result.user.email, user.email)
XCTAssertEqual(result.additionalUserInfo?.isNewUser, false)
// User is still signed in.
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testReauthenticateWithCredentialSuccess
@brief Tests the flow of a successful @c reauthenticateWithCredential call.
*/
func testReauthenticateWithCredentialSuccess() throws {
let expectation = self.expectation(description: #function)
signInWithGoogleCredential { user in
do {
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(withJSON: ["idToken": RPCBaseTests.kFakeAccessToken,
"refreshToken": self.kRefreshToken,
"federatedId": self.kGoogleID,
"providerId": GoogleAuthProvider.id,
"localId": self.kLocalID,
"displayName": self.kGoogleDisplayName,
"rawUserInfo": self.kGoogleProfile,
"username": self.kUserName])
}
let googleCredential = GoogleAuthProvider.credential(withIDToken: self.kGoogleIDToken,
accessToken: self.kGoogleAccessToken)
user.reauthenticate(with: googleCredential) { reauthenticatedAuthResult, error in
XCTAssertTrue(Thread.isMainThread)
do {
try self.assertUserGoogle(reauthenticatedAuthResult?.user)
} catch {
XCTFail("\(error)")
}
XCTAssertNil(error)
// Verify that the current user is unchanged.
XCTAssertEqual(self.auth?.currentUser, user)
// Verify that the current user and reauthenticated user are not same pointers.
XCTAssertNotEqual(user, reauthenticatedAuthResult?.user)
// Verify that anyway the current user and reauthenticated user have same IDs.
XCTAssertEqual(reauthenticatedAuthResult?.user.uid, user.uid)
XCTAssertEqual(reauthenticatedAuthResult?.user.email, user.email)
XCTAssertEqual(reauthenticatedAuthResult?.user.displayName, user.displayName)
XCTAssertEqual(reauthenticatedAuthResult?.additionalUserInfo?.username, self.kUserName)
XCTAssertEqual(reauthenticatedAuthResult?.additionalUserInfo?.providerID,
GoogleAuthProvider.id)
XCTAssertEqual(
reauthenticatedAuthResult?.additionalUserInfo?.profile as? [String: String],
self.kGoogleProfile
)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
try assertUserGoogle(auth?.currentUser)
}
/** @fn testReauthenticateFailure
@brief Tests the flow of a failed @c reauthenticateWithCredential:completion: call.
*/
func testReauthenticateFailure() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(withJSON: ["idToken": RPCBaseTests.kFakeAccessToken,
"refreshToken": self.kRefreshToken])
}
self.setFakeGetAccountProvider(withLocalID: "A different Local ID")
let emailCredential = EmailAuthProvider.credential(withEmail: self.kEmail,
password: self.kFakePassword)
user.reauthenticate(with: emailCredential) { reauthenticatedAuthResult, rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userMismatch.rawValue)
// Email should not have changed on the client side.
XCTAssertEqual(user.email, self.kEmail)
// User is still signed in.
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testReauthenticateUserMismatchFailure
@brief Tests the flow of a failed @c reauthenticateWithCredential:completion: call due to trying
to reauthenticate a user that does not exist.
*/
func testReauthenticateUserMismatchFailure() {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
do {
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(serverErrorMessage: "USER_NOT_FOUND")
}
let googleCredential = GoogleAuthProvider.credential(withIDToken: self.kGoogleIDToken,
accessToken: self.kGoogleAccessToken)
user.reauthenticate(with: googleCredential) { reauthenticatedAuthResult, rawError in
XCTAssertTrue(Thread.isMainThread)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userMismatch.rawValue)
// Email should not have changed on the client side.
XCTAssertEqual(user.email, self.kEmail)
// User is still signed in.
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testLinkAndRetrieveDataSuccess (and old testLinkCredentialSuccess)
@brief Tests the flow of a successful @c linkWithCredential call.
*/
func testLinkAndRetrieveDataSuccess() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
let auth = try XCTUnwrap(self.auth)
signInWithFacebookCredential { user in
XCTAssertNotNil(user)
do {
self.setFakeGoogleGetAccountProvider()
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(withJSON: ["idToken": RPCBaseTests.kFakeAccessToken,
"refreshToken": self.kRefreshToken,
"federatedId": self.kGoogleID,
"providerId": GoogleAuthProvider.id,
"localId": self.kLocalID,
"displayName": self.kGoogleDisplayName,
"rawUserInfo": self.kGoogleProfile,
"username": self.kUserName])
}
let googleCredential = GoogleAuthProvider.credential(withIDToken: self.kGoogleIDToken,
accessToken: self.kGoogleAccessToken)
user.link(with: googleCredential) { linkAuthResult, error in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(error)
// Verify that the current user is unchanged.
XCTAssertEqual(auth.currentUser, user)
// Verify that the current user and reauthenticated user are the same pointers.
XCTAssertEqual(user, linkAuthResult?.user)
// Verify that anyway the current user and reauthenticated user have same IDs.
XCTAssertEqual(linkAuthResult?.user.uid, user.uid)
XCTAssertEqual(linkAuthResult?.user.email, user.email)
XCTAssertEqual(linkAuthResult?.user.displayName, user.displayName)
XCTAssertEqual(linkAuthResult?.additionalUserInfo?.username, self.kUserName)
XCTAssertEqual(linkAuthResult?.additionalUserInfo?.providerID,
GoogleAuthProvider.id)
XCTAssertEqual(
linkAuthResult?.additionalUserInfo?.profile as? [String: String],
self.kGoogleProfile
)
XCTAssertEqual(linkAuthResult?.user.providerData.first?.providerID, GoogleAuthProvider.id)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
try assertUserGoogle(auth.currentUser)
}
/** @fn testLinkAndRetrieveDataError
@brief Tests the flow of an unsuccessful @c linkWithCredential:completion:
call with an error from the backend.
*/
func testLinkAndRetrieveDataError() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithFacebookCredential { user in
XCTAssertNotNil(user)
do {
self.setFakeGetAccountProvider()
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(serverErrorMessage: "CREDENTIAL_TOO_OLD_LOGIN_AGAIN")
}
let googleCredential = GoogleAuthProvider.credential(withIDToken: self.kGoogleIDToken,
accessToken: self.kGoogleAccessToken)
user.link(with: googleCredential) { linkAuthResult, rawError in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(linkAuthResult)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.requiresRecentLogin.rawValue)
// Email should not have changed on the client side.
XCTAssertEqual(user.email, self.kFacebookEmail)
// User is still signed in.
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testLinkAndRetrieveDataProviderAlreadyLinked and old testLinkCredentialProviderAlreadyLinkedError
@brief Tests the flow of an unsuccessful @c linkWithCredential:completion:
call with FIRAuthErrorCodeProviderAlreadyLinked, which is a client side error.
*/
func testLinkAndRetrieveDataProviderAlreadyLinked() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithFacebookCredential { user in
XCTAssertNotNil(user)
do {
self.setFakeGetAccountProvider()
let facebookCredential =
FacebookAuthProvider.credential(withAccessToken: self.kFacebookAccessToken)
user.link(with: facebookCredential) { linkAuthResult, rawError in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(linkAuthResult)
do {
let error = try XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.providerAlreadyLinked.rawValue)
} catch {
XCTFail("Expected to throw providerAlreadyLinked error.")
}
// User is still signed in.
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testLinkAndRetrieveDataErrorAutoSignOut (and old testLinkCredentialError)
@brief Tests the flow of an unsuccessful @c linkWithCredential:completion:
call that automatically signs out.
*/
func testLinkAndRetrieveDataErrorAutoSignOut() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithFacebookCredential { user in
XCTAssertNotNil(user)
do {
self.setFakeGetAccountProvider()
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(serverErrorMessage: "USER_DISABLED")
}
let googleCredential = GoogleAuthProvider.credential(withIDToken: self.kGoogleIDToken,
accessToken: self.kGoogleAccessToken)
user.link(with: googleCredential) { linkAuthResult, rawError in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(linkAuthResult)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userDisabled.rawValue)
// User is signed out.
XCTAssertNil(self.auth?.currentUser)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testLinkEmailAndRetrieveDataSuccess
@brief Tests the flow of a successful @c linkWithCredential:completion:
invocation for email credential.
*/
func testLinkEmailAndRetrieveDataSuccess() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
let auth = try XCTUnwrap(self.auth)
signInWithFacebookCredential { user in
XCTAssertNotNil(user)
do {
self.rpcIssuer.respondBlock = {
let request = self.rpcIssuer?.request as? SignUpNewUserRequest
XCTAssertNotNil(request)
XCTAssertEqual(request?.email, self.kEmail)
XCTAssertEqual(request?.password, self.kFakePassword)
XCTAssertNil(request?.displayName)
let (data, error) = try self.rpcIssuer.respond(withJSON: [
"idToken": RPCBaseTests.kFakeAccessToken,
"refreshToken": self.kRefreshToken,
])
self.setFakeGetAccountProvider(withProviderID: EmailAuthProvider.id)
return (data, error)
}
let emailCredential = EmailAuthProvider.credential(withEmail: self.kEmail,
password: self.kFakePassword)
user.link(with: emailCredential) { linkAuthResult, error in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(error)
// Verify that the current user is unchanged.
XCTAssertEqual(auth.currentUser, user)
// Verify that the current user and reauthenticated user are the same pointers.
XCTAssertEqual(user, linkAuthResult?.user)
// Verify that anyway the current user and reauthenticated user have same IDs.
XCTAssertEqual(linkAuthResult?.user.uid, user.uid)
XCTAssertEqual(linkAuthResult?.user.email, user.email)
XCTAssertEqual(linkAuthResult?.user.displayName, user.displayName)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn tesLlinkEmailProviderAlreadyLinkedError
@brief Tests the flow of an unsuccessful @c linkWithCredential:completion:
invocation for email credential and FIRAuthErrorCodeProviderAlreadyLinked which is a client
side error.
*/
func testLinkEmailProviderAlreadyLinkedError() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithFacebookCredential { user in
XCTAssertNotNil(user)
do {
self.rpcIssuer.respondBlock = {
let request = self.rpcIssuer?.request as? SignUpNewUserRequest
XCTAssertNotNil(request)
XCTAssertEqual(request?.email, self.kEmail)
XCTAssertEqual(request?.password, self.kFakePassword)
let (data, error) = try self.rpcIssuer.respond(withJSON: [
"idToken": RPCBaseTests.kFakeAccessToken,
"refreshToken": self.kRefreshToken,
])
self.setFakeGetAccountProvider(withProviderID: EmailAuthProvider.id)
return (data, error)
}
let emailCredential = EmailAuthProvider.credential(withEmail: self.kEmail,
password: self.kFakePassword)
user.link(with: emailCredential) { linkAuthResult, error in
XCTAssertEqual(user, linkAuthResult?.user)
linkAuthResult?.user.link(with: emailCredential) { linkLinkAuthResult, rawError in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(linkLinkAuthResult)
do {
let error = try XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.providerAlreadyLinked.rawValue)
} catch {
XCTFail("Expected to throw providerAlreadyLinked error.")
}
// User is still signed in.
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testLinkEmailAndRetrieveDataError
@brief Tests the flow of an unsuccessful @c linkWithCredential:completion:
invocation for email credential and an error from the backend.
*/
func testLinkEmailAndRetrieveDataError() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithFacebookCredential { user in
XCTAssertNotNil(user)
do {
self.rpcIssuer.respondBlock = {
let request = self.rpcIssuer?.request as? SignUpNewUserRequest
XCTAssertNotNil(request)
XCTAssertEqual(request?.email, self.kEmail)
XCTAssertEqual(request?.password, self.kFakePassword)
return try self.rpcIssuer.respond(serverErrorMessage: "TOO_MANY_ATTEMPTS_TRY_LATER")
}
let emailCredential = EmailAuthProvider.credential(withEmail: self.kEmail,
password: self.kFakePassword)
user.link(with: emailCredential) { linkAuthResult, rawError in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(linkAuthResult)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.tooManyRequests.rawValue)
// User is still signed in.
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testLinkEmailAndRetrieveDataErrorAutoSignOut
@brief Tests the flow of an unsuccessful @c linkWithCredential:completion:
invocation that automatically signs out because the session has ended.
*/
func testLinkEmailAndRetrieveDataErrorAutoSignOut() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithFacebookCredential { user in
XCTAssertNotNil(user)
do {
self.rpcIssuer.respondBlock = {
XCTAssertNotNil(self.rpcIssuer?.request as? SignUpNewUserRequest)
// The refresh that checks the session fails too.
self.rpcIssuer.secureTokenErrorString = "TOKEN_EXPIRED"
return try self.rpcIssuer.respond(serverErrorMessage: "TOKEN_EXPIRED")
}
let emailCredential = EmailAuthProvider.credential(withEmail: self.kEmail,
password: self.kFakePassword)
user.link(with: emailCredential) { linkAuthResult, rawError in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(linkAuthResult)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue)
// User is signed out.
XCTAssertNil(self.auth?.currentUser)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testLinkEmailTokenExpiredWithValidSessionStaysSignedIn
@brief Tests that an unsuccessful @c linkWithCredential:completion: invocation for email
credential with @c TOKEN_EXPIRED refreshes the token, and keeps the user signed in when
the refresh succeeds.
*/
func testLinkEmailTokenExpiredWithValidSessionStaysSignedIn() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithFacebookCredential { user in
self.rpcIssuer.respondBlock = {
XCTAssertNotNil(self.rpcIssuer?.request as? SignUpNewUserRequest)
self.setFakeSecureTokenService(fakeAccessToken: self.kRefreshedAccessToken)
return try self.rpcIssuer.respond(serverErrorMessage: "TOKEN_EXPIRED")
}
let emailCredential = EmailAuthProvider.credential(withEmail: self.kEmail,
password: self.kFakePassword)
user.link(with: emailCredential) { linkAuthResult, rawError in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(linkAuthResult)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue)
// The token was refreshed and the user is still signed in.
XCTAssertEqual(user.rawAccessToken(), self.kRefreshedAccessToken)
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
waitForExpectations(timeout: 5)
}
#if os(iOS)
private class FakeOAuthProvider: OAuthProvider {
override func credential(with uiDelegate: AuthUIDelegate?) async throws -> AuthCredential {
return OAuthCredential(
withProviderID: GoogleAuthProvider.id,
sessionID: UserTests.kOAuthSessionID,
OAuthResponseURLString: UserTests.kOAuthRequestURI
)
}
}
/** @fn testLinkProviderFailure
@brief Tests the flow of a failed @c linkWithProvider:completion:
call.
*/
func testLinkProviderFailure() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
let auth = try XCTUnwrap(self.auth)
signInWithFacebookCredential { user in
XCTAssertNotNil(user)
do {
self.setFakeGetAccountProvider()
self.rpcIssuer.respondBlock = {
// The refresh that checks the session fails too.
self.rpcIssuer.secureTokenErrorString = "TOKEN_EXPIRED"
return try self.rpcIssuer.respond(serverErrorMessage: "TOKEN_EXPIRED")
}
user.link(with: FakeOAuthProvider(providerID: "foo", auth: auth),
uiDelegate: nil) { linkAuthResult, rawError in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(linkAuthResult)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue)
// User is signed out.
XCTAssertNil(self.auth?.currentUser)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testReauthenticateWithProviderFailure
@brief Tests the flow of a failed @c reauthenticateWithProvider:completion: call.
*/
func testReauthenticateWithProviderFailure() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
let auth = try XCTUnwrap(self.auth)
signInWithFacebookCredential { user in
XCTAssertNotNil(user)
do {
self.setFakeGetAccountProvider()
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(serverErrorMessage: "TOKEN_EXPIRED")
}
user.reauthenticate(with: FakeOAuthProvider(providerID: "foo", auth: auth),
uiDelegate: nil) { linkAuthResult, rawError in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(linkAuthResult)
let error = try! XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue)
// User is still signed in.
XCTAssertEqual(self.auth?.currentUser, user)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testLinkPhoneAuthCredentialSuccess
@brief Tests the flow of a successful @c linkWithCredential call using a phoneAuthCredential.
*/
func testLinkPhoneAuthCredentialSuccess() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
let auth = try XCTUnwrap(self.auth)
signInWithEmailPasswordReturnFakeUser { user in
XCTAssertNotNil(user)
self.expectVerifyPhoneNumberRequest(isLink: true)
do {
self.setFakeGetAccountProvider(withProviderID: PhoneAuthProvider.id)
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(withJSON: ["idToken": RPCBaseTests.kFakeAccessToken,
"refreshToken": self.kRefreshToken])
}
let credential = PhoneAuthProvider.provider(auth: auth).credential(
withVerificationID: self.kVerificationID,
verificationCode: self.kVerificationCode
)
user.link(with: credential) { linkAuthResult, error in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(error)
// Verify that the current user is unchanged.
XCTAssertEqual(auth.currentUser, user)
// Verify that the current user and reauthenticated user are the same pointers.
XCTAssertEqual(user, linkAuthResult?.user)
// Verify that anyway the current user and reauthenticated user have same IDs.
XCTAssertEqual(linkAuthResult?.user.uid, user.uid)
XCTAssertEqual(linkAuthResult?.user.email, user.email)
XCTAssertEqual(linkAuthResult?.user.displayName, user.displayName)
XCTAssertEqual(auth.currentUser?.providerData.first?.providerID, PhoneAuthProvider.id)
XCTAssertEqual(
linkAuthResult?.user.providerData.first?.providerID,
PhoneAuthProvider.id
)
XCTAssertEqual(auth.currentUser?.phoneNumber, self.kTestPhoneNumber)
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testUnlinkPhoneAuthCredentialSuccess
@brief Tests the flow of a successful @c unlinkFromProvider:completion: call using a
@c FIRPhoneAuthProvider.
*/
func testUnlinkPhoneAuthCredentialSuccess() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
let auth = try XCTUnwrap(self.auth)
signInWithEmailPasswordReturnFakeUser { user in
XCTAssertNotNil(user)
self.expectVerifyPhoneNumberRequest(isLink: true)
do {
self.setFakeGetAccountProvider(withProviderID: PhoneAuthProvider.id)
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(withJSON: ["idToken": RPCBaseTests.kFakeAccessToken,
"refreshToken": self.kRefreshToken])
}
let credential = PhoneAuthProvider.provider(auth: auth).credential(
withVerificationID: self.kVerificationID,
verificationCode: self.kVerificationCode
)
user.link(with: credential) { linkAuthResult, error in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(error)
// Verify that the current user is unchanged.
XCTAssertEqual(auth.currentUser, user)
// Verify that the current user and reauthenticated user are the same pointers.
XCTAssertEqual(user, linkAuthResult?.user)
// Verify that anyway the current user and reauthenticated user have same IDs.
XCTAssertEqual(linkAuthResult?.user.uid, user.uid)
XCTAssertEqual(linkAuthResult?.user.email, user.email)
XCTAssertEqual(linkAuthResult?.user.displayName, user.displayName)
XCTAssertEqual(auth.currentUser?.providerData.first?.providerID, PhoneAuthProvider.id)
XCTAssertEqual(
linkAuthResult?.user.providerData.first?.providerID,
PhoneAuthProvider.id
)
XCTAssertEqual(auth.currentUser?.phoneNumber, self.kTestPhoneNumber)
// Immediately unlink the phone auth provider.
self.rpcIssuer.respondBlock = {
let request = try XCTUnwrap(self.rpcIssuer?.request as? SetAccountInfoRequest)
XCTAssertEqual(request.apiKey, UserTests.kFakeAPIKey)
XCTAssertEqual(request.accessToken, RPCBaseTests.kFakeAccessToken)
XCTAssertNil(request.email)
XCTAssertNil(request.password)
XCTAssertNil(request.localID)
XCTAssertNil(request.displayName)
XCTAssertNil(request.photoURL)
XCTAssertNil(request.providers)
XCTAssertNil(request.deleteAttributes)
XCTAssertEqual(try XCTUnwrap(request.deleteProviders?.first), PhoneAuthProvider.id)
return try self.rpcIssuer.respond(withJSON: ["idToken": RPCBaseTests.kFakeAccessToken,
"refreshToken": self.kRefreshToken])
}
user.unlink(fromProvider: PhoneAuthProvider.id) { user, error in
XCTAssertNil(error)
XCTAssertEqual(auth.currentUser, user)
XCTAssertNil(auth.currentUser?.phoneNumber)
expectation.fulfill()
}
}
}
}
waitForExpectations(timeout: 5)
}
/** @fn testlinkPhoneAuthCredentialFailure
@brief Tests the flow of a failed call to @c linkWithCredential:completion: due
to a phone provider already being linked.
*/
func testlinkPhoneAuthCredentialFailure() throws {
setFakeGetAccountProvider(withPasswordHash: kFakePassword)
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser { user in
XCTAssertNotNil(user)
self.expectVerifyPhoneNumberRequest(isLink: true)
self.setFakeGetAccountProvider(withProviderID: PhoneAuthProvider.id)
let credential = EmailAuthCredential(withEmail: self.kEmail, password: self.kFakePassword)
user.link(with: credential) { linkAuthResult, rawError in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(linkAuthResult)
if let error = try? XCTUnwrap(rawError) {
XCTAssertEqual((error as NSError).code, AuthErrorCode.providerAlreadyLinked.rawValue)
} else {
XCTFail("Did not throw expected error")
}
expectation.fulfill()
}
}
waitForExpectations(timeout: 5)
}
/** @fn testlinkPhoneCredentialAlreadyExistsError
@brief Tests the flow of @c linkWithCredential:completion:
call using a phoneAuthCredential and a credential already exists error. In this case we
should get a AuthCredential in the error object.
*/
func testlinkPhoneCredentialAlreadyExistsError() throws {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
let auth = try XCTUnwrap(self.auth)
signInWithEmailPasswordReturnFakeUser { user in
XCTAssertNotNil(user)
self.expectVerifyPhoneNumberRequest(isLink: true)
do {
self.setFakeGetAccountProvider(withProviderID: PhoneAuthProvider.id)
self.rpcIssuer.respondBlock = {
try self.rpcIssuer.respond(withJSON: ["idToken": RPCBaseTests.kFakeAccessToken,
"refreshToken": self.kRefreshToken,
"phoneNumber": self.kTestPhoneNumber,
"temporaryProof": "Fake Temporary Proof"])
}
let credential = PhoneAuthProvider.provider(auth: auth).credential(
withVerificationID: self.kVerificationID,
verificationCode: self.kVerificationCode
)
user.link(with: credential) { linkAuthResult, rawError in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(linkAuthResult)
do {
let error = try XCTUnwrap(rawError)
XCTAssertEqual((error as NSError).code, AuthErrorCode.credentialAlreadyInUse.rawValue)
let credential = try XCTUnwrap((error as NSError)
.userInfo[AuthErrors.userInfoUpdatedCredentialKey] as? PhoneAuthCredential)
switch credential.credentialKind {
case let .phoneNumber(phoneNumber, temporaryProof):
XCTAssertEqual(temporaryProof, "Fake Temporary Proof")
XCTAssertEqual(phoneNumber, self.kTestPhoneNumber)
case .verification: XCTFail("Should be phoneNumber case")
}
} catch {
XCTFail("Did not throw expected error \(error)")
}
expectation.fulfill()
}
}
}
waitForExpectations(timeout: 5)
}
#endif
func testRetrieveUserWithInvalidToken() async throws {
let auth = try XCTUnwrap(self.auth)
do {
_ = try await User.retrieveUser(
withAuth: auth,
accessToken: nil,
accessTokenExpirationDate: Date(),
refreshToken: nil,
anonymous: false
)
XCTFail("Expected an error to be thrown")
} catch let error as NSError {
XCTAssertEqual(error.domain, AuthErrors.domain)
XCTAssertEqual(error.code, AuthErrorCode.invalidUserToken.rawValue)
}
}
// MARK: Private helper functions
private func expectVerifyPhoneNumberRequest(isLink: Bool = false) {
rpcIssuer?.verifyPhoneNumberRequester = { request in
XCTAssertEqual(request.verificationID, self.kVerificationID)
XCTAssertEqual(request.verificationCode, self.kVerificationCode)
XCTAssertEqual(request.accessToken, RPCBaseTests.kFakeAccessToken)
if isLink {
XCTAssertEqual(request.operation, AuthOperationType.link)
} else {
XCTAssertEqual(request.operation, AuthOperationType.update)
}
}
}
private func internalGetIDTokenResult(token: String, forceRefresh: Bool = true,
emailMatch: String = "aunitestuser@gmail.com",
audMatch: String = "test_aud") {
setFakeGetAccountProvider()
let expectation = self.expectation(description: #function)
signInWithEmailPasswordReturnFakeUser(fakeAccessToken: token) { user in
user.getIDTokenResult(forcingRefresh: forceRefresh) { rawTokenResult, error in
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(error)
XCTAssertEqual(user.displayName, self.kDisplayName)
XCTAssertEqual(user.email, self.kEmail)
let tokenResult = try! XCTUnwrap(rawTokenResult)
XCTAssertEqual(tokenResult.token, token)
XCTAssertNotNil(tokenResult.issuedAtDate)
XCTAssertNotNil(tokenResult.authDate)
XCTAssertNotNil(tokenResult.expirationDate)
XCTAssertNotNil(tokenResult.signInProvider)
// The lowercased is for the base64 test which seems to be an erroneously uppercased
// "Password"?
XCTAssertEqual(tokenResult.signInProvider.lowercased(), EmailAuthProvider.id)
XCTAssertEqual(tokenResult.claims["email"] as! String, emailMatch)
XCTAssertEqual(tokenResult.claims["aud"] as! String, audMatch)
XCTAssertEqual(tokenResult.signInSecondFactor, "")
expectation.fulfill()
}
}
waitForExpectations(timeout: 5)
}
private func changeUserEmail(user: User, changeEmail: Bool = false,
expectation: XCTestExpectation) {
do {
XCTAssertEqual(user.providerID, "Firebase")
XCTAssertEqual(user.uid, kLocalID)
XCTAssertEqual(user.displayName, kDisplayName)
XCTAssertEqual(user.photoURL, URL(string: kTestPhotoURL))
XCTAssertEqual(user.email, kEmail)
// Pretend that the display name on the server has been changed since the original signin.
setFakeGetAccountProvider(withNewDisplayName: kNewDisplayName)
rpcIssuer.respondBlock = {
let request = try XCTUnwrap(self.rpcIssuer?.request as? SetAccountInfoRequest)
XCTAssertEqual(request.apiKey, UserTests.kFakeAPIKey)
XCTAssertEqual(request.accessToken, RPCBaseTests.kFakeAccessToken)
if changeEmail {
XCTAssertEqual(request.email, self.kNewEmail)
XCTAssertNil(request.password)
} else {
XCTAssertEqual(request.password, self.kNewPassword)
XCTAssertNil(request.email)
}
XCTAssertNil(request.localID)
XCTAssertNil(request.displayName)
XCTAssertNil(request.photoURL)
XCTAssertNil(request.providers)
XCTAssertNil(request.deleteAttributes)
XCTAssertNil(request.deleteProviders)
return try self.rpcIssuer.respond(withJSON: ["idToken": RPCBaseTests.kFakeAccessToken,
"email": self.kNewEmail,
"refreshToken": self.kRefreshToken])
}
if changeEmail {
user.updateEmail(to: kNewEmail) { error in
XCTAssertNil(error)
XCTAssertEqual(user.email, self.kNewEmail)
XCTAssertEqual(user.displayName, self.kNewDisplayName)
XCTAssertFalse(user.isAnonymous)
expectation.fulfill()
}
} else {
user.updatePassword(to: kNewPassword) { error in
XCTAssertNil(error)
XCTAssertEqual(user.displayName, self.kNewDisplayName)
XCTAssertFalse(user.isAnonymous)
expectation.fulfill()
}
}
}
}
private func signInWithEmailPasswordReturnFakeUser(fakeAccessToken: String = RPCBaseTests
.kFakeAccessToken,
completion: @escaping (User) -> Void) {
let kRefreshToken = "fakeRefreshToken"
setFakeSecureTokenService(fakeAccessToken: fakeAccessToken)
rpcIssuer?.verifyPasswordRequester = { request in
// 2. Validate the created Request instance.
XCTAssertEqual(request.email, self.kEmail)
XCTAssertEqual(request.password, self.kFakePassword)
XCTAssertEqual(request.apiKey, UserTests.kFakeAPIKey)
XCTAssertTrue(request.returnSecureToken)
do {
// 3. Send the response from the fake backend.
return try self.rpcIssuer.respond(withJSON: ["idToken": fakeAccessToken,
"isNewUser": true,
"refreshToken": kRefreshToken])
} catch {
XCTFail("Failure sending response: \(error)")
return (nil, nil)
}
}
// 1. After setting up fakes, sign out and sign in.
do {
try auth?.signOut()
} catch {
XCTFail("Sign out failed: \(error)")
return
}
auth?.signIn(withEmail: kEmail, password: kFakePassword) { authResult, error in
// 4. After the response triggers the callback, verify the returned result.
XCTAssertTrue(Thread.isMainThread)
guard let user = authResult?.user else {
XCTFail("authResult.user is missing")
return
}
XCTAssertEqual(user.refreshToken, kRefreshToken)
XCTAssertFalse(user.isAnonymous)
XCTAssertEqual(user.email, self.kEmail)
guard let additionalUserInfo = authResult?.additionalUserInfo else {
XCTFail("authResult.additionalUserInfo is missing")
return
}
XCTAssertFalse(additionalUserInfo.isNewUser)
XCTAssertEqual(additionalUserInfo.providerID, EmailAuthProvider.id)
XCTAssertNil(error)
// Clear the password Requester to avoid being called again by reauthenticate tests.
self.rpcIssuer?.verifyPasswordRequester = nil
completion(user)
}
}
private func signInWithGoogleCredential(completion: @escaping (User) -> Void) {
setFakeSecureTokenService(fakeAccessToken: RPCBaseTests.kFakeAccessToken)
setFakeGoogleGetAccountProvider()
rpcIssuer.respondBlock = {
try self.verifyGoogleAssertionRequest(
XCTUnwrap(self.rpcIssuer?.request as? VerifyAssertionRequest)
)
// 3. Send the response from the fake backend.
return try self.rpcIssuer.respond(withJSON: ["idToken": RPCBaseTests.kFakeAccessToken,
"providerId": GoogleAuthProvider.id,
"refreshToken": self.kRefreshToken,
"localId": self.kLocalID,
"displayName": self.kDisplayName,
"rawUserInfo": self.kGoogleProfile,
"username": self.kUserName])
}
do {
try auth?.signOut()
let googleCredential = GoogleAuthProvider.credential(withIDToken: kGoogleIDToken,
accessToken: kGoogleAccessToken)
auth?.signIn(with: googleCredential) { authResult, error in
// 4. After the response triggers the callback, verify the returned result.
XCTAssertTrue(Thread.isMainThread)
guard let user = authResult?.user else {
XCTFail("authResult.user is missing")
return
}
XCTAssertEqual(user.refreshToken, self.kRefreshToken)
XCTAssertFalse(user.isAnonymous)
XCTAssertEqual(user.email, self.kGoogleEmail)
guard let additionalUserInfo = authResult?.additionalUserInfo,
let profile = additionalUserInfo.profile as? [String: String] else {
XCTFail("authResult.additionalUserInfo and/or profile is missing")
return
}
XCTAssertEqual(profile, self.kGoogleProfile)
XCTAssertFalse(additionalUserInfo.isNewUser)
XCTAssertEqual(additionalUserInfo.providerID, GoogleAuthProvider.id)
XCTAssertEqual(additionalUserInfo.username, self.kUserName)
XCTAssertNil(error)
completion(user)
}
} catch {
XCTFail("Throw in \(#function): \(error)")
}
}
private func verifyGoogleAssertionRequest(_ request: VerifyAssertionRequest) {
XCTAssertEqual(request.providerID, GoogleAuthProvider.id)
XCTAssertEqual(request.providerIDToken, kGoogleIDToken)
XCTAssertEqual(request.providerAccessToken, kGoogleAccessToken)
XCTAssertTrue(request.returnSecureToken)
XCTAssertEqual(request.apiKey, UserTests.kFakeAPIKey)
XCTAssertTrue(request.returnSecureToken)
}
private func signInWithFacebookCredential(completion: @escaping (User) -> Void) {
setFakeSecureTokenService(fakeAccessToken: RPCBaseTests.kFakeAccessToken)
setFakeGetAccountProvider(withNewDisplayName: kFacebookDisplayName,
withProviderID: FacebookAuthProvider.id,
withFederatedID: kFacebookID,
withEmail: kFacebookEmail)
rpcIssuer.respondBlock = {
let request = try XCTUnwrap(self.rpcIssuer?.request as? VerifyAssertionRequest)
XCTAssertEqual(request.providerID, FacebookAuthProvider.id)
XCTAssertEqual(request.providerIDToken, self.kFacebookIDToken)
XCTAssertEqual(request.providerAccessToken, self.kFacebookAccessToken)
XCTAssertTrue(request.returnSecureToken)
XCTAssertEqual(request.apiKey, UserTests.kFakeAPIKey)
XCTAssertTrue(request.returnSecureToken)
// 3. Send the response from the fake backend.
return try self.rpcIssuer.respond(withJSON: ["idToken": RPCBaseTests.kFakeAccessToken,
"refreshToken": self.kRefreshToken,
"federatedId": self.kFacebookID,
"providerId": FacebookAuthProvider.id,
"localId": self.kLocalID,
"displayName": self.kDisplayName,
"rawUserInfo": self.kGoogleProfile,
"username": self.kUserName])
}
do {
try auth?.signOut()
let facebookCredential = FacebookAuthProvider
.credential(withAccessToken: kFacebookAccessToken)
auth?.signIn(with: facebookCredential) { authResult, error in
// 4. After the response triggers the callback, verify the returned result.
XCTAssertTrue(Thread.isMainThread)
guard let user = authResult?.user else {
XCTFail("authResult.user is missing")
return
}
XCTAssertEqual(user.refreshToken, self.kRefreshToken)
XCTAssertFalse(user.isAnonymous)
XCTAssertEqual(user.email, self.kFacebookEmail)
XCTAssertEqual(user.displayName, self.kFacebookDisplayName)
XCTAssertEqual(user.providerData.count, 1)
guard let additionalUserInfo = authResult?.additionalUserInfo,
let facebookUserInfo = user.providerData.first,
let profile = additionalUserInfo.profile as? [String: String] else {
XCTFail("authResult.additionalUserInfo and/or profile is missing")
return
}
XCTAssertEqual(facebookUserInfo.providerID, FacebookAuthProvider.id)
XCTAssertEqual(facebookUserInfo.uid, self.kFacebookID)
XCTAssertEqual(facebookUserInfo.displayName, self.kFacebookDisplayName)
XCTAssertEqual(facebookUserInfo.email, self.kFacebookEmail)
XCTAssertEqual(profile, self.kGoogleProfile)
XCTAssertFalse(additionalUserInfo.isNewUser)
XCTAssertEqual(additionalUserInfo.providerID, FacebookAuthProvider.id)
XCTAssertEqual(additionalUserInfo.username, self.kUserName)
XCTAssertNil(error)
completion(user)
}
} catch {
XCTFail("Throw in \(#function): \(error)")
}
}
private func signInWithEmailPasswordReturnFakeUserLink(completion: @escaping (User) -> Void) {
let kRefreshToken = "fakeRefreshToken"
setFakeSecureTokenService()
rpcIssuer.respondBlock = {
let request = try XCTUnwrap(self.rpcIssuer?.request as? EmailLinkSignInRequest)
XCTAssertEqual(request.email, self.kEmail)
XCTAssertEqual(request.apiKey, UserTests.kFakeAPIKey)
XCTAssertEqual(request.oobCode, "aCode")
XCTAssertNil(request.idToken)
// Send the response from the fake backend.
return try self.rpcIssuer.respond(withJSON: ["idToken": RPCBaseTests.kFakeAccessToken,
"isNewUser": true,
"refreshToken": kRefreshToken])
}
do {
try auth?.signOut()
auth?.signIn(
withEmail: kEmail,
link: "https://www.google.com?oobCode=aCode&mode=signIn"
) { authResult, error in
// 4. After the response triggers the callback, verify the returned result.
XCTAssertTrue(Thread.isMainThread)
guard let user = authResult?.user else {
XCTFail("authResult.user is missing")
return
}
XCTAssertEqual(user.refreshToken, kRefreshToken)
XCTAssertFalse(user.isAnonymous)
XCTAssertEqual(user.email, self.kEmail)
guard let additionalUserInfo = authResult?.additionalUserInfo else {
XCTFail("authResult.additionalUserInfo is missing")
return
}
XCTAssertTrue(additionalUserInfo.isNewUser)
XCTAssertEqual(additionalUserInfo.providerID, EmailAuthProvider.id)
XCTAssertNil(error)
completion(user)
}
} catch {
XCTFail("Throw in \(#function): \(error)")
}
}
}