mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
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.
412 lines
15 KiB
Objective-C
412 lines
15 KiB
Objective-C
/*
|
|
* 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
|