Files
app-check/AppCheckCore/Tests/Unit/DebugProvider/AppCheckCoreDebugProviderTests.swift
T

852 lines
34 KiB
Swift
Raw Normal View History

2026-09-23 15:22:08 -07:00
// 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 XCTest
#if canImport(GoogleUtilities)
import GoogleUtilities
#endif
@testable import AppCheckCore
class MockAppCheckDebugProviderAPIService: NSObject, AppCheckCoreDebugProviderAPIServiceProtocol {
var passedDebugToken: String?
var passedLimitedUse: Bool?
var tokenResult: Result<AppCheckCoreToken, Error>?
var limitedUseTokenResult: Result<AppCheckCoreToken, Error>?
func appCheckToken(debugToken: String, limitedUse: Bool) async throws -> AppCheckCoreToken {
passedDebugToken = debugToken
passedLimitedUse = limitedUse
let result = limitedUse ? limitedUseTokenResult : tokenResult
guard let result = result else {
throw NSError(domain: "MockError", code: -1, userInfo: nil)
}
switch result {
case let .success(token):
return token
case let .failure(error):
throw error
}
}
}
class AppCheckCoreDebugProviderTests: XCTestCase {
let kDebugTokenEnvKey = "AppCheckDebugToken"
let kFirebaseDebugTokenEnvKey = "FIRAAppCheckDebugToken"
let kDebugTokenUserDefaultsKey = AppCheckCoreDebugProvider.debugTokenUserDefaultsKey
let kDebugTokenRegisteredUserDefaultsKey = AppCheckCoreDebugProvider
.debugTokenRegisteredUserDefaultsKeyPrefix
var provider: AppCheckCoreDebugProvider!
var fakeAPIService: MockAppCheckDebugProviderAPIService!
override func setUp() {
super.setUp()
fakeAPIService = MockAppCheckDebugProviderAPIService()
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [:])
}
override func tearDown() {
provider = nil
UserDefaults.standard.removeObject(forKey: kDebugTokenUserDefaultsKey)
UserDefaults.standard.removeObject(forKey: kDebugTokenRegisteredUserDefaultsKey)
// The prefix alone is not a real key; also clear the flag for the suite's default provider.
UserDefaults.standard.removeObject(
forKey: AppCheckCoreDebugProvider.registeredUserDefaultsKey(
forServiceName: "test-service",
resourceName: "projects/test-project/apps/test-app"
)
)
2026-09-23 15:22:08 -07:00
super.tearDown()
}
// MARK: - Debug token generating/storing
// IMMUTABLE TEST: Do not edit, update, or remove under any circumstance.
// This test locks in an immutable backwards compatibility contract with App Check 11.
// If this test fails, revert changes to the implementation instead of modifying this test.
func testDebugTokenUserDefaultsKeys_MatchContract() {
XCTAssertEqual(
AppCheckCoreDebugProvider.debugTokenUserDefaultsKey,
"GACAppCheckDebugToken",
"Debug token key mismatch breaks App Check 11 migration compatibility."
)
XCTAssertEqual(
AppCheckCoreDebugProvider.debugTokenRegisteredUserDefaultsKeyPrefix,
"GACAppCheckDebugTokenRegistered",
"Registered token key prefix mismatch breaks App Check 11 migration compatibility."
)
}
func testLegacyDebugTokenCompatibility() {
let legacyKey = kDebugTokenUserDefaultsKey
let legacyToken = "legacy_stored_debug_token"
UserDefaults.standard.set(legacyToken, forKey: legacyKey)
defer {
UserDefaults.standard.removeObject(forKey: legacyKey)
}
let debugProvider = AppCheckCoreDebugProvider(
apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [:]
)
XCTAssertEqual(
debugProvider.currentDebugToken(),
legacyToken,
"Debug token mismatch breaks App Check 11 migration compatibility."
)
}
func testSetDebugToken_CompatibilityWithLegacyGACStorage() {
let legacyKey = kDebugTokenUserDefaultsKey
UserDefaults.standard.removeObject(forKey: legacyKey)
defer {
UserDefaults.standard.removeObject(forKey: legacyKey)
}
let debugProvider = AppCheckCoreDebugProvider(
apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [:]
)
let generatedToken = debugProvider.currentDebugToken()
let storedLegacyValue = UserDefaults.standard.string(forKey: legacyKey)
XCTAssertEqual(
storedLegacyValue,
generatedToken,
"Generated debug token was not stored under legacy key."
)
}
func testLegacyRegisteredDebugTokenCompatibility() async throws {
let serviceName = "test-service"
let resourceName = "projects/test-project/apps/test-app"
let expectedLegacyRegisteredKey =
"GACAppCheckDebugTokenRegistered_test-service_projects_test-project_apps_test-app"
// 1. Verify key resolution matches legacy v11 format.
let resolvedKey = AppCheckCoreDebugProvider.registeredUserDefaultsKey(
forServiceName: serviceName,
resourceName: resourceName
)
XCTAssertEqual(
resolvedKey,
expectedLegacyRegisteredKey,
"Resolved registered token key does not match legacy v11 format."
)
// 2. Verify write-back persists to UserDefaults using legacy key format.
UserDefaults.standard.removeObject(forKey: expectedLegacyRegisteredKey)
defer {
UserDefaults.standard.removeObject(forKey: expectedLegacyRegisteredKey)
}
let validToken = AppCheckCoreToken(
token: "valid_token",
expirationDate: Date().addingTimeInterval(3600)
)
fakeAPIService.tokenResult = .success(validToken)
let debugProvider = AppCheckCoreDebugProvider(
apiService: fakeAPIService,
serviceName: serviceName,
resourceName: resourceName,
environment: [:]
)
XCTAssertFalse(
UserDefaults.standard.bool(forKey: expectedLegacyRegisteredKey),
"Precondition: Key should not be registered before getToken()."
)
_ = try await debugProvider.getToken()
XCTAssertTrue(
UserDefaults.standard.bool(forKey: expectedLegacyRegisteredKey),
"Registered flag was not stored under legacy key format GACAppCheckDebugTokenRegistered_<service>_<resource>."
)
}
func testCurrentTokenWhenEnvironmentVariableSetAndTokenStored() {
UserDefaults.standard.set("stored token", forKey: kDebugTokenUserDefaultsKey)
let envToken = "env token"
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [kDebugTokenEnvKey: envToken])
XCTAssertEqual(provider.currentDebugToken(), envToken)
}
func testCurrentTokenWhenFirebaseAndCoreEnvironmentVariablesSetAndTokenStored() {
UserDefaults.standard.set("stored token", forKey: kDebugTokenUserDefaultsKey)
let envToken = "env token"
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [
kDebugTokenEnvKey: envToken,
kFirebaseDebugTokenEnvKey: "firebase env token",
])
XCTAssertEqual(provider.currentDebugToken(), envToken)
}
func testCurrentTokenWhenFirebaseEnvironmentVariableSetAndTokenStored() {
UserDefaults.standard.set("stored token", forKey: kDebugTokenUserDefaultsKey)
let envToken = "env token"
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [kFirebaseDebugTokenEnvKey: envToken])
XCTAssertEqual(provider.currentDebugToken(), envToken)
}
func testCurrentTokenWhenFirebaseAndCoreEnvironmentVariablesSet() {
let envToken = "env token"
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [
kDebugTokenEnvKey: envToken,
kFirebaseDebugTokenEnvKey: "firebase env token",
])
XCTAssertEqual(provider.currentDebugToken(), envToken)
}
func testCurrentTokenWhenNoEnvironmentVariableAndTokenStored() {
let storedToken = "stored token"
UserDefaults.standard.set(storedToken, forKey: kDebugTokenUserDefaultsKey)
XCTAssertEqual(provider.currentDebugToken(), storedToken)
XCTAssertEqual(provider.currentDebugToken(), storedToken)
}
func testCurrentTokenWhenNoEnvironmentVariableAndNoTokenStored() {
UserDefaults.standard.removeObject(forKey: kDebugTokenUserDefaultsKey)
XCTAssertNil(UserDefaults.standard.string(forKey: kDebugTokenUserDefaultsKey))
let generatedToken = provider.currentDebugToken()
XCTAssertNotNil(generatedToken)
// Check if the generated token is stored to the user defaults.
XCTAssertEqual(UserDefaults.standard.string(forKey: kDebugTokenUserDefaultsKey), generatedToken)
// Check if the same token is used once generated.
XCTAssertEqual(provider.currentDebugToken(), generatedToken)
}
// MARK: - Debug token source
func testDebugTokenSourceIsLocalWhenNoEnvironmentVariableOrProvidedToken() {
XCTAssertEqual(provider.debugTokenSource, .local)
}
func testDebugTokenSourceIsLocalWhenEmptyDebugTokenProvided() {
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [:],
debugToken: "")
XCTAssertEqual(provider.debugTokenSource, .local)
}
func testDebugTokenSourceIsLocalWhenWhitespaceOnlyDebugTokenProvided() {
let storedToken = "stored token"
UserDefaults.standard.set(storedToken, forKey: kDebugTokenUserDefaultsKey)
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [:],
debugToken: " \n")
XCTAssertEqual(provider.debugTokenSource, .local)
XCTAssertEqual(provider.currentDebugToken(), storedToken)
}
func testCurrentTokenTrimsWhitespaceFromProvidedDebugToken() {
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [:],
debugToken: " provided token\n")
XCTAssertEqual(provider.debugTokenSource, .provided)
XCTAssertEqual(provider.currentDebugToken(), "provided token")
}
func testDebugTokenSourceIsProvidedWhenDebugTokenProvided() {
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [:],
debugToken: "provided token")
XCTAssertEqual(provider.debugTokenSource, .provided)
}
func testDebugTokenSourceIsEnvironmentVariableWhenEnvironmentVariableSet() {
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [kFirebaseDebugTokenEnvKey: "env token"])
XCTAssertEqual(provider.debugTokenSource, .environmentVariable)
}
func testDebugTokenSourceIsEnvironmentVariableWhenTokenAlsoProvided() {
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [kDebugTokenEnvKey: "same token"],
debugToken: "same token")
// An environment variable wins even when it matches the provided token.
XCTAssertEqual(provider.debugTokenSource, .environmentVariable)
}
// MARK: - Provided debug token
func testCurrentTokenWhenDebugTokenProvidedAndNoEnvironmentVariable() {
UserDefaults.standard.removeObject(forKey: kDebugTokenUserDefaultsKey)
let providedToken = "provided token"
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [:],
debugToken: providedToken)
XCTAssertEqual(provider.currentDebugToken(), providedToken)
// The provided token must not be persisted, nor a local token generated in its place.
XCTAssertNil(UserDefaults.standard.string(forKey: kDebugTokenUserDefaultsKey))
}
func testCurrentTokenWhenDebugTokenProvidedAndTokenStored() {
UserDefaults.standard.set("stored token", forKey: kDebugTokenUserDefaultsKey)
let providedToken = "provided token"
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [:],
debugToken: providedToken)
XCTAssertEqual(provider.currentDebugToken(), providedToken)
XCTAssertEqual(provider.localDebugToken(), "stored token")
}
func testCurrentTokenWhenEnvironmentVariableSetAndDebugTokenProvided() {
let envToken = "env token"
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [kDebugTokenEnvKey: envToken],
debugToken: "provided token")
XCTAssertEqual(provider.currentDebugToken(), envToken)
}
func testCurrentTokenWhenFirebaseEnvironmentVariableSetAndDebugTokenProvided() {
let envToken = "firebase env token"
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [kFirebaseDebugTokenEnvKey: envToken],
debugToken: "provided token")
XCTAssertEqual(provider.currentDebugToken(), envToken)
}
func testCurrentTokenWhenEmptyDebugTokenProvidedAndTokenStored() {
let storedToken = "stored token"
UserDefaults.standard.set(storedToken, forKey: kDebugTokenUserDefaultsKey)
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [:],
debugToken: "")
XCTAssertEqual(provider.currentDebugToken(), storedToken)
}
func testCurrentTokenWhenEmptyDebugTokenProvidedAndNoTokenStored() {
UserDefaults.standard.removeObject(forKey: kDebugTokenUserDefaultsKey)
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [:],
debugToken: "")
let generatedToken = provider.currentDebugToken()
XCTAssertFalse(generatedToken.isEmpty)
XCTAssertEqual(UserDefaults.standard.string(forKey: kDebugTokenUserDefaultsKey), generatedToken)
}
func testLocalDebugTokenIsIndependentOfProvidedDebugToken() {
UserDefaults.standard.removeObject(forKey: kDebugTokenUserDefaultsKey)
let providedToken = "provided token"
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [:],
debugToken: providedToken)
let localToken = provider.localDebugToken()
XCTAssertNotEqual(localToken, providedToken)
XCTAssertEqual(UserDefaults.standard.string(forKey: kDebugTokenUserDefaultsKey), localToken)
XCTAssertEqual(provider.currentDebugToken(), providedToken)
}
func testGetTokenSuccessWithProvidedDebugTokenDoesNotSetRegisteredFlag() async throws {
let registeredKey = AppCheckCoreDebugProvider.registeredUserDefaultsKey(
forServiceName: "test-service",
resourceName: "projects/test-project/apps/test-app"
)
UserDefaults.standard.removeObject(forKey: registeredKey)
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [:],
debugToken: "provided token")
fakeAPIService.tokenResult = .success(AppCheckCoreToken(
token: "valid_token",
expirationDate: Date().addingTimeInterval(3600)
))
_ = try await provider.getToken()
// Only the locally generated token is tracked as registered; a provided token that later
// disappears must not suppress the registration instructions for the local token.
XCTAssertNil(UserDefaults.standard.object(forKey: registeredKey))
}
func testGetTokenSuccessWithEnvironmentDebugTokenDoesNotSetRegisteredFlag() async throws {
let registeredKey = AppCheckCoreDebugProvider.registeredUserDefaultsKey(
forServiceName: "test-service",
resourceName: "projects/test-project/apps/test-app"
)
UserDefaults.standard.removeObject(forKey: registeredKey)
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [kDebugTokenEnvKey: "env token"])
fakeAPIService.tokenResult = .success(AppCheckCoreToken(
token: "valid_token",
expirationDate: Date().addingTimeInterval(3600)
))
_ = try await provider.getToken()
XCTAssertNil(UserDefaults.standard.object(forKey: registeredKey))
}
func testGetTokenFailureWithProvidedDebugTokenPreservesRegisteredFlag() async throws {
let registeredKey = AppCheckCoreDebugProvider.registeredUserDefaultsKey(
forServiceName: "test-service",
resourceName: "projects/test-project/apps/test-app"
)
UserDefaults.standard.set(true, forKey: registeredKey)
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [:],
debugToken: "provided token")
fakeAPIService.tokenResult = .failure(NSError(domain: "testDomain", code: -1, userInfo: nil))
await XCTAssertThrowsErrorAsync(try await provider.getToken())
// The local token was not exchanged, so its registration state must be left untouched.
XCTAssertTrue(UserDefaults.standard.bool(forKey: registeredKey))
}
func testGetTokenFailureWithEnvironmentDebugTokenPreservesRegisteredFlag() async throws {
let registeredKey = AppCheckCoreDebugProvider.registeredUserDefaultsKey(
forServiceName: "test-service",
resourceName: "projects/test-project/apps/test-app"
)
UserDefaults.standard.set(true, forKey: registeredKey)
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [kDebugTokenEnvKey: "env token"])
fakeAPIService.tokenResult = .failure(NSError(domain: "testDomain", code: -1, userInfo: nil))
await XCTAssertThrowsErrorAsync(try await provider.getToken())
XCTAssertTrue(UserDefaults.standard.bool(forKey: registeredKey))
}
/// Asserts that `expression` throws, since `XCTAssertThrowsError` does not accept async
/// expressions.
private func XCTAssertThrowsErrorAsync<T>(_ expression: @autoclosure () async throws -> T,
file: StaticString = #filePath,
line: UInt = #line) async {
do {
_ = try await expression()
XCTFail("Expected error to be thrown", file: file, line: line)
} catch {}
}
func testGetTokenUsesProvidedDebugToken() async throws {
let providedToken = "provided token"
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [:],
debugToken: providedToken)
let validToken = AppCheckCoreToken(
token: "valid_token",
expirationDate: Date(),
receivedAt: Date()
)
fakeAPIService.tokenResult = .success(validToken)
let token = try await provider.getToken()
XCTAssertEqual(token.token, validToken.token)
XCTAssertEqual(fakeAPIService.passedDebugToken, providedToken)
XCTAssertEqual(fakeAPIService.passedLimitedUse, false)
}
2026-09-23 15:22:08 -07:00
// MARK: - Debug token to FAC token exchange
func testGetTokenSuccess() async throws {
// 1. Stub API service.
let expectedDebugToken = provider.currentDebugToken()
let validToken = AppCheckCoreToken(
token: "valid_token",
expirationDate: Date(),
receivedAt: Date()
)
fakeAPIService.tokenResult = .success(validToken)
// 2. Validate get token.
let token = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<
AppCheckCoreToken,
Error
>) in
provider.getToken { token, error in
if let error = error {
continuation.resume(throwing: error)
} else if let token = token {
continuation.resume(returning: token)
} else {
continuation.resume(throwing: NSError(domain: "TestError", code: -1, userInfo: nil))
}
}
}
XCTAssertEqual(token.token, validToken.token)
XCTAssertEqual(token.expirationDate, validToken.expirationDate)
XCTAssertEqual(token.receivedAtDate, validToken.receivedAtDate)
// 3. Verify fakes.
XCTAssertEqual(fakeAPIService.passedDebugToken, expectedDebugToken)
XCTAssertEqual(fakeAPIService.passedLimitedUse, false)
}
func testGetTokenAPIError() async throws {
// 1. Stub API service.
let expectedDebugToken = provider.currentDebugToken()
let apiError = NSError(domain: "testGetTokenAPIError", code: -1, userInfo: nil)
fakeAPIService.tokenResult = .failure(apiError)
// 2. Validate get token.
do {
_ = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<
AppCheckCoreToken,
Error
>) in
provider.getToken { token, error in
if let error = error {
continuation.resume(throwing: error)
} else if let token = token {
continuation.resume(returning: token)
} else {
continuation.resume(throwing: NSError(domain: "TestError", code: -1, userInfo: nil))
}
}
}
XCTFail("Expected error to be thrown")
} catch let error as NSError {
XCTAssertEqual(error, apiError)
}
// 3. Verify fakes.
XCTAssertEqual(fakeAPIService.passedDebugToken, expectedDebugToken)
XCTAssertEqual(fakeAPIService.passedLimitedUse, false)
}
func testGetLimitedUseTokenSuccess() async throws {
// 1. Stub API service.
let expectedDebugToken = provider.currentDebugToken()
let validToken = AppCheckCoreToken(
token: "valid_token",
expirationDate: Date(),
receivedAt: Date()
)
fakeAPIService.limitedUseTokenResult = .success(validToken)
// 2. Validate get limited-use token.
let token = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<
AppCheckCoreToken,
Error
>) in
provider.getLimitedUseToken { token, error in
if let error = error {
continuation.resume(throwing: error)
} else if let token = token {
continuation.resume(returning: token)
} else {
continuation.resume(throwing: NSError(domain: "TestError", code: -1, userInfo: nil))
}
}
}
XCTAssertEqual(token.token, validToken.token)
XCTAssertEqual(token.expirationDate, validToken.expirationDate)
XCTAssertEqual(token.receivedAtDate, validToken.receivedAtDate)
// 3. Verify fakes.
XCTAssertEqual(fakeAPIService.passedDebugToken, expectedDebugToken)
XCTAssertEqual(fakeAPIService.passedLimitedUse, true)
}
func testGetLimitedUseTokenAPIError() async throws {
// 1. Stub API service.
let expectedDebugToken = provider.currentDebugToken()
let apiError = NSError(domain: "testGetLimitedUseTokenAPIError", code: -1, userInfo: nil)
fakeAPIService.limitedUseTokenResult = .failure(apiError)
// 2. Validate get limited-use token.
do {
_ = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<
AppCheckCoreToken,
Error
>) in
provider.getLimitedUseToken { token, error in
if let error = error {
continuation.resume(throwing: error)
} else if let token = token {
continuation.resume(returning: token)
} else {
continuation.resume(throwing: NSError(domain: "TestError", code: -1, userInfo: nil))
}
}
}
XCTFail("Expected error to be thrown")
} catch let error as NSError {
XCTAssertEqual(error, apiError)
}
// 3. Verify fakes.
XCTAssertEqual(fakeAPIService.passedDebugToken, expectedDebugToken)
XCTAssertEqual(fakeAPIService.passedLimitedUse, true)
}
func testGetTokenSuccessSetsRegisteredFlag() async throws {
// 1. Stub API service.
let expectedDebugToken = provider.currentDebugToken()
let validToken = AppCheckCoreToken(
token: "valid_token",
expirationDate: Date(),
receivedAt: Date()
)
fakeAPIService.tokenResult = .success(validToken)
// The mirror way to get registeredUserDefaultsKey, or since we know it, we can just hardcode or
// access it
let registeredKey =
"\(kDebugTokenRegisteredUserDefaultsKey)_test-service_projects_test-project_apps_test-app"
UserDefaults.standard.removeObject(forKey: registeredKey)
// 2. Validate get token.
_ = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<
AppCheckCoreToken,
Error
>) in
provider.getToken { token, error in
if let error = error {
continuation.resume(throwing: error)
} else if let token = token {
continuation.resume(returning: token)
} else {
continuation.resume(throwing: NSError(domain: "TestError", code: -1, userInfo: nil))
}
}
}
// 3. Verify flag is now YES.
XCTAssertTrue(UserDefaults.standard.bool(forKey: registeredKey))
// 4. Verify fakes.
XCTAssertEqual(fakeAPIService.passedDebugToken, expectedDebugToken)
XCTAssertEqual(fakeAPIService.passedLimitedUse, false)
}
func testGetTokenPermanentFailureClearsRegisteredFlag() async throws {
// 1. Stub API service.
let expectedDebugToken = provider.currentDebugToken()
let apiError = NSError(
domain: "testGetTokenPermanentFailureClearsRegisteredFlag",
code: -1,
userInfo: nil
)
fakeAPIService.tokenResult = .failure(apiError)
let registeredKey =
"\(kDebugTokenRegisteredUserDefaultsKey)_test-service_projects_test-project_apps_test-app"
UserDefaults.standard.set(true, forKey: registeredKey)
// 2. Validate get token.
do {
_ = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<
AppCheckCoreToken,
Error
>) in
provider.getToken { token, error in
if let error = error {
continuation.resume(throwing: error)
} else if let token = token {
continuation.resume(returning: token)
} else {
continuation.resume(throwing: NSError(domain: "TestError", code: -1, userInfo: nil))
}
}
}
XCTFail("Expected error to be thrown")
} catch _ {
// expected
}
// 3. Verify flag is cleared.
XCTAssertNil(UserDefaults.standard.object(forKey: registeredKey))
// 4. Verify fakes.
XCTAssertEqual(fakeAPIService.passedDebugToken, expectedDebugToken)
XCTAssertEqual(fakeAPIService.passedLimitedUse, false)
}
func testGetTokenNetworkFailureDoesNotClearRegisteredFlag() async throws {
// 1. Stub API service.
let expectedDebugToken = provider.currentDebugToken()
let networkError = NSError(
domain: AppCheckCoreErrorDomain,
code: AppCheckCoreErrorCode.serverUnreachable.rawValue,
userInfo: nil
)
fakeAPIService.tokenResult = .failure(networkError)
let registeredKey =
"\(kDebugTokenRegisteredUserDefaultsKey)_test-service_projects_test-project_apps_test-app"
UserDefaults.standard.set(true, forKey: registeredKey)
// 2. Validate get token.
do {
_ = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<
AppCheckCoreToken,
Error
>) in
provider.getToken { token, error in
if let error = error {
continuation.resume(throwing: error)
} else if let token = token {
continuation.resume(returning: token)
} else {
continuation.resume(throwing: NSError(domain: "TestError", code: -1, userInfo: nil))
}
}
}
XCTFail("Expected error to be thrown")
} catch let error as NSError {
XCTAssertEqual(error, networkError)
}
// 3. Verify flag is still YES.
XCTAssertTrue(UserDefaults.standard.bool(forKey: registeredKey))
// 4. Verify fakes.
XCTAssertEqual(fakeAPIService.passedDebugToken, expectedDebugToken)
XCTAssertEqual(fakeAPIService.passedLimitedUse, false)
}
func testGetTokenWithEnvironmentDebugTokenDoesNotCreateLocalToken() async throws {
// `setUp` creates a provider without an environment variable, which stores a local token.
UserDefaults.standard.removeObject(forKey: kDebugTokenUserDefaultsKey)
provider = AppCheckCoreDebugProvider(apiService: fakeAPIService,
serviceName: "test-service",
resourceName: "projects/test-project/apps/test-app",
environment: [kDebugTokenEnvKey: "env token"])
fakeAPIService.tokenResult = .success(AppCheckCoreToken(
token: "valid_token",
expirationDate: Date().addingTimeInterval(3600)
))
_ = try await provider.getToken()
XCTAssertEqual(fakeAPIService.passedDebugToken, "env token")
// The local token is only generated, stored, and logged when it is the token in use.
XCTAssertNil(UserDefaults.standard.string(forKey: kDebugTokenUserDefaultsKey))
}
2026-09-23 15:22:08 -07:00
// MARK: - Registered UserDefaults key construction
/// Backfilled from the v11 suite
/// (`testRegisteredUserDefaultsKeyForServiceName_resourceName`). This key
/// namespaces the "debug token already registered" flag, so a change to its
/// construction would silently orphan the flag for every existing install and
/// cause a spurious re-registration.
func testRegisteredUserDefaultsKeyForServiceNameResourceName() {
// Slashes in the resource name must be sanitized to underscores.
XCTAssertEqual(
AppCheckCoreDebugProvider.registeredUserDefaultsKey(
forServiceName: "app1", resourceName: "projects/p1/apps/a1"
),
"GACAppCheckDebugTokenRegistered_app1_projects_p1_apps_a1"
)
XCTAssertEqual(
AppCheckCoreDebugProvider.registeredUserDefaultsKey(
forServiceName: "app2", resourceName: "projects/p2/apps/a2"
),
"GACAppCheckDebugTokenRegistered_app2_projects_p2_apps_a2"
)
// Empty components fall back to "default".
//
// Note: v11 also covered `nil` for both parameters. Swift's type system
// makes that unrepresentable here (both are non-optional `String`), so the
// empty-string case is the only reachable fallback.
XCTAssertEqual(
AppCheckCoreDebugProvider.registeredUserDefaultsKey(
forServiceName: "", resourceName: ""
),
"GACAppCheckDebugTokenRegistered_default_default"
)
}
}