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

127 lines
4.8 KiB
Swift

// Copyright 2026 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
/// Concurrency tests for `User` properties that `update(withGetAccountInfoResponse:)` replaces.
///
/// These tests rarely crash on their own, as the race window is a few instructions wide. Run
/// them with Thread Sanitizer enabled to detect unsynchronized access.
/// See https://github.com/firebase/firebase-ios-sdk/issues/16975.
class UserPropertyThreadSafetyTests: XCTestCase {
private let kCreationDate = Date(timeIntervalSince1970: 1_505_858_500)
private let kLastSignInDate = Date(timeIntervalSince1970: 1_505_858_583)
private let kIterations = 2000
/// Reads `metadata` on several threads while `update(withGetAccountInfoResponse:)` replaces it.
func testConcurrentMetadataReadDuringUpdate() throws {
let user = makeUser()
let response = try makeGetAccountInfoResponse()
user.update(withGetAccountInfoResponse: response)
DispatchQueue.concurrentPerform(iterations: 5) { index in
for _ in 0 ..< kIterations {
if index == 0 {
user.update(withGetAccountInfoResponse: response)
} else {
_ = user.metadata.creationDate
}
}
}
XCTAssertEqual(user.metadata.creationDate, kCreationDate)
XCTAssertEqual(user.metadata.lastSignInDate, kLastSignInDate)
}
#if os(iOS) || os(macOS)
/// Reads `multiFactor` on several threads while `update(withGetAccountInfoResponse:)`
/// replaces it.
func testConcurrentMultiFactorReadDuringUpdate() throws {
let user = makeUser()
let response = try makeGetAccountInfoResponse()
user.update(withGetAccountInfoResponse: response)
DispatchQueue.concurrentPerform(iterations: 5) { index in
for _ in 0 ..< kIterations {
if index == 0 {
user.update(withGetAccountInfoResponse: response)
} else {
_ = user.multiFactor.enrolledFactors
}
}
}
XCTAssertEqual(user.multiFactor.enrolledFactors.count, 1)
}
/// Archives the user, as `Auth` does when it saves the user to the keychain, while
/// `update(withGetAccountInfoResponse:)` runs. `encode(with:)` reads `multiFactor` while it
/// holds `propertyAccessQueue`.
func testConcurrentArchiveDuringUpdate() throws {
let user = makeUser()
let response = try makeGetAccountInfoResponse()
user.update(withGetAccountInfoResponse: response)
DispatchQueue.concurrentPerform(iterations: 2) { index in
for _ in 0 ..< kIterations / 4 {
if index == 0 {
user.update(withGetAccountInfoResponse: response)
} else {
_ = try? NSKeyedArchiver.archivedData(withRootObject: user,
requiringSecureCoding: true)
}
}
}
let data = try NSKeyedArchiver.archivedData(withRootObject: user,
requiringSecureCoding: true)
let unarchived = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(ofClass: User.self,
from: data))
XCTAssertEqual(unarchived.metadata.creationDate, kCreationDate)
XCTAssertEqual(unarchived.multiFactor.enrolledFactors.count, 1)
}
#endif
// MARK: - Helpers
private func makeUser() -> User {
let tokenService = SecureTokenService(
withRequestConfiguration: nil,
accessToken: "accessToken",
accessTokenExpirationDate: Date().addingTimeInterval(3600),
refreshToken: "refreshToken"
)
return User(withTokenService: tokenService,
backend: AuthBackend(rpcIssuer: AuthBackendRPCIssuer()))
}
private func makeGetAccountInfoResponse() throws -> GetAccountInfoResponse {
let mfaInfo: [[String: AnyHashable]] = [[
"phoneInfo": "+16505551234",
"mfaEnrollmentId": "enrollmentID",
"displayName": "phone",
]]
let userInfo: [String: AnyHashable] = [
"localId": "localID",
"createdAt": String(Int(kCreationDate.timeIntervalSince1970 * 1000)),
"lastLoginAt": String(Int(kLastSignInDate.timeIntervalSince1970 * 1000)),
"mfaInfo": mfaInfo,
]
return try GetAccountInfoResponse(dictionary: ["users": [userInfo]])
}
}