mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
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:
1 parent
509b9a3888
commit
c06a1d7199
11 files changed
+1027
-14
No files matched your search
@@ -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'
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
Reference in new issue
Block a user