// 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? var limitedUseTokenResult: Result? 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" ) ) 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__." ) } 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(_ 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) } // 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)) } // 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" ) } }