feat(appcheck): read CLI debug token on Simulator (#17017)

Co-authored-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com>

In a Simulator, AppCheckDebugProvider now uses the debug token that
`firebase appcheck:debugtokens:create` saves to
~/.config/configstore/firebase-appcheck.json on the host, so apps
launched from Xcode work without manually registering a token. The
token is looked up by each app's Google App ID through
SIMULATOR_HOST_HOME. The lookup is compiled out on devices, macOS and
Mac Catalyst. No public API changes.

Debug tokens are now resolved in this order:
1. AppCheckDebugToken or FIRAAppCheckDebugToken environment variable
2. Token from the CLI config store (Simulator only)
3. Token stored in UserDefaults
4. Newly generated UUID

The config store comes before the stored token so a newly provisioned
token takes effect without reinstalling the app.

Logging:
- I-FAA004003: debug token loaded from the config store.
- I-FAA004004: exchanging the config store token failed; suggests
  re-running the CLI with --force and relaunching.
- I-FAA004002: exchanging an environment variable token failed; now
  names the variables instead of showing an unrelated local token.
- I-FAA004005: the config store exists but can't be read or parsed.
A missing file, or a path where it can't exist (e.g., ~/.config is a
regular file), is silent. Exchange-failure warnings are skipped when
the server is unreachable, and no message includes the token.
This commit is contained in:
Andrew Heard authored and GitHub committed 2026-11-06 19:23:51 -05:00
1 parent 509b9a3888
commit c06a1d7199
11 files changed
+1027 -14

No files matched your search

+1 -1
View File
@@ -44,7 +44,7 @@ Pod::Spec.new do |s|
s.osx.weak_framework = 'DeviceCheck'
s.tvos.weak_framework = 'DeviceCheck'
s.dependency 'AppCheckCore', '~> 12.0'
s.dependency 'AppCheckCore', '~> 12.1'
s.dependency 'FirebaseAppCheckInterop', '~> 13.2.0'
s.dependency 'FirebaseCore', '~> 13.2.0'
s.dependency 'GoogleUtilities/Environment', '>= 8.1.3', '< 9.0'
+11
View File
@@ -1,3 +1,14 @@
# 13.2.0
- [added] `AppCheckDebugProvider` now automatically uses a debug token
provisioned by the Firebase CLI (`firebase appcheck:debugtokens:create`) when
running in a Simulator. The token is read for the app's Google App ID from
`~/.config/configstore/firebase-appcheck.json` on the host machine, so no
manual token registration is needed. The `AppCheckDebugToken` and
`FIRAAppCheckDebugToken` environment variables still take priority. (#17017)
- [fixed] When the `AppCheckDebugToken` or `FIRAAppCheckDebugToken` environment
variable is set, `AppCheckDebugProvider` no longer generates an unused local
debug token and logs registration instructions for it. (#17017)
# 12.19.0
- [fixed] Fixed an issue where the Recaptcha provider could cause build
errors in binary environments, due to unsupported platforms. (#16564)
@@ -35,10 +35,13 @@ FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeProviderIsMissing
// FIRAppCheckDebugProvider.m
FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageDebugProviderIncompleteFIROptions;
FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeDebugTokenExchangeFailed;
// FIRAppCheckDebugProviderFactory.m
FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeDebugTokenFromConfigStore;
FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeConfigStoreDebugTokenExchangeFailed;
FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeDebugToken;
// FIRAppCheckDebugTokenConfigStore.m
FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeDebugTokenConfigStoreUnreadable;
// FIRDeviceCheckProvider.m
FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageDeviceCheckProviderIncompleteFIROptions;
@@ -29,10 +29,13 @@ NSString *const kFIRLoggerAppCheckMessageCodeProviderIsMissing = @"I-FAA002002";
// FIRAppCheckDebugProvider.m
NSString *const kFIRLoggerAppCheckMessageDebugProviderIncompleteFIROptions = @"I-FAA004001";
NSString *const kFIRLoggerAppCheckMessageCodeDebugTokenExchangeFailed = @"I-FAA004002";
// FIRAppCheckDebugProviderFactory.m
NSString *const kFIRLoggerAppCheckMessageCodeDebugTokenFromConfigStore = @"I-FAA004003";
NSString *const kFIRLoggerAppCheckMessageCodeConfigStoreDebugTokenExchangeFailed = @"I-FAA004004";
NSString *const kFIRLoggerAppCheckMessageCodeDebugToken = @"I-FAA005001";
// FIRAppCheckDebugTokenConfigStore.m
NSString *const kFIRLoggerAppCheckMessageCodeDebugTokenConfigStoreUnreadable = @"I-FAA004005";
// FIRDeviceCheckProvider.m
NSString *const kFIRLoggerAppCheckMessageDeviceCheckProviderIncompleteFIROptions = @"I-FAA006001";
@@ -29,6 +29,7 @@
#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h"
#import "FirebaseAppCheck/Sources/Core/FIRAppCheckValidator.h"
#import "FirebaseAppCheck/Sources/Core/FIRHeartbeatLogger+AppCheck.h"
#import "FirebaseAppCheck/Sources/DebugProvider/FIRAppCheckDebugTokenConfigStore.h"
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
@@ -39,6 +40,12 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic, readonly) GACAppCheckDebugProvider *debugProvider;
@property(nonatomic, readonly, copy, nullable) NSString *projectID;
@property(nonatomic, readonly, copy) NSString *googleAppID;
/// Where `debugProvider` got the debug token it exchanges.
///
/// The only token `-initWithApp:` passes to `debugProvider` is one read from the Firebase CLI
/// config store on the host machine, so `GACAppCheckDebugTokenSourceProvided` means that token is
/// in use.
@property(nonatomic, readonly) GACAppCheckDebugTokenSource debugTokenSource;
@end
@@ -52,8 +59,22 @@ NS_ASSUME_NONNULL_BEGIN
_debugProvider = debugProvider;
_projectID = [projectID copy];
_googleAppID = [googleAppID copy];
_debugTokenSource = debugProvider.debugTokenSource;
[self logDebugTokenMessageWithMessageCode:kFIRLoggerAppCheckMessageCodeDebugToken prefix:@""];
switch (_debugTokenSource) { // No `default:` so `-Wswitch` flags new AppCheckCore sources.
case GACAppCheckDebugTokenSourceLocal:
[self logDebugTokenMessageWithMessageCode:kFIRLoggerAppCheckMessageCodeDebugToken
prefix:@""];
break;
case GACAppCheckDebugTokenSourceProvided:
FIRLogNotice(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeDebugTokenFromConfigStore,
@"Debug token loaded from ~/%@.",
FIRAppCheckDebugTokenConfigStore.relativePath);
break;
case GACAppCheckDebugTokenSourceEnvironmentVariable:
// App Check Core logs that the environment variable is used; the local token is unused.
break;
}
}
return self;
}
@@ -69,12 +90,18 @@ NS_ASSUME_NONNULL_BEGIN
return nil;
}
// In a Simulator, use the debug token provisioned by the Firebase CLI on the host machine (if
// any) instead of generating a new one that would need to be registered manually.
NSString *configStoreDebugToken =
[FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:app.options.googleAppID];
GACAppCheckDebugProvider *debugProvider =
[[GACAppCheckDebugProvider alloc] initWithServiceName:app.name
resourceName:app.resourceName
baseURL:nil
APIKey:app.options.APIKey
requestHooks:@[ [app.heartbeatLogger requestHook] ]];
requestHooks:@[ [app.heartbeatLogger requestHook] ]
debugToken:configStoreDebugToken];
return [self initWithDebugProvider:debugProvider
projectID:app.options.projectID
@@ -119,9 +146,57 @@ NS_ASSUME_NONNULL_BEGIN
}
}
- (void)logDebugTokenExchangeError {
[self logDebugTokenMessageWithMessageCode:kFIRLoggerAppCheckMessageCodeDebugTokenExchangeFailed
prefix:@"Failed to exchange debug token. "];
- (void)logDebugTokenExchangeError:(NSError *)error {
if (self.debugTokenSource == GACAppCheckDebugTokenSourceLocal) {
[self logDebugTokenMessageWithMessageCode:kFIRLoggerAppCheckMessageCodeDebugTokenExchangeFailed
prefix:@"Failed to exchange debug token. "];
return;
}
NSString *message = [self debugTokenExchangeFailureMessageForError:error];
if (!message) {
return;
}
NSString *messageCode = self.debugTokenSource == GACAppCheckDebugTokenSourceProvided
? kFIRLoggerAppCheckMessageCodeConfigStoreDebugTokenExchangeFailed
: kFIRLoggerAppCheckMessageCodeDebugTokenExchangeFailed;
FIRLogWarning(kFIRLoggerAppCheck, messageCode, @"%@", message);
}
/// Returns the message to log when exchanging an environment variable or config store debug token
/// fails, or `nil` when there is nothing actionable to report (e.g., the server was unreachable).
///
/// Unlike the local token's registration instructions, these messages never include the token.
- (nullable NSString *)debugTokenExchangeFailureMessageForError:(NSError *)error {
if ([error.domain isEqualToString:GACAppCheckErrors.errorDomain] &&
error.code == GACAppCheckErrorCodeServerUnreachable) {
return nil;
}
NSString *projectArgument = self.projectID.length > 0
? [NSString stringWithFormat:@" --project %@", self.projectID]
: @"";
// App Check Core errors describe the failure in `NSLocalizedFailureReasonErrorKey`; using it
// directly avoids Foundation's generic "The operation couldn’t be completed." prefix.
NSString *errorDescription = error.localizedFailureReason ?: error.localizedDescription;
if (self.debugTokenSource == GACAppCheckDebugTokenSourceProvided) {
// The config store is only read at launch, so a new token requires relaunching the app.
return [NSString stringWithFormat:@"Failed to exchange the debug token loaded from ~/%@: %@\n\n"
@"If the token was rejected by the server, run "
@"`firebase appcheck:debugtokens:create --force%@ --app %@` "
@"to provision a new token, then relaunch the app.",
FIRAppCheckDebugTokenConfigStore.relativePath,
errorDescription, projectArgument, self.googleAppID];
}
if (self.debugTokenSource == GACAppCheckDebugTokenSourceEnvironmentVariable) {
return [NSString
stringWithFormat:@"Failed to exchange the debug token from the `AppCheckDebugToken` or "
@"`FIRAAppCheckDebugToken` environment variable: %@\n\n"
@"Make sure the token is registered for app %@, for example with "
@"`firebase appcheck:debugtokens:create <token>%@ --app %@`.",
errorDescription, self.googleAppID, projectArgument, self.googleAppID];
}
return nil;
}
#pragma mark - FIRAppCheckProvider
@@ -131,7 +206,7 @@ NS_ASSUME_NONNULL_BEGIN
[self.debugProvider getTokenWithCompletion:^(GACAppCheckToken *_Nullable internalToken,
NSError *_Nullable error) {
if (error) {
[self logDebugTokenExchangeError];
[self logDebugTokenExchangeError:error];
handler(nil, error);
return;
}
@@ -145,7 +220,7 @@ NS_ASSUME_NONNULL_BEGIN
[self.debugProvider getLimitedUseTokenWithCompletion:^(GACAppCheckToken *_Nullable internalToken,
NSError *_Nullable error) {
if (error) {
[self logDebugTokenExchangeError];
[self logDebugTokenExchangeError:error];
handler(nil, error);
return;
}
@@ -0,0 +1,51 @@
/*
* 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/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// Reads App Check debug tokens provisioned by the Firebase CLI
/// (`firebase appcheck:debugtokens:create`) into the config store file on the developer's host
/// machine (`~/.config/configstore/firebase-appcheck.json`).
///
/// Only apps running in a Simulator (iOS, tvOS, watchOS, or visionOS) can reach the host file
/// system, using the `SIMULATOR_HOST_HOME` environment variable that the Simulator sets to the
/// host user's home directory. On devices, macOS, and Mac Catalyst the lookup is compiled out and
/// always returns `nil`.
@interface FIRAppCheckDebugTokenConfigStore : NSObject
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
/// The path of the config store file, relative to the host user's home directory.
@property(class, nonatomic, readonly) NSString *relativePath;
/// Returns the debug token provisioned for the given app, or `nil` when not running in a
/// Simulator, when `SIMULATOR_HOST_HOME` is unset, or when the config store file is missing,
/// unreadable, or contains no token for the app.
///
/// @param googleAppID The Google App ID of the `FirebaseApp` to look up.
+ (nullable NSString *)debugTokenForGoogleAppID:(NSString *)googleAppID;
/// Variant of `+debugTokenForGoogleAppID:` that reads `SIMULATOR_HOST_HOME` from the given
/// environment rather than from the current process. Exposed for testing.
+ (nullable NSString *)debugTokenForGoogleAppID:(NSString *)googleAppID
environment:(NSDictionary<NSString *, NSString *> *)environment;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,124 @@
/*
* 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 "FirebaseAppCheck/Sources/DebugProvider/FIRAppCheckDebugTokenConfigStore.h"
#import <TargetConditionals.h>
#import <errno.h>
#import "FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h"
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
NS_ASSUME_NONNULL_BEGIN
#if TARGET_OS_SIMULATOR
/// Environment variable set by the Simulator to the host user's home directory.
static NSString *const kSimulatorHostHomeEnvKey = @"SIMULATOR_HOST_HOME";
/// Top-level key of the config store file mapping Google App IDs to debug tokens.
static NSString *const kTokensKey = @"tokens";
#endif // TARGET_OS_SIMULATOR
@implementation FIRAppCheckDebugTokenConfigStore
+ (NSString *)relativePath {
return @".config/configstore/firebase-appcheck.json";
}
#if TARGET_OS_SIMULATOR
/// Returns whether `error`, from reading the config store file, means that the file does not exist.
///
/// Besides a missing file, this includes a path component that is a regular file rather than a
/// directory (e.g., `~/.config`), since the config store cannot exist at that path either.
+ (BOOL)isMissingConfigStoreError:(nullable NSError *)error {
if ([error.domain isEqualToString:NSCocoaErrorDomain] &&
error.code == NSFileReadNoSuchFileError) {
return YES;
}
NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey];
if (![underlyingError isKindOfClass:[NSError class]]) {
return NO;
}
return [underlyingError.domain isEqualToString:NSPOSIXErrorDomain] &&
underlyingError.code == ENOTDIR;
}
#endif // TARGET_OS_SIMULATOR
+ (nullable NSString *)debugTokenForGoogleAppID:(NSString *)googleAppID {
return [self debugTokenForGoogleAppID:googleAppID
environment:[NSProcessInfo processInfo].environment];
}
+ (nullable NSString *)debugTokenForGoogleAppID:(NSString *)googleAppID
environment:
(NSDictionary<NSString *, NSString *> *)environment {
#if TARGET_OS_SIMULATOR
NSString *hostHome = environment[kSimulatorHostHomeEnvKey];
if (hostHome.length == 0 || googleAppID.length == 0) {
return nil;
}
NSURL *fileURL = [[NSURL fileURLWithPath:hostHome
isDirectory:YES] URLByAppendingPathComponent:self.relativePath
isDirectory:NO];
NSError *readError;
NSData *data = [NSData dataWithContentsOfURL:fileURL options:0 error:&readError];
if (!data) {
// A missing file (or a path where it cannot exist) means the Firebase CLI flow is not in use.
if (![self isMissingConfigStoreError:readError]) {
FIRLogWarning(kFIRLoggerAppCheck,
kFIRLoggerAppCheckMessageCodeDebugTokenConfigStoreUnreadable,
@"Failed to read App Check debug tokens from ~/%@: %@", self.relativePath,
readError.localizedDescription);
}
return nil;
}
NSError *parseError;
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
if (![json isKindOfClass:[NSDictionary class]]) {
FIRLogWarning(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeDebugTokenConfigStoreUnreadable,
@"Failed to parse App Check debug tokens from ~/%@: %@", self.relativePath,
parseError.localizedDescription ?: @"expected a JSON object");
return nil;
}
NSDictionary *tokens = json[kTokensKey];
if (!tokens) {
// A config store without `tokens` (e.g., `{}` after the last token was removed) is valid.
return nil;
}
if (![tokens isKindOfClass:[NSDictionary class]]) {
FIRLogWarning(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeDebugTokenConfigStoreUnreadable,
@"Failed to parse App Check debug tokens from ~/%@: `%@` is not a JSON object",
self.relativePath, kTokensKey);
return nil;
}
NSString *token = tokens[googleAppID];
if (![token isKindOfClass:[NSString class]] || token.length == 0) {
return nil;
}
return token;
#else
return nil;
#endif // TARGET_OS_SIMULATOR
}
@end
NS_ASSUME_NONNULL_END
@@ -47,6 +47,11 @@ NS_ASSUME_NONNULL_BEGIN
/// Once the debug token is registered the debug provider will be able to provide a valid Firebase
/// App Check token.
///
/// Alternatively, provision a debug token with the Firebase CLI
/// (`firebase appcheck:debugtokens:create --app <app-id>`), which registers it and saves it to
/// `~/.config/configstore/firebase-appcheck.json`. Apps launched from Xcode in a Simulator
/// pick up the saved token automatically, so no manual registration is needed.
///
/// To use `AppCheckDebugProvider` on a simulator on a build server:
/// 1. Create a new Firebase App Check debug token in the Firebase console
/// 2. Add the debug token to the secure storage of your build environment. E.g. see [Encrypted
@@ -73,6 +78,9 @@ NS_SWIFT_NAME(AppCheckDebugProvider)
/** Returns the currently used App Check debug token. The priority:
* - `AppCheckDebugToken` env variable value
* - `FIRAAppCheckDebugToken` env variable value (deprecated)
* - Simulator only: the token for the app's Google App ID in
* `~/.config/configstore/firebase-appcheck.json` on the host machine, as
* provisioned by `firebase appcheck:debugtokens:create`
* - A previously generated token, stored locally on the device
* - A newly generated random token. The generated token will be stored
* locally for future use
@@ -14,6 +14,7 @@
* limitations under the License.
*/
#import <TargetConditionals.h>
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
@@ -21,6 +22,7 @@
#import "FirebaseAppCheck/Sources/Core/FIRApp+AppCheck.h"
#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h"
#import "FirebaseAppCheck/Sources/Core/FIRHeartbeatLogger+AppCheck.h"
#import "FirebaseAppCheck/Sources/DebugProvider/FIRAppCheckDebugTokenConfigStore.h"
#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckDebugProvider.h"
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
@@ -36,8 +38,11 @@ static NSString *const kProjectNumber = @"123456789";
- (instancetype)initWithDebugProvider:(GACAppCheckDebugProvider *)debugProvider
projectID:(nullable NSString *)projectID
googleAppID:(NSString *)googleAppID;
- (nullable NSString *)debugTokenExchangeFailureMessageForError:(NSError *)error;
@property(nonatomic, readonly, copy, nullable) NSString *projectID;
@property(nonatomic, readonly, copy) NSString *googleAppID;
@property(nonatomic, readonly) GACAppCheckDebugTokenSource debugTokenSource;
@end
@@ -45,6 +50,8 @@ static NSString *const kProjectNumber = @"123456789";
@property(nonatomic, copy) NSString *resourceName;
@property(nonatomic) id debugProviderMock;
/// Returned by `debugProviderMock` whenever `debugTokenSource` is called.
@property(nonatomic) GACAppCheckDebugTokenSource stubbedDebugTokenSource;
@property(nonatomic) FIRAppCheckDebugProvider *provider;
@end
@@ -54,6 +61,15 @@ static NSString *const kProjectNumber = @"123456789";
- (void)setUp {
self.resourceName = [NSString stringWithFormat:@"projects/%@/apps/%@", kProjectID, kAppID];
self.debugProviderMock = OCMStrictClassMock([GACAppCheckDebugProvider class]);
// A single stub that reads `stubbedDebugTokenSource`, since OCMock uses the first matching stub:
// a later `OCMStub` or `OCMExpect` for `debugTokenSource` would be shadowed by this one. Set
// `stubbedDebugTokenSource` instead.
__weak __typeof__(self) weakSelf = self;
OCMStub([self.debugProviderMock debugTokenSource]).andDo(^(NSInvocation *invocation) {
GACAppCheckDebugTokenSource source = weakSelf.stubbedDebugTokenSource;
[invocation setReturnValue:&source];
});
self.stubbedDebugTokenSource = GACAppCheckDebugTokenSourceLocal;
self.provider = [[FIRAppCheckDebugProvider alloc] initWithDebugProvider:self.debugProviderMock
projectID:nil
googleAppID:kAppID];
@@ -84,6 +100,7 @@ static NSString *const kProjectNumber = @"123456789";
- (void)testInitWithDebugProvider {
XCTAssertNil(self.provider.projectID);
XCTAssertEqualObjects(self.provider.googleAppID, kAppID);
XCTAssertEqual(self.provider.debugTokenSource, GACAppCheckDebugTokenSourceLocal);
}
- (void)testInitWithIncompleteApp {
@@ -103,6 +120,241 @@ static NSString *const kProjectNumber = @"123456789";
XCTAssertNil([[FIRAppCheckDebugProvider alloc] initWithApp:missingProjectIDApp]);
}
#pragma mark - Debug token source
- (void)testInitWithConfigStoreDebugTokenLogsAttribution {
self.stubbedDebugTokenSource = GACAppCheckDebugTokenSourceProvided;
FIRAppCheckDebugProvider *provider =
[[FIRAppCheckDebugProvider alloc] initWithDebugProvider:self.debugProviderMock
projectID:kProjectID
googleAppID:kAppID];
XCTAssertEqual(provider.debugTokenSource, GACAppCheckDebugTokenSourceProvided);
// `localDebugToken` is never stubbed, so the strict mock fails if the local token is logged.
OCMVerifyAll(self.debugProviderMock);
}
- (void)testInitWithEnvironmentVariableDebugTokenDoesNotLogLocalToken {
self.stubbedDebugTokenSource = GACAppCheckDebugTokenSourceEnvironmentVariable;
FIRAppCheckDebugProvider *provider =
[[FIRAppCheckDebugProvider alloc] initWithDebugProvider:self.debugProviderMock
projectID:kProjectID
googleAppID:kAppID];
XCTAssertEqual(provider.debugTokenSource, GACAppCheckDebugTokenSourceEnvironmentVariable);
// `localDebugToken` is never stubbed, so the strict mock fails if an unused local token is
// generated or logged.
OCMVerifyAll(self.debugProviderMock);
}
- (void)testInitWithLocalDebugTokenLogsLocalToken {
self.stubbedDebugTokenSource = GACAppCheckDebugTokenSourceLocal;
OCMExpect([self.debugProviderMock localDebugToken]).andReturn(@"local_token");
FIRAppCheckDebugProvider *provider =
[[FIRAppCheckDebugProvider alloc] initWithDebugProvider:self.debugProviderMock
projectID:kProjectID
googleAppID:kAppID];
XCTAssertEqual(provider.debugTokenSource, GACAppCheckDebugTokenSourceLocal);
OCMVerifyAll(self.debugProviderMock);
}
- (void)testGetTokenAPIErrorWithConfigStoreDebugTokenDoesNotLogLocalToken {
FIRAppCheckDebugProvider *provider =
[self providerWithDebugTokenSource:GACAppCheckDebugTokenSourceProvided projectID:kProjectID];
NSError *expectedError = [NSError errorWithDomain:@"testGetTokenAPIError" code:-1 userInfo:nil];
OCMExpect([self.debugProviderMock
getTokenWithCompletion:([OCMArg invokeBlockWithArgs:[NSNull null], expectedError, nil])]);
__block BOOL handlerCalled = NO;
[provider getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) {
handlerCalled = YES;
XCTAssertNil(token);
XCTAssertEqualObjects(error, expectedError);
}];
XCTAssertTrue(handlerCalled);
// `localDebugToken` is never stubbed, so the strict mock fails if the fallback message is logged.
OCMVerifyAll(self.debugProviderMock);
}
- (void)testGetLimitedUseTokenAPIErrorWithConfigStoreDebugTokenDoesNotLogLocalToken {
FIRAppCheckDebugProvider *provider =
[self providerWithDebugTokenSource:GACAppCheckDebugTokenSourceProvided projectID:kProjectID];
NSError *expectedError = [NSError errorWithDomain:@"testGetLimitedUseTokenAPIError"
code:-1
userInfo:nil];
OCMExpect([self.debugProviderMock
getLimitedUseTokenWithCompletion:([OCMArg invokeBlockWithArgs:[NSNull null], expectedError,
nil])]);
__block BOOL handlerCalled = NO;
[provider getLimitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token,
NSError *_Nullable error) {
handlerCalled = YES;
XCTAssertNil(token);
XCTAssertEqualObjects(error, expectedError);
}];
XCTAssertTrue(handlerCalled);
// `localDebugToken` is never stubbed, so the strict mock fails if the fallback message is logged.
OCMVerifyAll(self.debugProviderMock);
}
- (void)testGetTokenAPIErrorWithEnvironmentVariableDebugTokenDoesNotLogLocalToken {
FIRAppCheckDebugProvider *provider =
[self providerWithDebugTokenSource:GACAppCheckDebugTokenSourceEnvironmentVariable
projectID:kProjectID];
NSError *expectedError = [NSError errorWithDomain:@"testGetTokenAPIError" code:-1 userInfo:nil];
OCMExpect([self.debugProviderMock
getTokenWithCompletion:([OCMArg invokeBlockWithArgs:[NSNull null], expectedError, nil])]);
__block BOOL handlerCalled = NO;
[provider getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) {
handlerCalled = YES;
XCTAssertNil(token);
XCTAssertEqualObjects(error, expectedError);
}];
XCTAssertTrue(handlerCalled);
// `localDebugToken` is never stubbed, so the strict mock fails if the fallback message is logged.
OCMVerifyAll(self.debugProviderMock);
}
#pragma mark - Exchange failure messages
- (void)testConfigStoreExchangeFailureMessageSuggestsForceCreateAndRelaunch {
FIRAppCheckDebugProvider *provider =
[self providerWithDebugTokenSource:GACAppCheckDebugTokenSourceProvided projectID:kProjectID];
NSError *error = [NSError errorWithDomain:@"testDomain"
code:-1
userInfo:@{NSLocalizedDescriptionKey : @"Permission denied"}];
NSString *message = [provider debugTokenExchangeFailureMessageForError:error];
XCTAssertTrue([message containsString:@"~/.config/configstore/firebase-appcheck.json"]);
XCTAssertTrue([message containsString:@"Permission denied"]);
NSString *expectedCommand =
[NSString stringWithFormat:@"`firebase appcheck:debugtokens:create --force --project %@ "
@"--app %@`",
kProjectID, kAppID];
XCTAssertTrue([message containsString:expectedCommand], @"%@", message);
XCTAssertTrue([message containsString:@"relaunch the app"], @"%@", message);
}
- (void)testConfigStoreExchangeFailureMessageOmitsMissingProjectID {
FIRAppCheckDebugProvider *provider =
[self providerWithDebugTokenSource:GACAppCheckDebugTokenSourceProvided projectID:nil];
NSError *error = [NSError errorWithDomain:@"testDomain" code:-1 userInfo:nil];
NSString *message = [provider debugTokenExchangeFailureMessageForError:error];
XCTAssertFalse([message containsString:@"--project"], @"%@", message);
XCTAssertFalse([message containsString:@"(null)"], @"%@", message);
NSString *expectedCommand = [NSString
stringWithFormat:@"`firebase appcheck:debugtokens:create --force --app %@`", kAppID];
XCTAssertTrue([message containsString:expectedCommand], @"%@", message);
}
- (void)testConfigStoreExchangeFailureMessagePrefersFailureReason {
FIRAppCheckDebugProvider *provider =
[self providerWithDebugTokenSource:GACAppCheckDebugTokenSourceProvided projectID:kProjectID];
NSError *error = [NSError
errorWithDomain:@"testDomain"
code:-1
userInfo:@{NSLocalizedFailureReasonErrorKey : @"The server responded with an error"}];
NSString *message = [provider debugTokenExchangeFailureMessageForError:error];
XCTAssertTrue([message containsString:@": The server responded with an error"], @"%@", message);
XCTAssertFalse([message containsString:@"couldn’t be completed"], @"%@", message);
}
- (void)testEnvironmentVariableExchangeFailureMessageNamesVariablesWithoutForce {
FIRAppCheckDebugProvider *provider =
[self providerWithDebugTokenSource:GACAppCheckDebugTokenSourceEnvironmentVariable
projectID:kProjectID];
NSError *error = [NSError errorWithDomain:@"testDomain"
code:-1
userInfo:@{NSLocalizedDescriptionKey : @"Permission denied"}];
NSString *message = [provider debugTokenExchangeFailureMessageForError:error];
XCTAssertTrue([message containsString:@"`AppCheckDebugToken`"], @"%@", message);
XCTAssertTrue([message containsString:@"`FIRAAppCheckDebugToken`"], @"%@", message);
XCTAssertTrue([message containsString:@"Permission denied"], @"%@", message);
NSString *expectedCommand =
[NSString stringWithFormat:@"`firebase appcheck:debugtokens:create <token> --project %@ "
@"--app %@`",
kProjectID, kAppID];
XCTAssertTrue([message containsString:expectedCommand], @"%@", message);
// Provisioning a new config store token would not help while the environment variable wins.
XCTAssertFalse([message containsString:@"--force"], @"%@", message);
XCTAssertFalse([message containsString:@"firebase-appcheck.json"], @"%@", message);
}
- (void)testExchangeFailureMessageIsNilWhenServerUnreachable {
NSError *error = [NSError errorWithDomain:GACAppCheckErrors.errorDomain
code:GACAppCheckErrorCodeServerUnreachable
userInfo:nil];
FIRAppCheckDebugProvider *configStoreProvider =
[self providerWithDebugTokenSource:GACAppCheckDebugTokenSourceProvided projectID:kProjectID];
FIRAppCheckDebugProvider *environmentVariableProvider =
[self providerWithDebugTokenSource:GACAppCheckDebugTokenSourceEnvironmentVariable
projectID:kProjectID];
NSString *configStoreMessage =
[configStoreProvider debugTokenExchangeFailureMessageForError:error];
NSString *environmentVariableMessage =
[environmentVariableProvider debugTokenExchangeFailureMessageForError:error];
XCTAssertNil(configStoreMessage);
XCTAssertNil(environmentVariableMessage);
}
#if TARGET_OS_SIMULATOR
/// Exercises `-initWithApp:` end to end, without mocks: config store lookup by each app's Google
/// App ID, through to the token used by the App Check Core debug provider.
- (void)testInitWithAppUsesConfigStoreDebugTokenForEachApp {
NSString *otherAppID = @"other_test_app_id";
NSURL *hostHomeURL = [self temporaryHostHomeWithTokens:@{
kAppID : @"config_store_token",
otherAppID : @"other_config_store_token",
}];
[self setEnvironmentVariable:@"SIMULATOR_HOST_HOME" value:hostHomeURL.path];
[self setEnvironmentVariable:@"AppCheckDebugToken" value:nil];
[self setEnvironmentVariable:@"FIRAAppCheckDebugToken" value:nil];
FIRApp *app = [self appWithName:kAppName googleAppID:kAppID];
FIRApp *otherApp = [self appWithName:@"other_test_app_name" googleAppID:otherAppID];
FIRAppCheckDebugProvider *provider = [[FIRAppCheckDebugProvider alloc] initWithApp:app];
FIRAppCheckDebugProvider *otherProvider = [[FIRAppCheckDebugProvider alloc] initWithApp:otherApp];
XCTAssertEqualObjects([provider currentDebugToken], @"config_store_token");
XCTAssertEqual(provider.debugTokenSource, GACAppCheckDebugTokenSourceProvided);
XCTAssertEqualObjects([otherProvider currentDebugToken], @"other_config_store_token");
XCTAssertEqual(otherProvider.debugTokenSource, GACAppCheckDebugTokenSourceProvided);
}
/// An environment variable wins over the config store even when both hold the same token, so the
/// token must not be attributed to the config store.
- (void)testInitWithAppAttributesMatchingEnvironmentVariableToEnvironment {
NSURL *hostHomeURL = [self temporaryHostHomeWithTokens:@{kAppID : @"same_token"}];
[self setEnvironmentVariable:@"SIMULATOR_HOST_HOME" value:hostHomeURL.path];
[self setEnvironmentVariable:@"AppCheckDebugToken" value:@"same_token"];
[self setEnvironmentVariable:@"FIRAAppCheckDebugToken" value:nil];
FIRApp *app = [self appWithName:kAppName googleAppID:kAppID];
FIRAppCheckDebugProvider *provider = [[FIRAppCheckDebugProvider alloc] initWithApp:app];
XCTAssertEqualObjects([provider currentDebugToken], @"same_token");
XCTAssertEqual(provider.debugTokenSource, GACAppCheckDebugTokenSourceEnvironmentVariable);
}
#endif // TARGET_OS_SIMULATOR
#pragma mark - Current Debug token
- (void)testCurrentTokenShim {
@@ -204,4 +456,73 @@ static NSString *const kProjectNumber = @"123456789";
OCMVerifyAll(self.debugProviderMock);
}
#pragma mark - Helpers
/// Returns an app with complete options for token exchange and automatic token refresh disabled.
- (FIRApp *)appWithName:(NSString *)name googleAppID:(NSString *)googleAppID {
FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:googleAppID
GCMSenderID:kProjectNumber];
options.APIKey = kAPIKey;
options.projectID = kProjectID;
FIRApp *app = [[FIRApp alloc] initInstanceWithName:name options:options];
// The following disables automatic token refresh, which could interfere with tests.
app.dataCollectionDefaultEnabled = NO;
return app;
}
/// Creates a temporary host home directory containing a config store with the given tokens,
/// removed when the test finishes.
- (NSURL *)temporaryHostHomeWithTokens:(NSDictionary<NSString *, NSString *> *)tokens {
NSURL *hostHomeURL = [[NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES]
URLByAppendingPathComponent:[NSUUID UUID].UUIDString
isDirectory:YES];
NSURL *fileURL =
[hostHomeURL URLByAppendingPathComponent:FIRAppCheckDebugTokenConfigStore.relativePath
isDirectory:NO];
NSError *error;
XCTAssertTrue(
[[NSFileManager defaultManager] createDirectoryAtURL:fileURL.URLByDeletingLastPathComponent
withIntermediateDirectories:YES
attributes:nil
error:&error],
@"%@", error);
NSData *data = [NSJSONSerialization dataWithJSONObject:@{@"tokens" : tokens}
options:0
error:&error];
XCTAssertTrue([data writeToURL:fileURL options:NSDataWritingAtomic error:&error], @"%@", error);
[self addTeardownBlock:^{
[[NSFileManager defaultManager] removeItemAtURL:hostHomeURL error:nil];
}];
return hostHomeURL;
}
/// Sets (or unsets, when `value` is `nil`) a process environment variable for the duration of the
/// test, restoring its previous value afterwards.
- (void)setEnvironmentVariable:(NSString *)name value:(nullable NSString *)value {
const char *previousValue = getenv(name.UTF8String);
NSString *previous = previousValue ? @(previousValue) : nil;
if (value) {
setenv(name.UTF8String, value.UTF8String, 1);
} else {
unsetenv(name.UTF8String);
}
[self addTeardownBlock:^{
if (previous) {
setenv(name.UTF8String, previous.UTF8String, 1);
} else {
unsetenv(name.UTF8String);
}
}];
}
/// Returns a provider whose core debug provider resolved its debug token from `source`, which must
/// not be `GACAppCheckDebugTokenSourceLocal` (that would log the local token on the strict mock).
- (FIRAppCheckDebugProvider *)providerWithDebugTokenSource:(GACAppCheckDebugTokenSource)source
projectID:(nullable NSString *)projectID {
self.stubbedDebugTokenSource = source;
return [[FIRAppCheckDebugProvider alloc] initWithDebugProvider:self.debugProviderMock
projectID:projectID
googleAppID:kAppID];
}
@end
@@ -0,0 +1,411 @@
/*
* 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 <TargetConditionals.h>
#import <XCTest/XCTest.h>
#import "FirebaseAppCheck/Sources/DebugProvider/FIRAppCheckDebugTokenConfigStore.h"
static NSString *const kAppID = @"1:248211933065:ios:91feeddd848ff1a48f0358";
// The host config store can only be read from a Simulator; elsewhere the lookup is compiled out.
#if TARGET_OS_SIMULATOR
static NSString *const kOtherAppID = @"1:248211933065:android:0123456789abcdef012345";
static NSString *const kToken = @"29d93e77-7bea-4f2a-b6ed-c1d1db892666";
@interface FIRAppCheckDebugTokenConfigStore (Tests)
+ (BOOL)isMissingConfigStoreError:(nullable NSError *)error;
@end
@interface FIRAppCheckDebugTokenConfigStoreTests : XCTestCase
/// A temporary directory standing in for the host user's home directory.
@property(nonatomic) NSURL *hostHomeURL;
@property(nonatomic, copy) NSDictionary<NSString *, NSString *> *environment;
@end
@implementation FIRAppCheckDebugTokenConfigStoreTests
- (void)setUp {
[super setUp];
NSString *directoryName = [NSString
stringWithFormat:@"%@-%@", NSStringFromClass([self class]), [NSUUID UUID].UUIDString];
self.hostHomeURL = [[NSURL fileURLWithPath:NSTemporaryDirectory()
isDirectory:YES] URLByAppendingPathComponent:directoryName
isDirectory:YES];
NSError *error;
XCTAssertTrue([[NSFileManager defaultManager] createDirectoryAtURL:self.hostHomeURL
withIntermediateDirectories:YES
attributes:nil
error:&error],
@"%@", error);
self.environment = @{@"SIMULATOR_HOST_HOME" : self.hostHomeURL.path};
}
- (void)tearDown {
[[NSFileManager defaultManager] removeItemAtURL:self.hostHomeURL error:nil];
[super tearDown];
}
#pragma mark - Helpers
- (void)writeConfigStoreContents:(NSString *)contents {
NSURL *fileURL =
[self.hostHomeURL URLByAppendingPathComponent:FIRAppCheckDebugTokenConfigStore.relativePath];
NSError *error;
XCTAssertTrue(
[[NSFileManager defaultManager] createDirectoryAtURL:fileURL.URLByDeletingLastPathComponent
withIntermediateDirectories:YES
attributes:nil
error:&error],
@"%@", error);
XCTAssertTrue([contents writeToURL:fileURL
atomically:YES
encoding:NSUTF8StringEncoding
error:&error],
@"%@", error);
}
- (void)writeConfigStoreTokens:(NSDictionary<NSString *, id> *)tokens {
NSDictionary *json = @{
@"_comment" : @"This file contains secrets and should not be committed to source repositories",
@"tokens" : tokens,
};
NSError *error;
NSData *data = [NSJSONSerialization dataWithJSONObject:json options:0 error:&error];
XCTAssertNotNil(data, @"%@", error);
[self writeConfigStoreContents:[[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding]];
}
#pragma mark - Tests
- (void)testRelativePath {
XCTAssertEqualObjects(FIRAppCheckDebugTokenConfigStore.relativePath,
@".config/configstore/firebase-appcheck.json");
}
- (void)testDebugTokenForAppInConfigStore {
[self writeConfigStoreTokens:@{kAppID : kToken, kOtherAppID : @"other-token"}];
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:self.environment];
XCTAssertEqualObjects(token, kToken);
}
- (void)testDebugTokenForAppMissingFromConfigStore {
[self writeConfigStoreTokens:@{kOtherAppID : @"other-token"}];
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:self.environment];
XCTAssertNil(token);
}
- (void)testDebugTokenWhenConfigStoreFileMissing {
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:self.environment];
XCTAssertNil(token);
}
- (void)testDebugTokenWhenSimulatorHostHomeUnset {
[self writeConfigStoreTokens:@{kAppID : kToken}];
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:@{}];
XCTAssertNil(token);
}
- (void)testDebugTokenWhenSimulatorHostHomeEmpty {
[self writeConfigStoreTokens:@{kAppID : kToken}];
NSString *token =
[FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:@{@"SIMULATOR_HOST_HOME" : @""}];
XCTAssertNil(token);
}
- (void)testDebugTokenWhenConfigStoreMalformed {
[self writeConfigStoreContents:@"{ not json"];
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:self.environment];
XCTAssertNil(token);
}
- (void)testDebugTokenWhenTokensNotADictionary {
[self writeConfigStoreContents:@"{\"tokens\": [\"not\", \"a\", \"dictionary\"]}"];
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:self.environment];
XCTAssertNil(token);
}
- (void)testDebugTokenWhenTokensKeyMissing {
[self writeConfigStoreContents:@"{\"tokenNames\": {}}"];
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:self.environment];
XCTAssertNil(token);
}
- (void)testDebugTokenWhenConfigStoreIsEmptyObject {
[self writeConfigStoreContents:@"{}"];
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:self.environment];
XCTAssertNil(token);
}
- (void)testDebugTokenWhenTokenNotAString {
[self writeConfigStoreTokens:@{kAppID : @42}];
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:self.environment];
XCTAssertNil(token);
}
- (void)testDebugTokenWhenTokenEmpty {
[self writeConfigStoreTokens:@{kAppID : @""}];
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:self.environment];
XCTAssertNil(token);
}
- (void)testDebugTokenWhenGoogleAppIDEmpty {
[self writeConfigStoreTokens:@{kAppID : kToken}];
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:@""
environment:self.environment];
XCTAssertNil(token);
}
- (void)testDebugTokenWhenTopLevelJSONIsAnArray {
[self writeConfigStoreContents:@"[{\"tokens\": {}}]"];
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:self.environment];
XCTAssertNil(token);
}
- (void)testDebugTokenWhenTokenIsNull {
[self writeConfigStoreTokens:@{kAppID : [NSNull null]}];
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:self.environment];
XCTAssertNil(token);
}
- (void)testDebugTokenWhenSimulatorHostHomeHasTrailingSlash {
[self writeConfigStoreTokens:@{kAppID : kToken}];
NSDictionary<NSString *, NSString *> *environment =
@{@"SIMULATOR_HOST_HOME" : [self.hostHomeURL.path stringByAppendingString:@"/"]};
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:environment];
XCTAssertEqualObjects(token, kToken);
}
- (void)testDebugTokenWhenConfigStorePathIsADirectory {
NSURL *directoryURL =
[self.hostHomeURL URLByAppendingPathComponent:FIRAppCheckDebugTokenConfigStore.relativePath
isDirectory:YES];
NSError *error;
XCTAssertTrue([[NSFileManager defaultManager] createDirectoryAtURL:directoryURL
withIntermediateDirectories:YES
attributes:nil
error:&error],
@"%@", error);
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:self.environment];
XCTAssertNil(token);
}
- (void)testDebugTokenWhenConfigStoreParentIsAFile {
// `~/.config` is a regular file, so the config store cannot exist; treated like a missing file.
[self writeEmptyFileAtRelativePath:@".config"];
NSString *token = [FIRAppCheckDebugTokenConfigStore debugTokenForGoogleAppID:kAppID
environment:self.environment];
XCTAssertNil(token);
}
#pragma mark - Missing config store errors
- (void)testMissingFileIsMissingConfigStoreError {
NSError *error = [self configStoreReadError];
XCTAssertTrue([FIRAppCheckDebugTokenConfigStore isMissingConfigStoreError:error], @"%@", error);
}
- (void)testConfigParentFileIsMissingConfigStoreError {
[self writeEmptyFileAtRelativePath:@".config"];
NSError *error = [self configStoreReadError];
XCTAssertTrue([FIRAppCheckDebugTokenConfigStore isMissingConfigStoreError:error], @"%@", error);
}
- (void)testConfigstoreParentFileIsMissingConfigStoreError {
[self writeEmptyFileAtRelativePath:@".config/configstore"];
NSError *error = [self configStoreReadError];
XCTAssertTrue([FIRAppCheckDebugTokenConfigStore isMissingConfigStoreError:error], @"%@", error);
}
- (void)testDirectoryAtConfigStorePathIsNotMissingConfigStoreError {
NSURL *directoryURL =
[self.hostHomeURL URLByAppendingPathComponent:FIRAppCheckDebugTokenConfigStore.relativePath
isDirectory:YES];
NSError *createError;
XCTAssertTrue([[NSFileManager defaultManager] createDirectoryAtURL:directoryURL
withIntermediateDirectories:YES
attributes:nil
error:&createError],
@"%@", createError);
NSError *error = [self configStoreReadError];
XCTAssertFalse([FIRAppCheckDebugTokenConfigStore isMissingConfigStoreError:error], @"%@", error);
}
- (void)testUnreadableConfigStoreIsNotMissingConfigStoreError {
[self writeConfigStoreTokens:@{kAppID : kToken}];
NSString *path =
[self.hostHomeURL URLByAppendingPathComponent:FIRAppCheckDebugTokenConfigStore.relativePath]
.path;
NSError *attributesError;
XCTAssertTrue([[NSFileManager defaultManager] setAttributes:@{NSFilePosixPermissions : @0}
ofItemAtPath:path
error:&attributesError],
@"%@", attributesError);
NSError *error = [self configStoreReadError];
XCTAssertFalse([FIRAppCheckDebugTokenConfigStore isMissingConfigStoreError:error], @"%@", error);
}
- (void)testNilIsNotMissingConfigStoreError {
XCTAssertFalse([FIRAppCheckDebugTokenConfigStore isMissingConfigStoreError:nil]);
}
- (void)testNonErrorUnderlyingValueIsNotMissingConfigStoreError {
NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain
code:NSFileReadUnknownError
userInfo:@{NSUnderlyingErrorKey : @"not an error"}];
BOOL isMissing = [FIRAppCheckDebugTokenConfigStore isMissingConfigStoreError:error];
XCTAssertFalse(isMissing);
}
#pragma mark - Error helpers
/// Writes an empty regular file at `relativePath` under the temporary host home.
- (void)writeEmptyFileAtRelativePath:(NSString *)relativePath {
NSURL *fileURL = [self.hostHomeURL URLByAppendingPathComponent:relativePath isDirectory:NO];
NSError *error;
XCTAssertTrue(
[[NSFileManager defaultManager] createDirectoryAtURL:fileURL.URLByDeletingLastPathComponent
withIntermediateDirectories:YES
attributes:nil
error:&error],
@"%@", error);
XCTAssertTrue([@"" writeToURL:fileURL atomically:YES encoding:NSUTF8StringEncoding error:&error],
@"%@", error);
}
/// Reads the config store file under the temporary host home the same way the config store does,
/// and returns the resulting error.
- (nullable NSError *)configStoreReadError {
NSURL *fileURL =
[self.hostHomeURL URLByAppendingPathComponent:FIRAppCheckDebugTokenConfigStore.relativePath
isDirectory:NO];
NSError *error;
NSData *data = [NSData dataWithContentsOfURL:fileURL options:0 error:&error];
XCTAssertNil(data);
XCTAssertNotNil(error);
return error;
}
@end
#else // TARGET_OS_SIMULATOR
@interface FIRAppCheckDebugTokenConfigStoreTests : XCTestCase
@end
@implementation FIRAppCheckDebugTokenConfigStoreTests
- (void)testDebugTokenIsNilOutsideSimulator {
// Seed a valid config store so a `nil` result proves the lookup is compiled out rather than
// failing to find the file.
NSURL *hostHomeURL = [[NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES]
URLByAppendingPathComponent:[NSUUID UUID].UUIDString
isDirectory:YES];
NSURL *fileURL =
[hostHomeURL URLByAppendingPathComponent:FIRAppCheckDebugTokenConfigStore.relativePath
isDirectory:NO];
NSError *error;
XCTAssertTrue(
[[NSFileManager defaultManager] createDirectoryAtURL:fileURL.URLByDeletingLastPathComponent
withIntermediateDirectories:YES
attributes:nil
error:&error],
@"%@", error);
[self addTeardownBlock:^{
[[NSFileManager defaultManager] removeItemAtURL:hostHomeURL error:nil];
}];
NSString *contents = [NSString stringWithFormat:@"{\"tokens\": {\"%@\": \"token\"}}", kAppID];
XCTAssertTrue([contents writeToURL:fileURL
atomically:YES
encoding:NSUTF8StringEncoding
error:&error],
@"%@", error);
NSString *token = [FIRAppCheckDebugTokenConfigStore
debugTokenForGoogleAppID:kAppID
environment:@{@"SIMULATOR_HOST_HOME" : hostHomeURL.path}];
XCTAssertNil(token);
}
@end
#endif // TARGET_OS_SIMULATOR
+8 -2
View File
@@ -1848,12 +1848,18 @@ func appCheckDependency() -> Package.Dependency {
let appCheckURL = "https://github.com/google/app-check.git"
if let localPath = Context.environment["FIREBASE_APP_CHECK_LOCAL_PATH"] {
return .package(path: localPath)
// Pin the package name so a checkout in a directory not named `app-check` still resolves.
return .package(name: "app-check", path: localPath)
}
if let branch = Context.environment["FIREBASE_APP_CHECK_BRANCH"] {
return .package(url: appCheckURL, branch: branch)
}
return .package(url: appCheckURL, "12.0.0" ..< "13.0.0")
// Point SPM CI to the tip of main of https://github.com/google/app-check until 12.1.0 is tagged.
if Context.environment["FIREBASECI_USE_LATEST_GOOGLEAPPMEASUREMENT"] != nil {
return .package(url: appCheckURL, branch: "main")
}
return .package(url: appCheckURL, "12.1.0" ..< "13.0.0")
}