From 9fcbebd4ba62191bb9268cad37b908fe7f7c5a17 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:54:14 -0700 Subject: [PATCH 1/9] g-orchestrated: add the GIDBundle seam with NSBundle conformance and a fake --- .../Sources/GIDBundle/API/GIDBundle.h | 35 +++++++++++++ .../Sources/GIDBundle/Fake/GIDFakeBundle.h | 34 +++++++++++++ .../Sources/GIDBundle/Fake/GIDFakeBundle.m | 49 +++++++++++++++++++ .../Implementation/NSBundle+GIDBundle.h | 28 +++++++++++ .../Implementation/NSBundle+GIDBundle.m | 24 +++++++++ 5 files changed, 170 insertions(+) create mode 100644 GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h create mode 100644 GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.h create mode 100644 GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.m create mode 100644 GoogleSignIn/Sources/GIDBundle/Implementation/NSBundle+GIDBundle.h create mode 100644 GoogleSignIn/Sources/GIDBundle/Implementation/NSBundle+GIDBundle.m diff --git a/GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h b/GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h new file mode 100644 index 00000000..2327a66b --- /dev/null +++ b/GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h @@ -0,0 +1,35 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +/// The subset of `NSBundle` that Google Sign-In reads at runtime. +/// +/// The SDK depends on this protocol rather than on `NSBundle` directly so that the host app's +/// `Info.plist` can be substituted in tests. `NSBundle` conforms; see `NSBundle+GIDBundle.h`. +@protocol GIDBundle + +/// The receiver's bundle identifier, if any. +@property(nonatomic, readonly, copy, nullable) NSString *bundleIdentifier; + +/// Returns the value associated with `key` in the receiver's information property list. +- (nullable id)objectForInfoDictionaryKey:(NSString *)key; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.h b/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.h new file mode 100644 index 00000000..b071210c --- /dev/null +++ b/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.h @@ -0,0 +1,34 @@ +/* + * 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 + +#import "GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h" + +NS_ASSUME_NONNULL_BEGIN + +/// An in-memory `GIDBundle` for tests. Owned by the `GIDBundle` seam. +@interface GIDFakeBundle : NSObject + +/// The bundle identifier to report. Defaults to nil. +@property(nonatomic, copy, nullable) NSString *bundleIdentifier; + +/// Sets (or, with nil, removes) the value returned by `-objectForInfoDictionaryKey:` for `key`. +- (void)setObject:(nullable id)object forInfoDictionaryKey:(NSString *)key; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.m b/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.m new file mode 100644 index 00000000..5d7f8632 --- /dev/null +++ b/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.m @@ -0,0 +1,49 @@ +/* + * 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 "GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation GIDFakeBundle { + NSMutableDictionary *_infoDictionary; +} + +@synthesize bundleIdentifier = _bundleIdentifier; + +- (instancetype)init { + self = [super init]; + if (self) { + _infoDictionary = [NSMutableDictionary dictionary]; + } + return self; +} + +- (nullable id)objectForInfoDictionaryKey:(NSString *)key { + return _infoDictionary[key]; +} + +- (void)setObject:(nullable id)object forInfoDictionaryKey:(NSString *)key { + if (object) { + [_infoDictionary setObject:object forKey:key]; + } else { + [_infoDictionary removeObjectForKey:key]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDBundle/Implementation/NSBundle+GIDBundle.h b/GoogleSignIn/Sources/GIDBundle/Implementation/NSBundle+GIDBundle.h new file mode 100644 index 00000000..5b2bc467 --- /dev/null +++ b/GoogleSignIn/Sources/GIDBundle/Implementation/NSBundle+GIDBundle.h @@ -0,0 +1,28 @@ +/* + * 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 + +#import "GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h" + +NS_ASSUME_NONNULL_BEGIN + +/// `NSBundle` already implements every `GIDBundle` requirement; this category only declares the +/// conformance. +@interface NSBundle (GIDBundle) +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDBundle/Implementation/NSBundle+GIDBundle.m b/GoogleSignIn/Sources/GIDBundle/Implementation/NSBundle+GIDBundle.m new file mode 100644 index 00000000..722ae872 --- /dev/null +++ b/GoogleSignIn/Sources/GIDBundle/Implementation/NSBundle+GIDBundle.m @@ -0,0 +1,24 @@ +/* + * 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 "GoogleSignIn/Sources/GIDBundle/Implementation/NSBundle+GIDBundle.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation NSBundle (GIDBundle) +@end + +NS_ASSUME_NONNULL_END From 6700aba7777148518db7c8a25c6c67bc3d04699c Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:58:30 -0700 Subject: [PATCH 2/9] g-orchestrated: inject bundle and user defaults into GIDSignIn, GIDAuthStateMigration and GIDSignInCallbackSchemes --- .../Fake/GIDFakeAuthStateMigration.m | 5 -- .../GIDAuthStateMigration.h | 13 +++- .../Implementation/GIDAuthStateMigration.m | 32 +++++++--- GoogleSignIn/Sources/GIDSignIn.m | 59 +++++++++++++------ .../Sources/GIDSignInCallbackSchemes.h | 10 +++- .../Sources/GIDSignInCallbackSchemes.m | 22 ++++--- GoogleSignIn/Sources/GIDSignIn_Private.h | 19 ++++++ .../Tests/Unit/GIDAuthStateMigrationTest.m | 2 +- 8 files changed, 120 insertions(+), 42 deletions(-) diff --git a/GoogleSignIn/Sources/GIDAuthStateMigration/Fake/GIDFakeAuthStateMigration.m b/GoogleSignIn/Sources/GIDAuthStateMigration/Fake/GIDFakeAuthStateMigration.m index b753b814..a0c08bef 100644 --- a/GoogleSignIn/Sources/GIDAuthStateMigration/Fake/GIDFakeAuthStateMigration.m +++ b/GoogleSignIn/Sources/GIDAuthStateMigration/Fake/GIDFakeAuthStateMigration.m @@ -22,11 +22,6 @@ @implementation GIDFakeAuthStateMigration @synthesize migrationInvokedCallback = _migrationInvokedCallback; -- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore { - self = [super initWithKeychainStore:keychainStore]; - return self; -} - - (void)migrateIfNeededWithTokenURL:(NSURL *)tokenURL callbackPath:(NSString *)callbackPath isFreshInstall:(BOOL)isFreshInstall { diff --git a/GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h b/GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h index 3f246b77..2559734f 100644 --- a/GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h +++ b/GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h @@ -16,6 +16,8 @@ #import +#import "GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h" + @class GTMKeychainStore; @class GTMAuthSession; @@ -24,8 +26,15 @@ NS_ASSUME_NONNULL_BEGIN /// A class providing migration support for auth state saved by older versions of the SDK. @interface GIDAuthStateMigration : NSObject -/// Creates an instance of this migration type with the keychain storage wrapper it will use. -- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore NS_DESIGNATED_INITIALIZER; +/// Creates an instance of this migration type with the keychain storage wrapper it will use, the +/// user defaults in which it records that a migration check has run, and the bundle whose +/// identifier was used to fingerprint legacy auth state. +- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore + userDefaults:(NSUserDefaults *)userDefaults + bundle:(id)bundle NS_DESIGNATED_INITIALIZER; + +/// Same as above, using `[NSUserDefaults standardUserDefaults]` and `[NSBundle mainBundle]`. +- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore; /// Perform necessary migrations from legacy auth state storage to most recent GTMAppAuth version. - (void)migrateIfNeededWithTokenURL:(NSURL *)tokenURL diff --git a/GoogleSignIn/Sources/GIDAuthStateMigration/Implementation/GIDAuthStateMigration.m b/GoogleSignIn/Sources/GIDAuthStateMigration/Implementation/GIDAuthStateMigration.m index e9c1c107..69727055 100644 --- a/GoogleSignIn/Sources/GIDAuthStateMigration/Implementation/GIDAuthStateMigration.m +++ b/GoogleSignIn/Sources/GIDAuthStateMigration/Implementation/GIDAuthStateMigration.m @@ -13,6 +13,8 @@ // limitations under the License. #import "GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h" + +#import "GoogleSignIn/Sources/GIDBundle/Implementation/NSBundle+GIDBundle.h" #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h" @import GTMAppAuth; @@ -47,16 +49,29 @@ @interface GIDAuthStateMigration () @end -@implementation GIDAuthStateMigration +@implementation GIDAuthStateMigration { + NSUserDefaults *_userDefaults; + id _bundle; +} -- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore { +- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore + userDefaults:(NSUserDefaults *)userDefaults + bundle:(id)bundle { self = [super init]; if (self) { _keychainStore = keychainStore; + _userDefaults = userDefaults; + _bundle = bundle; } return self; } +- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore { + return [self initWithKeychainStore:keychainStore + userDefaults:[NSUserDefaults standardUserDefaults] + bundle:[NSBundle mainBundle]]; +} + - (instancetype)init { GTMKeychainStore *keychainStore = [[GTMKeychainStore alloc] initWithItemName:@"auth"]; return [self initWithKeychainStore:keychainStore]; @@ -68,7 +83,7 @@ - (void)migrateIfNeededWithTokenURL:(NSURL *)tokenURL // If this is a fresh install, take no action and mark the migration checks as having been // performed. if (isFreshInstall) { - NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; + NSUserDefaults* defaults = _userDefaults; #if TARGET_OS_OSX [defaults setBool:YES forKey:kDataProtectedMigrationCheckPerformedKey]; #elif TARGET_OS_IOS && !TARGET_OS_MACCATALYST @@ -89,7 +104,7 @@ - (void)migrateIfNeededWithTokenURL:(NSURL *)tokenURL // Migrate from the fileBasedKeychain to dataProtectedKeychain with GTMAppAuth 5.0. - (void)performDataProtectedMigrationIfNeeded { // See if we've performed the migration check previously. - NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; + NSUserDefaults* defaults = _userDefaults; if ([defaults boolForKey:kDataProtectedMigrationCheckPerformedKey]) { return; } @@ -117,7 +132,7 @@ - (void)performDataProtectedMigrationIfNeeded { - (void)performGIDMigrationIfNeededWithTokenURL:(NSURL *)tokenURL callbackPath:(NSString *)callbackPath { // See if we've performed the migration check previously. - NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; + NSUserDefaults* defaults = _userDefaults; if ([defaults boolForKey:kGTMAppAuthMigrationCheckPerformedKey]) { return; } @@ -155,7 +170,7 @@ - (nullable GTMAuthSession *)extractAuthSessionWithTokenURL:(NSURL *)tokenURL } // Parse the fingerprint. - NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier]; + NSString *bundleID = _bundle.bundleIdentifier; NSString *pattern = [NSString stringWithFormat:@"^%@-(.+)-(?:email|profile|https:\\/\\/).*$", bundleID]; NSError *error; @@ -174,8 +189,9 @@ - (nullable GTMAuthSession *)extractAuthSessionWithTokenURL:(NSURL *)tokenURL NSString *clientID = [fingerprint substringWithRange:[matches[0] rangeAtIndex:1]]; // Generate the redirect URI from the extracted client ID. - NSString *scheme = - [[[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:clientID] clientIdentifierScheme]; + NSString *scheme = [[[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:clientID + bundle:_bundle] + clientIdentifierScheme]; NSString *redirectURI = [NSString stringWithFormat:@"%@:%@", scheme, callbackPath]; // Retrieve the additional token request parameters value. diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index dc26c07f..4577ccf1 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -22,6 +22,7 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInResult.h" #import "GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h" +#import "GoogleSignIn/Sources/GIDBundle/Implementation/NSBundle+GIDBundle.h" #import "GoogleSignIn/Sources/GIDEMMSupport.h" #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h" #import "GoogleSignIn/Sources/GIDSignInPreferences.h" @@ -193,6 +194,8 @@ @implementation GIDSignIn { BOOL _restarting; // Keychain manager for GTMAppAuth GTMKeychainStore *_keychainStore; + id _bundle; + NSUserDefaults *_userDefaults; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST // The class used to manage presenting the loading screen for fetching app check tokens. GIDTimedLoader *_timedLoader; @@ -688,18 +691,25 @@ - (void)configureDebugProviderWithAPIKey:(NSString *)APIKey - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore authStateMigrationService:(GIDAuthStateMigration *)authStateMigrationService { + return [self initWithKeychainStore:keychainStore + authStateMigrationService:authStateMigrationService + bundle:NSBundle.mainBundle + userDefaults:NSUserDefaults.standardUserDefaults]; +} + +- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore + authStateMigrationService:(GIDAuthStateMigration *)authStateMigrationService + bundle:(id)bundle + userDefaults:(NSUserDefaults *)userDefaults { self = [super init]; if (self) { + _bundle = bundle; + _userDefaults = userDefaults; _keychainStore = keychainStore; _claimsInternalOptions = [[GIDClaimsInternalOptions alloc] init]; - // Get the bundle of the current executable. - NSBundle *bundle = NSBundle.mainBundle; - - // If we have a bundle, try to set the active configuration from the bundle's Info.plist. - if (bundle) { - _configuration = [GIDSignIn configurationFromBundle:bundle]; - } + // Try to set the active configuration from the bundle's Info.plist. + _configuration = [GIDSignIn configurationFromBundle:_bundle]; // Check to see if the 3P app is being run for the first time after a fresh install. BOOL isFreshInstall = [self isFreshInstall]; @@ -728,8 +738,22 @@ - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore authStateMigrationService:(GIDAuthStateMigration *)authStateMigrationService appCheck:(GIDAppCheck *)appCheck { + return [self initWithKeychainStore:keychainStore + authStateMigrationService:authStateMigrationService + appCheck:appCheck + bundle:NSBundle.mainBundle + userDefaults:NSUserDefaults.standardUserDefaults]; +} + +- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore + authStateMigrationService:(GIDAuthStateMigration *)authStateMigrationService + appCheck:(GIDAppCheck *)appCheck + bundle:(id)bundle + userDefaults:(NSUserDefaults *)userDefaults { self = [self initWithKeychainStore:keychainStore - authStateMigrationService:authStateMigrationService]; + authStateMigrationService:authStateMigrationService + bundle:bundle + userDefaults:userDefaults]; if (self) { _appCheck = appCheck; _configureAppCheckCalled = NO; @@ -764,7 +788,8 @@ - (void)signInWithOptions:(GIDSignInInternalOptions *)options { // If the application does not support the required URL schemes tell the developer so. GIDSignInCallbackSchemes *schemes = - [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:options.configuration.clientID]; + [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:options.configuration.clientID + bundle:_bundle]; NSArray *unsupportedSchemes = [schemes unsupportedSchemes]; if (unsupportedSchemes.count != 0) { // NOLINTNEXTLINE(google-objc-avoid-throwing-exception) @@ -957,7 +982,8 @@ - (void)authorizationRequestWithOptions:(GIDSignInInternalOptions *)options comp - (NSURL *)redirectURLWithOptions:(GIDSignInInternalOptions *)options { GIDSignInCallbackSchemes *schemes = - [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:options.configuration.clientID]; + [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:options.configuration.clientID + bundle:_bundle]; NSURL *redirectURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@:%@", [schemes clientIdentifierScheme], kBrowserCallbackPath]]; @@ -1336,11 +1362,10 @@ - (void)assertValidPresentingViewController { // Checks whether or not this is the first time the app runs. - (BOOL)isFreshInstall { - NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; - if ([defaults boolForKey:kAppHasRunBeforeKey]) { + if ([_userDefaults boolForKey:kAppHasRunBeforeKey]) { return NO; } - [defaults setBool:YES forKey:kAppHasRunBeforeKey]; + [_userDefaults setBool:YES forKey:kAppHasRunBeforeKey]; return YES; } @@ -1378,8 +1403,8 @@ - (GIDProfileData *)profileDataWithIDToken:(OIDIDToken *)idToken { imageURL:[NSURL URLWithString:idToken.claims[kBasicProfilePictureKey]]]; } -// Try to retrieve a configuration value from an |NSBundle|'s Info.plist for a given key. -+ (nullable NSString *)configValueFromBundle:(NSBundle *)bundle forKey:(NSString *)key { +// Try to retrieve a configuration value from the bundle's Info.plist for a given key. ++ (nullable NSString *)configValueFromBundle:(id)bundle forKey:(NSString *)key { NSString *value; id configValue = [bundle objectForInfoDictionaryKey:key]; if ([configValue isKindOfClass:[NSString class]]) { @@ -1388,8 +1413,8 @@ + (nullable NSString *)configValueFromBundle:(NSBundle *)bundle forKey:(NSString return value; } -// Try to generate a |GIDConfiguration| from an |NSBundle|'s Info.plist. -+ (nullable GIDConfiguration *)configurationFromBundle:(NSBundle *)bundle { +// Try to generate a |GIDConfiguration| from the bundle's Info.plist. ++ (nullable GIDConfiguration *)configurationFromBundle:(id)bundle { GIDConfiguration *configuration; // Retrieve any valid config parameters from the bundle's Info.plist. diff --git a/GoogleSignIn/Sources/GIDSignInCallbackSchemes.h b/GoogleSignIn/Sources/GIDSignInCallbackSchemes.h index 42ba99ec..98e16576 100644 --- a/GoogleSignIn/Sources/GIDSignInCallbackSchemes.h +++ b/GoogleSignIn/Sources/GIDSignInCallbackSchemes.h @@ -16,6 +16,8 @@ #import +#import "GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h" + NS_ASSUME_NONNULL_BEGIN // A utility class for dealing with callback schemes. @@ -24,8 +26,12 @@ NS_ASSUME_NONNULL_BEGIN // Please call the designated initializer. - (instancetype)init NS_UNAVAILABLE; -// The designated initializer. -- (instancetype)initWithClientIdentifier:(NSString *)clientIdentifier NS_DESIGNATED_INITIALIZER; +// Creates an instance that validates the host app's URL schemes against `bundle`'s Info.plist. +- (instancetype)initWithClientIdentifier:(NSString *)clientIdentifier + bundle:(id)bundle NS_DESIGNATED_INITIALIZER; + +// Same as above, using `[NSBundle mainBundle]`. +- (instancetype)initWithClientIdentifier:(NSString *)clientIdentifier; // The canonical client identifier callback scheme. Requires clientId to be set on GIDSignIn. - (NSString *)clientIdentifierScheme; diff --git a/GoogleSignIn/Sources/GIDSignInCallbackSchemes.m b/GoogleSignIn/Sources/GIDSignInCallbackSchemes.m index 7f5888f7..58d44c1f 100644 --- a/GoogleSignIn/Sources/GIDSignInCallbackSchemes.m +++ b/GoogleSignIn/Sources/GIDSignInCallbackSchemes.m @@ -14,23 +14,25 @@ #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h" +#import "GoogleSignIn/Sources/GIDBundle/Implementation/NSBundle+GIDBundle.h" + NS_ASSUME_NONNULL_BEGIN @implementation GIDSignInCallbackSchemes { NSString *_clientIdentifier; + id _bundle; } /** * @fn relevantURLSchemes - * @brief Extracts CFBundleURLSchemes from the host app's info.plist. - * @return An array of lowercase NSString *'s representing the URL schemes the host app has declared + * @brief Extracts CFBundleURLSchemes from the injected bundle's Info.plist. + * @return An array of lowercase NSString *'s representing the URL schemes the bundle has declared * support for. * @remarks Branched from google3/googlemac/iPhone/Firebase/Source/GGLBundleUtil.m */ -+ (NSArray *)relevantURLSchemes { +- (NSArray *)relevantURLSchemes { NSMutableArray *result = [NSMutableArray array]; - NSBundle *bundle = [NSBundle mainBundle]; - NSArray *urlTypes = [bundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]; + NSArray *urlTypes = [_bundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]; for (NSDictionary *urlType in urlTypes) { NSArray *urlTypeSchemes = urlType[@"CFBundleURLSchemes"]; for (NSString *urlTypeScheme in urlTypeSchemes) { @@ -40,14 +42,20 @@ + (NSArray *)relevantURLSchemes { return result; } -- (instancetype)initWithClientIdentifier:(NSString *)clientIdentifier { +- (instancetype)initWithClientIdentifier:(NSString *)clientIdentifier + bundle:(id)bundle { self = [super init]; if (self) { _clientIdentifier = [clientIdentifier copy]; + _bundle = bundle; } return self; } +- (instancetype)initWithClientIdentifier:(NSString *)clientIdentifier { + return [self initWithClientIdentifier:clientIdentifier bundle:[NSBundle mainBundle]]; +} + - (NSString *)clientIdentifierScheme { NSArray *clientIdentifierParts = [_clientIdentifier componentsSeparatedByString:@"."]; NSString *reversedClientIdentifier = @@ -66,7 +74,7 @@ - (NSArray *)allSchemes { - (NSMutableArray *)unsupportedSchemes { NSMutableArray *unsupportedSchemes = [NSMutableArray arrayWithArray:[self allSchemes]]; - NSArray *supportedSchemes = [[self class] relevantURLSchemes]; + NSArray *supportedSchemes = [self relevantURLSchemes]; [unsupportedSchemes removeObjectsInArray:supportedSchemes]; return unsupportedSchemes; } diff --git a/GoogleSignIn/Sources/GIDSignIn_Private.h b/GoogleSignIn/Sources/GIDSignIn_Private.h index 803cc10e..09df27ef 100644 --- a/GoogleSignIn/Sources/GIDSignIn_Private.h +++ b/GoogleSignIn/Sources/GIDSignIn_Private.h @@ -18,6 +18,8 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" +#import "GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h" + #if __has_include() #import #elif __has_include() @@ -61,6 +63,23 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); API_AVAILABLE(ios(14)); #endif // TARGET_OS_IOS || !TARGET_OS_MACCATALYST +/// Private initializer taking a `GTMKeychainStore`, the bundle whose Info.plist supplies the +/// configuration and URL schemes, and the user defaults that record whether the app has run before. +- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore + authStateMigrationService:(GIDAuthStateMigration *)authStateMigrationService + bundle:(id)bundle + userDefaults:(NSUserDefaults *)userDefaults; + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +/// Same as above, also taking the `GIDAppCheck` to use. +- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore + authStateMigrationService:(GIDAuthStateMigration *)authStateMigrationService + appCheck:(GIDAppCheck *)appCheck + bundle:(id)bundle + userDefaults:(NSUserDefaults *)userDefaults +API_AVAILABLE(ios(14)); +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST + /// Authenticates with extra options. - (void)signInWithOptions:(GIDSignInInternalOptions *)options; diff --git a/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m b/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m index 1504224a..90664ae4 100644 --- a/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m +++ b/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m @@ -291,7 +291,7 @@ - (void)setUpCommonExtractAuthorizationMocksWithFingerPrint:(NSString *)fingerpr [[[_mockNSBundle expect] andReturn:kBundleID] bundleIdentifier]; [[[_mockGIDSignInCallbackSchemes expect] andReturn:_mockGIDSignInCallbackSchemes] alloc]; (void)[[[_mockGIDSignInCallbackSchemes expect] andReturn:_mockGIDSignInCallbackSchemes] - initWithClientIdentifier:kClientID]; + initWithClientIdentifier:kClientID bundle:OCMOCK_ANY]; [[[_mockGIDSignInCallbackSchemes expect] andReturn:kDotReversedClientID] clientIdentifierScheme]; [[[_mockGIDAuthStateMigration expect] andReturn:kAdditionalTokenRequestParameters] passwordForService:[self additionalTokenRequestParametersKeyFromFingerprint:fingerprint]]; From 6de01e5c5ba322acf0d21542cf5bb4a3520a192a Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:05:23 -0700 Subject: [PATCH 3/9] g-orchestrated: replace the NSBundle swizzle in GIDFakeMainBundle with a GIDFakeBundle subclass --- GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h | 21 ++--- GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m | 81 +++++++------------ .../Tests/Unit/GIDSignInCallbackSchemesTest.m | 15 ++-- 3 files changed, 40 insertions(+), 77 deletions(-) diff --git a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h index 3086880e..f1cb26c7 100644 --- a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h +++ b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h @@ -14,26 +14,21 @@ * limitations under the License. */ -#import +#import "GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.h" /** * @class GIDFakeMainBundle - * @brief Helps fake [NSBundle mainBundle] + * @brief A `GIDFakeBundle` pre-populated the way a host app's Info.plist would be for Google + * Sign-In, with helpers to describe common URL-scheme configurations. */ -@interface GIDFakeMainBundle : NSObject +@interface GIDFakeMainBundle : GIDFakeBundle /** - * @fn startFakingWithClientID: - * @brief Starts faking [NSBundle mainBundle] - * @param clientID The fake client idenfitier for the app. - */ -- (void)startFakingWithClientID:(NSString *)clientID; - -/** - * @fn stopFaking - * @brief Stops faking [NSBundle mainBundle] + * @fn initWithClientID: + * @brief Creates a bundle whose `GIDClientID` Info.plist value is `clientID`. + * @param clientID The fake client identifier for the app. */ -- (void)stopFaking; +- (instancetype)initWithClientID:(NSString *)clientID; #pragma mark - URL Schemes diff --git a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m index d017e60c..cc67afb2 100644 --- a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m +++ b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m @@ -14,9 +14,6 @@ #import "GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h" -#import -#import - static NSString *const kCFBundleURLTypesKey = @"CFBundleURLTypes"; static NSString *const kCFBundleURLSchemesKey = @"CFBundleURLSchemes"; @@ -29,44 +26,15 @@ @implementation GIDFakeMainBundle { NSString *_clientId; - - // Represents the Info.plist keys to fake. - NSArray *_fakedKeys; - - // Represents the values for any Info.plist keys to be faked. - NSMutableDictionary *_fakeConfig; } -- (void)startFakingWithClientID:(NSString *)clientId { - _clientId = clientId; - - _fakedKeys = @[ kCFBundleURLTypesKey, - kConfigClientIDKey, - kConfigServerClientIDKey, - kConfigHostedDomainKey, - kConfigOpenIDRealmKey ]; - - _fakeConfig = [@{ @"GIDClientID" : clientId } mutableCopy]; - - [GULSwizzler swizzleClass:[NSBundle class] - selector:@selector(objectForInfoDictionaryKey:) - isClassSelector:NO - withBlock:^id(id _self, NSString *key) { - if ([self->_fakedKeys containsObject:key]) { - return self->_fakeConfig[key]; - } else { - @throw [NSException exceptionWithName:@"Requested unexpected info.plist key." - reason:nil - userInfo:nil]; - } - }]; -} - -- (void)stopFaking { - [GULSwizzler unswizzleClass:[NSBundle class] - selector:@selector(objectForInfoDictionaryKey:) - isClassSelector:NO]; - _fakeConfig = nil; +- (instancetype)initWithClientID:(NSString *)clientId { + self = [super init]; + if (self) { + _clientId = clientId; + [self setObject:clientId forInfoDictionaryKey:kConfigClientIDKey]; + } + return self; } #pragma mark - Utilities @@ -108,31 +76,34 @@ - (NSString *)stringByFlippingCasesInString:(NSString *)original { #pragma mark - URL Schemes - (void)fakeAllSchemesSupported { - _fakeConfig[kCFBundleURLTypesKey] = @[ + [self setObject:@[ @{ kCFBundleURLSchemesKey : @[ [self reversedClientId] ] } - ]; + ] + forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeAllSchemesSupportedAndMerged { - _fakeConfig[kCFBundleURLTypesKey] = @[ + [self setObject:@[ @{ kCFBundleURLSchemesKey : @[ [self reversedClientId] ] }, - ]; + ] + forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeAllSchemesSupportedWithCasesMangled { NSString *caseFlippedReverseClientId = [self stringByFlippingCasesInString:[self reversedClientId]]; - _fakeConfig[kCFBundleURLTypesKey] = @[ + [self setObject:@[ @{ kCFBundleURLSchemesKey : @[ caseFlippedReverseClientId ] } - ]; + ] + forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeMissingClientIdScheme { @@ -140,36 +111,38 @@ - (void)fakeMissingClientIdScheme { } - (void)fakeMissingAllSchemes { - _fakeConfig[kCFBundleURLTypesKey] = nil; + [self setObject:nil forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeOtherSchemes { - _fakeConfig[kCFBundleURLTypesKey] = @[ + [self setObject:@[ @{ kCFBundleURLSchemesKey : @[ @"junk" ] } - ]; + ] + forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeOtherSchemesAndAllSchemes { - _fakeConfig[kCFBundleURLTypesKey] = @[ + [self setObject:@[ @{ kCFBundleURLSchemesKey : @[ @"junk" ] }, @{ kCFBundleURLSchemesKey : @[ [self reversedClientId] ] } - ]; + ] + forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeWithClientID:(id)clientID serverClientID:(id)serverClientID hostedDomain:(id)hostedDomain openIDRealm:(id)openIDRealm { - _fakeConfig[kConfigClientIDKey] = clientID; - _fakeConfig[kConfigServerClientIDKey] = serverClientID; - _fakeConfig[kConfigHostedDomainKey] = hostedDomain; - _fakeConfig[kConfigOpenIDRealmKey] = openIDRealm; + [self setObject:clientID forInfoDictionaryKey:kConfigClientIDKey]; + [self setObject:serverClientID forInfoDictionaryKey:kConfigServerClientIDKey]; + [self setObject:hostedDomain forInfoDictionaryKey:kConfigHostedDomainKey]; + [self setObject:openIDRealm forInfoDictionaryKey:kConfigOpenIDRealmKey]; } @end diff --git a/GoogleSignIn/Tests/Unit/GIDSignInCallbackSchemesTest.m b/GoogleSignIn/Tests/Unit/GIDSignInCallbackSchemesTest.m index c34536a5..4495025e 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInCallbackSchemesTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInCallbackSchemesTest.m @@ -27,25 +27,20 @@ @implementation GIDSignInCallbackSchemesTest { } - (void)setUp { - _fakeMainBundle = [[GIDFakeMainBundle alloc] init]; - [_fakeMainBundle startFakingWithClientID:kClientId]; -} - -- (void)tearDown { - [_fakeMainBundle stopFaking]; + _fakeMainBundle = [[GIDFakeMainBundle alloc] initWithClientID:kClientId]; } #pragma mark - Tests - (void)testClientIdentifierScheme { GIDSignInCallbackSchemes *schemes = - [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:kClientId]; + [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:kClientId bundle:_fakeMainBundle]; XCTAssertEqualObjects(schemes.clientIdentifierScheme, kClientId.lowercaseString); } - (void)testAllSchemes { GIDSignInCallbackSchemes *schemes = - [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:kClientId]; + [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:kClientId bundle:_fakeMainBundle]; XCTAssertEqual(schemes.allSchemes.count, 1U); for (NSString *scheme in schemes.allSchemes) { if ([scheme isEqual:kClientId.lowercaseString]) { @@ -64,7 +59,7 @@ - (void)testAllSchemes { */ - (void)testUnsupportedSchemes { GIDSignInCallbackSchemes *schemes = - [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:kClientId]; + [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:kClientId bundle:_fakeMainBundle]; [_fakeMainBundle fakeAllSchemesSupported]; XCTAssertEqual([schemes unsupportedSchemes].count, 0ul); @@ -90,7 +85,7 @@ - (void)testUnsupportedSchemes { - (void)testURLSchemeIsCallbackScheme { GIDSignInCallbackSchemes *schemes = - [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:kClientId]; + [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:kClientId bundle:_fakeMainBundle]; NSURL *clientIdentifierURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@:/", kClientId]]; NSURL *junkIdentifierURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@:/", @"junk"]]; From c115435a0084ed3fddef4edc2656c3b12d9cdea6 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:05:23 -0700 Subject: [PATCH 4/9] g-orchestrated: use a real presenter, an injected bundle and test user defaults in GIDSignInTest --- GoogleSignIn/Tests/Unit/GIDSignInTest.m | 77 ++++++++++++------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 630cd484..ef257ee7 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -171,17 +171,6 @@ static NSString *const kMultipleClaimsJsonString = @"{\"id_token\":{\"amr\":{\"essential\":false},\"auth_time\":{\"essential\":false}}}"; -#if TARGET_OS_IOS || TARGET_OS_MACCATALYST -// This category is used to allow the test to swizzle a private method. -@interface UIViewController (Testing) - -// This private method provides access to the window. It's declared here to avoid a warning about -// an unrecognized selector in the test. -- (UIWindow *)_window; - -@end -#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST - // This class extension exposes GIDSignIn methods to our tests. @interface GIDSignIn () @@ -215,11 +204,11 @@ @interface GIDSignInTest : XCTestCase { id _keychainStore; #if TARGET_OS_IOS || TARGET_OS_MACCATALYST - // Mock |UIViewController|. - id _presentingViewController; + // The presenting view controller passed to the sign-in calls. + UIViewController *_presentingViewController; #elif TARGET_OS_OSX - // Mock |NSWindow|. - id _presentingWindow; + // The presenting window passed to the sign-in calls. + NSWindow *_presentingWindow; #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST // Mock for |GIDGoogleUser|. @@ -240,7 +229,7 @@ @interface GIDSignInTest : XCTestCase { // Fake fetcher service to emulate network requests. GIDFakeFetcherService *_fetcherService; - // Fake [NSBundle mainBundle]; + // The bundle injected into |GIDSignIn| in place of the host app's main bundle. GIDFakeMainBundle *_fakeMainBundle; // Whether |saveParamsToKeychainForName:authentication:| has been called. @@ -284,7 +273,7 @@ @interface GIDSignInTest : XCTestCase { // Status returned by saveAuthorization:toKeychainForName: BOOL _saveAuthorizationReturnValue; - // Test userDefaults for use with `GIDAppCheck` + // Test user defaults injected into |GIDSignIn| (and used by |GIDAppCheck|). NSUserDefaults *_testUserDefaults; } @end @@ -307,9 +296,9 @@ - (void)setUp { // Mocks #if TARGET_OS_IOS || TARGET_OS_MACCATALYST - _presentingViewController = OCMStrictClassMock([UIViewController class]); + _presentingViewController = [[UIViewController alloc] init]; #elif TARGET_OS_OSX - _presentingWindow = OCMStrictClassMock([NSWindow class]); + _presentingWindow = [[NSWindow alloc] init]; #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST _authState = OCMClassMock([OIDAuthState class]); OCMStub([_authState alloc]).andReturn(_authState); @@ -348,8 +337,7 @@ - (void)setUp { // Fakes _authStateMigrationService = [[GIDFakeAuthStateMigration alloc] init]; _fetcherService = [[GIDFakeFetcherService alloc] init]; - _fakeMainBundle = [[GIDFakeMainBundle alloc] init]; - [_fakeMainBundle startFakingWithClientID:kClientId]; + _fakeMainBundle = [[GIDFakeMainBundle alloc] initWithClientID:kClientId]; [_fakeMainBundle fakeAllSchemesSupported]; // Object under test @@ -357,7 +345,9 @@ - (void)setUp { [_testUserDefaults setBool:YES forKey:kAppHasRunBeforeKey]; _signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore - authStateMigrationService:_authStateMigrationService]; + authStateMigrationService:_authStateMigrationService + bundle:_fakeMainBundle + userDefaults:_testUserDefaults]; _hint = nil; __weak GIDSignInTest *weakSelf = self; @@ -380,15 +370,8 @@ - (void)tearDown { OCMVerifyAll(_user); OCMVerifyAll(_oidAuthorizationService); -#if TARGET_OS_IOS || TARGET_OS_MACCATALYST - OCMVerifyAll(_presentingViewController); -#elif TARGET_OS_OSX - OCMVerifyAll(_presentingWindow); -#endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST - [_testUserDefaults removePersistentDomainForName:kUserDefaultsSuiteName]; - [_fakeMainBundle stopFaking]; [GIDSignInPreferences resetWrapperIdentifier]; [super tearDown]; } @@ -410,7 +393,9 @@ - (void)testConfigureSucceeds { GIDSignIn *signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore authStateMigrationService:_authStateMigrationService - appCheck:appCheck]; + appCheck:appCheck + bundle:_fakeMainBundle + userDefaults:_testUserDefaults]; [signIn configureWithCompletion:^(NSError * _Nullable error) { XCTAssertNil(error); [configureSucceedsExpecation fulfill]; @@ -434,7 +419,9 @@ - (void)testConfigureFailsNoTokenOrError { GIDSignIn *signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore authStateMigrationService:_authStateMigrationService - appCheck:appCheck]; + appCheck:appCheck + bundle:_fakeMainBundle + userDefaults:_testUserDefaults]; // Should fail if missing both token and error [signIn configureWithCompletion:^(NSError * _Nullable error) { @@ -453,7 +440,9 @@ - (void)testInitWithKeychainStore { GTMKeychainStore *store = [[GTMKeychainStore alloc] initWithItemName:@"foo"]; GIDSignIn *signIn; signIn = [[GIDSignIn alloc] initWithKeychainStore:store - authStateMigrationService:_authStateMigrationService]; + authStateMigrationService:_authStateMigrationService + bundle:_fakeMainBundle + userDefaults:_testUserDefaults]; XCTAssertNotNil(signIn.configuration); XCTAssertEqual(signIn.configuration.clientID, kClientId); XCTAssertNil(signIn.configuration.serverClientID); @@ -469,7 +458,9 @@ - (void)testInitWithKeychainStore_noConfig { GTMKeychainStore *store = [[GTMKeychainStore alloc] initWithItemName:@"foo"]; GIDSignIn *signIn; signIn = [[GIDSignIn alloc] initWithKeychainStore:store - authStateMigrationService:_authStateMigrationService]; + authStateMigrationService:_authStateMigrationService + bundle:_fakeMainBundle + userDefaults:_testUserDefaults]; XCTAssertNil(signIn.configuration); } @@ -482,7 +473,9 @@ - (void)testInitWithKeychainStore_fullConfig { GTMKeychainStore *store = [[GTMKeychainStore alloc] initWithItemName:@"foo"]; GIDSignIn *signIn; signIn = [[GIDSignIn alloc] initWithKeychainStore:store - authStateMigrationService:_authStateMigrationService]; + authStateMigrationService:_authStateMigrationService + bundle:_fakeMainBundle + userDefaults:_testUserDefaults]; XCTAssertNotNil(signIn.configuration); XCTAssertEqual(signIn.configuration.clientID, kClientId); XCTAssertEqual(signIn.configuration.serverClientID, kServerClientId); @@ -498,7 +491,9 @@ - (void)testInitWithKeychainStore_invalidConfig { GTMKeychainStore *store = [[GTMKeychainStore alloc] initWithItemName:@"foo"]; GIDSignIn *signIn; signIn = [[GIDSignIn alloc] initWithKeychainStore:store - authStateMigrationService:_authStateMigrationService]; + authStateMigrationService:_authStateMigrationService + bundle:_fakeMainBundle + userDefaults:_testUserDefaults]; XCTAssertNil(signIn.configuration); } @@ -512,7 +507,9 @@ - (void)testInitWithKeychainStore_attemptsMigration { GTMKeychainStore *store = [[GTMKeychainStore alloc] initWithItemName:kKeychainName]; GIDSignIn *signIn = [[GIDSignIn alloc] initWithKeychainStore:store - authStateMigrationService:_authStateMigrationService]; + authStateMigrationService:_authStateMigrationService + bundle:_fakeMainBundle + userDefaults:_testUserDefaults]; XCTAssertNotNil(signIn.configuration); [self waitForExpectationsWithTimeout:1 handler:nil]; @@ -1418,11 +1415,13 @@ - (void)testNotHandleWrongPath { #pragma mark - Test Fresh Install - (void)testFreshInstall_removesKeychainEntries { - // Simulate that the app has been deleted and user defaults removed. - [NSUserDefaults.standardUserDefaults removeObjectForKey:kAppHasRunBeforeKey]; + // Simulate that the app has been deleted and the key removed from the test user defaults. + [_testUserDefaults removeObjectForKey:kAppHasRunBeforeKey]; // Initialization should check `isFreshInstall`. GIDSignIn *signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore - authStateMigrationService:_authStateMigrationService]; + authStateMigrationService:_authStateMigrationService + bundle:_fakeMainBundle + userDefaults:_testUserDefaults]; // If `isFreshInstall`, keychain entries should be removed. XCTAssertNotNil(signIn); XCTAssertTrue(self->_keychainRemoved); From dd0dd206db57d23ac9093115cc0175ef6156c317 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:05:23 -0700 Subject: [PATCH 5/9] g-orchestrated: use real user defaults and a fake bundle in GIDAuthStateMigrationTest --- .../Tests/Unit/GIDAuthStateMigrationTest.m | 93 ++++++++++--------- 1 file changed, 51 insertions(+), 42 deletions(-) diff --git a/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m b/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m index 90664ae4..8ac75b21 100644 --- a/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m +++ b/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m @@ -15,6 +15,7 @@ #import #import "GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h" +#import "GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.h" #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h" #if TARGET_OS_OSX #import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h" @@ -56,6 +57,7 @@ static NSString *const kDataProtectedMigrationCheckPerformedKey = @"GID_DataProtectedMigrationCheckPerformed"; static NSString *const kFingerprintService = @"fingerprint"; +static NSString *const kUserDefaultsSuiteName = @"GIDAuthStateMigrationTestSuite"; NS_ASSUME_NONNULL_BEGIN @@ -76,12 +78,12 @@ @interface GIDAuthStateMigrationTest : XCTestCase @end @implementation GIDAuthStateMigrationTest { - id _mockUserDefaults; + NSUserDefaults *_userDefaults; id _mockGTMAppAuthFetcherAuthorization; id _mockGIDAuthStateMigration; id _mockGTMKeychainStore; id _mockKeychainHelper; - id _mockNSBundle; + GIDFakeBundle *_bundle; id _mockGIDSignInCallbackSchemes; id _mockGTMOAuth2Compatibility; #if TARGET_OS_OSX @@ -92,12 +94,14 @@ @implementation GIDAuthStateMigrationTest { - (void)setUp { [super setUp]; - _mockUserDefaults = OCMClassMock([NSUserDefaults class]); + _userDefaults = [[NSUserDefaults alloc] initWithSuiteName:kUserDefaultsSuiteName]; + [_userDefaults removePersistentDomainForName:kUserDefaultsSuiteName]; + _bundle = [[GIDFakeBundle alloc] init]; + _bundle.bundleIdentifier = kBundleID; _mockGTMAppAuthFetcherAuthorization = OCMStrictClassMock([GTMAuthSession class]); _mockGIDAuthStateMigration = OCMStrictClassMock([GIDAuthStateMigration class]); _mockGTMKeychainStore = OCMStrictClassMock([GTMKeychainStore class]); _mockKeychainHelper = OCMProtocolMock(@protocol(GTMKeychainHelper)); - _mockNSBundle = OCMStrictClassMock([NSBundle class]); _mockGIDSignInCallbackSchemes = OCMStrictClassMock([GIDSignInCallbackSchemes class]); _mockGTMOAuth2Compatibility = OCMStrictClassMock([GTMOAuth2Compatibility class]); #if TARGET_OS_OSX @@ -109,8 +113,7 @@ - (void)setUp { } - (void)tearDown { - [_mockUserDefaults verify]; - [_mockUserDefaults stopMocking]; + [_userDefaults removePersistentDomainForName:kUserDefaultsSuiteName]; [_mockGTMAppAuthFetcherAuthorization verify]; [_mockGTMAppAuthFetcherAuthorization stopMocking]; [_mockGIDAuthStateMigration verify]; @@ -119,8 +122,6 @@ - (void)tearDown { [_mockGTMKeychainStore stopMocking]; [_mockKeychainHelper verify]; [_mockKeychainHelper stopMocking]; - [_mockNSBundle verify]; - [_mockNSBundle stopMocking]; [_mockGIDSignInCallbackSchemes verify]; [_mockGIDSignInCallbackSchemes stopMocking]; [_mockGTMOAuth2Compatibility verify]; @@ -136,10 +137,6 @@ - (void)tearDown { #if TARGET_OS_OSX - (void)testMigrateIfNeeded_NoPreviousMigration_DataProtectedMigration { - [[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults]; - [[[_mockUserDefaults expect] andReturnValue:@NO] boolForKey:kDataProtectedMigrationCheckPerformedKey]; - [[_mockUserDefaults expect] setBool:YES forKey:kDataProtectedMigrationCheckPerformedKey]; - [[_mockGTMKeychainStore expect] saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]; [[[_mockGTMKeychainStore expect] andReturn:kKeychainName] itemName]; @@ -151,19 +148,19 @@ - (void)testMigrateIfNeeded_NoPreviousMigration_DataProtectedMigration { XCTAssertNil(err); GIDAuthStateMigration *migration = - [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore]; + [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore + userDefaults:_userDefaults + bundle:_bundle]; [migration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL] callbackPath:kCallbackPath isFreshInstall:NO]; + XCTAssertTrue([_userDefaults boolForKey:kDataProtectedMigrationCheckPerformedKey]); // verify that the auth session was removed during migration XCTAssertNil([_realLegacyGTMKeychainStore retrieveAuthSessionWithError:nil]); } - (void)testMigrateIfNeeded_KeychainFailure_DataProtectedMigration { - [[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults]; - [[[_mockUserDefaults expect] andReturnValue:@NO] boolForKey:kDataProtectedMigrationCheckPerformedKey]; - NSError *keychainSaveError = [NSError new]; OCMStub([_mockGTMKeychainStore saveAuthSession:OCMOCK_ANY error:[OCMArg setTo:keychainSaveError]]); @@ -175,51 +172,57 @@ - (void)testMigrateIfNeeded_KeychainFailure_DataProtectedMigration { XCTAssertNil(err); GIDAuthStateMigration *migration = - [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore]; + [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore + userDefaults:_userDefaults + bundle:_bundle]; [migration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL] callbackPath:kCallbackPath isFreshInstall:NO]; + XCTAssertTrue([_userDefaults boolForKey:kDataProtectedMigrationCheckPerformedKey]); XCTAssertNil([_realLegacyGTMKeychainStore retrieveAuthSessionWithError:nil]); } #elif TARGET_OS_IOS && !TARGET_OS_MACCATALYST - (void)testMigrateIfNeeded_NoPreviousMigration_GTMAppAuthMigration { - [[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults]; - [[[_mockUserDefaults expect] andReturnValue:@NO] boolForKey:kGTMAppAuthMigrationCheckPerformedKey]; - [[_mockUserDefaults expect] setBool:YES forKey:kGTMAppAuthMigrationCheckPerformedKey]; - [[_mockGTMKeychainStore expect] saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]; [self setUpCommonExtractAuthorizationMocksWithFingerPrint:kSavedFingerprint]; GIDAuthStateMigration *migration = - [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore]; + [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore + userDefaults:_userDefaults + bundle:_bundle]; [migration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL] callbackPath:kCallbackPath isFreshInstall:NO]; + + XCTAssertTrue([_userDefaults boolForKey:kGTMAppAuthMigrationCheckPerformedKey]); } - (void)testMigrateIfNeeded_KeychainFailure_GTMAppAuthMigration { - [[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults]; - [[[_mockUserDefaults expect] andReturnValue:@NO] boolForKey:kGTMAppAuthMigrationCheckPerformedKey]; - NSError *keychainSaveError = [NSError new]; OCMStub([_mockGTMKeychainStore saveAuthSession:OCMOCK_ANY error:[OCMArg setTo:keychainSaveError]]); [self setUpCommonExtractAuthorizationMocksWithFingerPrint:kSavedFingerprint]; GIDAuthStateMigration *migration = - [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore]; + [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore + userDefaults:_userDefaults + bundle:_bundle]; [migration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL] callbackPath:kCallbackPath isFreshInstall:NO]; + + XCTAssertTrue([_userDefaults boolForKey:kGTMAppAuthMigrationCheckPerformedKey]); } - (void)testExtractAuthorization { [self setUpCommonExtractAuthorizationMocksWithFingerPrint:kSavedFingerprint]; GIDAuthStateMigration *migration = - [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore]; + [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore + userDefaults:_userDefaults + bundle:_bundle]; GTMAuthSession *authorization = [migration extractAuthSessionWithTokenURL:[NSURL URLWithString:kTokenURL] callbackPath:kCallbackPath]; @@ -231,7 +234,9 @@ - (void)testExtractAuthorization_HostedDomain { [self setUpCommonExtractAuthorizationMocksWithFingerPrint:kSavedFingerprint_HostedDomain]; GIDAuthStateMigration *migration = - [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore]; + [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore + userDefaults:_userDefaults + bundle:_bundle]; GTMAuthSession *authorization = [migration extractAuthSessionWithTokenURL:[NSURL URLWithString:kTokenURL] callbackPath:kCallbackPath]; @@ -241,35 +246,41 @@ - (void)testExtractAuthorization_HostedDomain { #endif // TARGET_OS_OSX - (void)testMigrateIfNeeded_HasPreviousMigration { - [[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults]; #if TARGET_OS_OSX - [[[_mockUserDefaults expect] andReturnValue:@YES] boolForKey:kDataProtectedMigrationCheckPerformedKey]; - [[_mockUserDefaults reject] setBool:YES forKey:kDataProtectedMigrationCheckPerformedKey]; + [_userDefaults setBool:YES forKey:kDataProtectedMigrationCheckPerformedKey]; #elif TARGET_OS_IOS && !TARGET_OS_MACCATALYST - [[[_mockUserDefaults expect] andReturnValue:@YES] boolForKey:kGTMAppAuthMigrationCheckPerformedKey]; - [[_mockUserDefaults reject] setBool:YES forKey:kGTMAppAuthMigrationCheckPerformedKey]; + [_userDefaults setBool:YES forKey:kGTMAppAuthMigrationCheckPerformedKey]; #endif // TARGET_OS_OSX GIDAuthStateMigration *migration = - [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore]; + [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore + userDefaults:_userDefaults + bundle:_bundle]; [migration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL] callbackPath:kCallbackPath isFreshInstall:NO]; -} -- (void)testMigrateIfNeeded_isFreshInstall { - [[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults]; #if TARGET_OS_OSX - [[_mockUserDefaults expect] setBool:YES forKey:kDataProtectedMigrationCheckPerformedKey]; + XCTAssertTrue([_userDefaults boolForKey:kDataProtectedMigrationCheckPerformedKey]); #elif TARGET_OS_IOS && !TARGET_OS_MACCATALYST - [[_mockUserDefaults expect] setBool:YES forKey:kGTMAppAuthMigrationCheckPerformedKey]; + XCTAssertTrue([_userDefaults boolForKey:kGTMAppAuthMigrationCheckPerformedKey]); #endif // TARGET_OS_OSX +} +- (void)testMigrateIfNeeded_isFreshInstall { GIDAuthStateMigration *migration = - [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore]; + [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore + userDefaults:_userDefaults + bundle:_bundle]; [migration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL] callbackPath:kCallbackPath isFreshInstall:YES]; + +#if TARGET_OS_OSX + XCTAssertTrue([_userDefaults boolForKey:kDataProtectedMigrationCheckPerformedKey]); +#elif TARGET_OS_IOS && !TARGET_OS_MACCATALYST + XCTAssertTrue([_userDefaults boolForKey:kGTMAppAuthMigrationCheckPerformedKey]); +#endif // TARGET_OS_OSX } @@ -287,8 +298,6 @@ - (void)setUpCommonExtractAuthorizationMocksWithFingerPrint:(NSString *)fingerpr (void)[[[_mockKeychainHelper expect] andReturn:kGTMOAuth2PersistenceString] passwordForService:fingerprint error:OCMArg.anyObjectRef]; [[[_mockGTMKeychainStore expect] andReturn:_mockKeychainHelper] keychainHelper]; - [[[_mockNSBundle expect] andReturn:_mockNSBundle] mainBundle]; - [[[_mockNSBundle expect] andReturn:kBundleID] bundleIdentifier]; [[[_mockGIDSignInCallbackSchemes expect] andReturn:_mockGIDSignInCallbackSchemes] alloc]; (void)[[[_mockGIDSignInCallbackSchemes expect] andReturn:_mockGIDSignInCallbackSchemes] initWithClientIdentifier:kClientID bundle:OCMOCK_ANY]; From dc1112f3c9d675432b307e2c84df14b654991826 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:29:04 -0700 Subject: [PATCH 6/9] g-orchestrated: Follow the Objective-C style guide in the bundle seam --- .../Implementation/GIDAuthStateMigration.m | 23 ++++++----- .../Sources/GIDBundle/Fake/GIDFakeBundle.h | 12 +++++- .../Sources/GIDBundle/Fake/GIDFakeBundle.m | 11 +++++- GoogleSignIn/Sources/GIDSignIn.m | 7 +++- .../Sources/GIDSignInCallbackSchemes.h | 21 +++++----- .../Sources/GIDSignInCallbackSchemes.m | 16 ++++---- GoogleSignIn/Sources/GIDSignIn_Private.h | 11 +++--- .../Tests/Unit/GIDAuthStateMigrationTest.m | 5 ++- GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h | 10 ++++- GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m | 38 +++++++++---------- 10 files changed, 93 insertions(+), 61 deletions(-) diff --git a/GoogleSignIn/Sources/GIDAuthStateMigration/Implementation/GIDAuthStateMigration.m b/GoogleSignIn/Sources/GIDAuthStateMigration/Implementation/GIDAuthStateMigration.m index 69727055..ee008829 100644 --- a/GoogleSignIn/Sources/GIDAuthStateMigration/Implementation/GIDAuthStateMigration.m +++ b/GoogleSignIn/Sources/GIDAuthStateMigration/Implementation/GIDAuthStateMigration.m @@ -50,7 +50,9 @@ @interface GIDAuthStateMigration () @end @implementation GIDAuthStateMigration { + // The user defaults in which the migration-check flags are recorded. NSUserDefaults *_userDefaults; + // The bundle whose identifier fingerprints legacy auth state. id _bundle; } @@ -83,11 +85,10 @@ - (void)migrateIfNeededWithTokenURL:(NSURL *)tokenURL // If this is a fresh install, take no action and mark the migration checks as having been // performed. if (isFreshInstall) { - NSUserDefaults* defaults = _userDefaults; #if TARGET_OS_OSX - [defaults setBool:YES forKey:kDataProtectedMigrationCheckPerformedKey]; + [_userDefaults setBool:YES forKey:kDataProtectedMigrationCheckPerformedKey]; #elif TARGET_OS_IOS && !TARGET_OS_MACCATALYST - [defaults setBool:YES forKey:kGTMAppAuthMigrationCheckPerformedKey]; + [_userDefaults setBool:YES forKey:kGTMAppAuthMigrationCheckPerformedKey]; #endif // TARGET_OS_OSX return; } @@ -104,8 +105,7 @@ - (void)migrateIfNeededWithTokenURL:(NSURL *)tokenURL // Migrate from the fileBasedKeychain to dataProtectedKeychain with GTMAppAuth 5.0. - (void)performDataProtectedMigrationIfNeeded { // See if we've performed the migration check previously. - NSUserDefaults* defaults = _userDefaults; - if ([defaults boolForKey:kDataProtectedMigrationCheckPerformedKey]) { + if ([_userDefaults boolForKey:kDataProtectedMigrationCheckPerformedKey]) { return; } @@ -123,7 +123,7 @@ - (void)performDataProtectedMigrationIfNeeded { } // Mark the migration check as having been performed. - [defaults setBool:YES forKey:kDataProtectedMigrationCheckPerformedKey]; + [_userDefaults setBool:YES forKey:kDataProtectedMigrationCheckPerformedKey]; } #elif TARGET_OS_IOS && !TARGET_OS_MACCATALYST @@ -132,8 +132,7 @@ - (void)performDataProtectedMigrationIfNeeded { - (void)performGIDMigrationIfNeededWithTokenURL:(NSURL *)tokenURL callbackPath:(NSString *)callbackPath { // See if we've performed the migration check previously. - NSUserDefaults* defaults = _userDefaults; - if ([defaults boolForKey:kGTMAppAuthMigrationCheckPerformedKey]) { + if ([_userDefaults boolForKey:kGTMAppAuthMigrationCheckPerformedKey]) { return; } @@ -148,7 +147,7 @@ - (void)performGIDMigrationIfNeededWithTokenURL:(NSURL *)tokenURL } // Mark the migration check as having been performed. - [defaults setBool:YES forKey:kGTMAppAuthMigrationCheckPerformedKey]; + [_userDefaults setBool:YES forKey:kGTMAppAuthMigrationCheckPerformedKey]; } // Returns a |GTMAuthSession| object containing any old auth state or |nil| if none @@ -189,9 +188,9 @@ - (nullable GTMAuthSession *)extractAuthSessionWithTokenURL:(NSURL *)tokenURL NSString *clientID = [fingerprint substringWithRange:[matches[0] rangeAtIndex:1]]; // Generate the redirect URI from the extracted client ID. - NSString *scheme = [[[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:clientID - bundle:_bundle] - clientIdentifierScheme]; + GIDSignInCallbackSchemes *schemes = + [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:clientID bundle:_bundle]; + NSString *scheme = [schemes clientIdentifierScheme]; NSString *redirectURI = [NSString stringWithFormat:@"%@:%@", scheme, callbackPath]; // Retrieve the additional token request parameters value. diff --git a/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.h b/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.h index b071210c..b0f57b81 100644 --- a/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.h +++ b/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.h @@ -20,12 +20,22 @@ NS_ASSUME_NONNULL_BEGIN -/// An in-memory `GIDBundle` for tests. Owned by the `GIDBundle` seam. +/// An in-memory `GIDBundle` for tests. Tests populate it with the Info.plist values and bundle +/// identifier the code under test reads. @interface GIDFakeBundle : NSObject /// The bundle identifier to report. Defaults to nil. @property(nonatomic, copy, nullable) NSString *bundleIdentifier; +/// Creates a bundle that reports `bundleIdentifier` and whose Info.plist holds the entries of +/// `infoDictionary`. +- (instancetype)initWithBundleIdentifier:(nullable NSString *)bundleIdentifier + infoDictionary:(NSDictionary *)infoDictionary + NS_DESIGNATED_INITIALIZER; + +/// Creates a bundle with no identifier and an empty Info.plist. +- (instancetype)init; + /// Sets (or, with nil, removes) the value returned by `-objectForInfoDictionaryKey:` for `key`. - (void)setObject:(nullable id)object forInfoDictionaryKey:(NSString *)key; diff --git a/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.m b/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.m index 5d7f8632..077d653e 100644 --- a/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.m +++ b/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.m @@ -19,19 +19,26 @@ NS_ASSUME_NONNULL_BEGIN @implementation GIDFakeBundle { + // The entries `-objectForInfoDictionaryKey:` reads. NSMutableDictionary *_infoDictionary; } @synthesize bundleIdentifier = _bundleIdentifier; -- (instancetype)init { +- (instancetype)initWithBundleIdentifier:(nullable NSString *)bundleIdentifier + infoDictionary:(NSDictionary *)infoDictionary { self = [super init]; if (self) { - _infoDictionary = [NSMutableDictionary dictionary]; + _bundleIdentifier = [bundleIdentifier copy]; + _infoDictionary = [infoDictionary mutableCopy]; } return self; } +- (instancetype)init { + return [self initWithBundleIdentifier:nil infoDictionary:@{}]; +} + - (nullable id)objectForInfoDictionaryKey:(NSString *)key { return _infoDictionary[key]; } diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index 4577ccf1..6638720a 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -194,7 +194,9 @@ @implementation GIDSignIn { BOOL _restarting; // Keychain manager for GTMAppAuth GTMKeychainStore *_keychainStore; + // The bundle whose Info.plist supplies the configuration and URL schemes. id _bundle; + // The user defaults that record whether the app has run before. NSUserDefaults *_userDefaults; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST // The class used to manage presenting the loading screen for fetching app check tokens. @@ -1403,7 +1405,8 @@ - (GIDProfileData *)profileDataWithIDToken:(OIDIDToken *)idToken { imageURL:[NSURL URLWithString:idToken.claims[kBasicProfilePictureKey]]]; } -// Try to retrieve a configuration value from the bundle's Info.plist for a given key. +// Returns the string value for `key` in the bundle's Info.plist, or nil if there is none or it +// is not a string. + (nullable NSString *)configValueFromBundle:(id)bundle forKey:(NSString *)key { NSString *value; id configValue = [bundle objectForInfoDictionaryKey:key]; @@ -1413,7 +1416,7 @@ + (nullable NSString *)configValueFromBundle:(id)bundle forKey:(NSStr return value; } -// Try to generate a |GIDConfiguration| from the bundle's Info.plist. +// Returns a |GIDConfiguration| built from the bundle's Info.plist, or nil if it has no client ID. + (nullable GIDConfiguration *)configurationFromBundle:(id)bundle { GIDConfiguration *configuration; diff --git a/GoogleSignIn/Sources/GIDSignInCallbackSchemes.h b/GoogleSignIn/Sources/GIDSignInCallbackSchemes.h index 98e16576..3719a500 100644 --- a/GoogleSignIn/Sources/GIDSignInCallbackSchemes.h +++ b/GoogleSignIn/Sources/GIDSignInCallbackSchemes.h @@ -20,29 +20,30 @@ NS_ASSUME_NONNULL_BEGIN -// A utility class for dealing with callback schemes. +/// A utility class for dealing with callback schemes. @interface GIDSignInCallbackSchemes : NSObject -// Please call the designated initializer. +/// Unavailable. Use `-initWithClientIdentifier:bundle:` or `-initWithClientIdentifier:`. - (instancetype)init NS_UNAVAILABLE; -// Creates an instance that validates the host app's URL schemes against `bundle`'s Info.plist. +/// Creates an instance that validates the host app's URL schemes against `bundle`'s Info.plist. - (instancetype)initWithClientIdentifier:(NSString *)clientIdentifier bundle:(id)bundle NS_DESIGNATED_INITIALIZER; -// Same as above, using `[NSBundle mainBundle]`. +/// Creates an instance that validates the host app's URL schemes against the main bundle's +/// Info.plist. - (instancetype)initWithClientIdentifier:(NSString *)clientIdentifier; -// The canonical client identifier callback scheme. Requires clientId to be set on GIDSignIn. +/// The canonical client identifier callback scheme. Requires clientId to be set on GIDSignIn. - (NSString *)clientIdentifierScheme; -// An array of all schemes used for sign-in callbacks. -- (NSArray *)allSchemes; +/// An array of all schemes used for sign-in callbacks. +- (NSArray *)allSchemes; -// Returns a list of URL schemes the current app host should support for Google Sign-In to work. -- (NSMutableArray *)unsupportedSchemes; +/// Returns a list of URL schemes the current app host should support for Google Sign-In to work. +- (NSMutableArray *)unsupportedSchemes; -// Indicates the scheme of an NSURL is a sign-in callback scheme. +/// Indicates the scheme of an NSURL is a sign-in callback scheme. - (BOOL)URLSchemeIsCallbackScheme:(NSURL *)URL; @end diff --git a/GoogleSignIn/Sources/GIDSignInCallbackSchemes.m b/GoogleSignIn/Sources/GIDSignInCallbackSchemes.m index 58d44c1f..e20e1434 100644 --- a/GoogleSignIn/Sources/GIDSignInCallbackSchemes.m +++ b/GoogleSignIn/Sources/GIDSignInCallbackSchemes.m @@ -20,6 +20,7 @@ @implementation GIDSignInCallbackSchemes { NSString *_clientIdentifier; + // The bundle whose Info.plist declares the host app's URL schemes. id _bundle; } @@ -30,9 +31,10 @@ @implementation GIDSignInCallbackSchemes { * support for. * @remarks Branched from google3/googlemac/iPhone/Firebase/Source/GGLBundleUtil.m */ -- (NSArray *)relevantURLSchemes { - NSMutableArray *result = [NSMutableArray array]; - NSArray *urlTypes = [_bundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]; +- (NSArray *)relevantURLSchemes { + NSMutableArray *result = [NSMutableArray array]; + NSArray *> *urlTypes = + [_bundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]; for (NSDictionary *urlType in urlTypes) { NSArray *urlTypeSchemes = urlType[@"CFBundleURLSchemes"]; for (NSString *urlTypeScheme in urlTypeSchemes) { @@ -63,8 +65,8 @@ - (NSString *)clientIdentifierScheme { return reversedClientIdentifier.lowercaseString; } -- (NSArray *)allSchemes { - NSMutableArray *schemes = [NSMutableArray array]; +- (NSArray *)allSchemes { + NSMutableArray *schemes = [NSMutableArray array]; NSString *clientIdentifierScheme = [self clientIdentifierScheme]; if (clientIdentifierScheme) { [schemes addObject:clientIdentifierScheme]; @@ -72,9 +74,9 @@ - (NSArray *)allSchemes { return schemes; } -- (NSMutableArray *)unsupportedSchemes { +- (NSMutableArray *)unsupportedSchemes { NSMutableArray *unsupportedSchemes = [NSMutableArray arrayWithArray:[self allSchemes]]; - NSArray *supportedSchemes = [self relevantURLSchemes]; + NSArray *supportedSchemes = [self relevantURLSchemes]; [unsupportedSchemes removeObjectsInArray:supportedSchemes]; return unsupportedSchemes; } diff --git a/GoogleSignIn/Sources/GIDSignIn_Private.h b/GoogleSignIn/Sources/GIDSignIn_Private.h index 09df27ef..ad19ee1e 100644 --- a/GoogleSignIn/Sources/GIDSignIn_Private.h +++ b/GoogleSignIn/Sources/GIDSignIn_Private.h @@ -18,14 +18,14 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" -#import "GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h" - #if __has_include() #import #elif __has_include() #import #endif +#import "GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h" + NS_ASSUME_NONNULL_BEGIN @class GIDGoogleUser; @@ -63,12 +63,13 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); API_AVAILABLE(ios(14)); #endif // TARGET_OS_IOS || !TARGET_OS_MACCATALYST -/// Private initializer taking a `GTMKeychainStore`, the bundle whose Info.plist supplies the -/// configuration and URL schemes, and the user defaults that record whether the app has run before. +/// Creates an instance with the keychain store, the migration service, the bundle whose Info.plist +/// supplies the configuration and URL schemes, and the user defaults that record whether the app +/// has run before. The designated initializer; the other initializers call through to it. - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore authStateMigrationService:(GIDAuthStateMigration *)authStateMigrationService bundle:(id)bundle - userDefaults:(NSUserDefaults *)userDefaults; + userDefaults:(NSUserDefaults *)userDefaults NS_DESIGNATED_INITIALIZER; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST /// Same as above, also taking the `GIDAppCheck` to use. diff --git a/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m b/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m index 8ac75b21..501a50fb 100644 --- a/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m +++ b/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m @@ -78,11 +78,13 @@ @interface GIDAuthStateMigrationTest : XCTestCase @end @implementation GIDAuthStateMigrationTest { + // Suite-scoped user defaults injected into the migration under test. NSUserDefaults *_userDefaults; id _mockGTMAppAuthFetcherAuthorization; id _mockGIDAuthStateMigration; id _mockGTMKeychainStore; id _mockKeychainHelper; + // The bundle injected into the migration under test. GIDFakeBundle *_bundle; id _mockGIDSignInCallbackSchemes; id _mockGTMOAuth2Compatibility; @@ -96,8 +98,7 @@ - (void)setUp { _userDefaults = [[NSUserDefaults alloc] initWithSuiteName:kUserDefaultsSuiteName]; [_userDefaults removePersistentDomainForName:kUserDefaultsSuiteName]; - _bundle = [[GIDFakeBundle alloc] init]; - _bundle.bundleIdentifier = kBundleID; + _bundle = [[GIDFakeBundle alloc] initWithBundleIdentifier:kBundleID infoDictionary:@{}]; _mockGTMAppAuthFetcherAuthorization = OCMStrictClassMock([GTMAuthSession class]); _mockGIDAuthStateMigration = OCMStrictClassMock([GIDAuthStateMigration class]); _mockGTMKeychainStore = OCMStrictClassMock([GTMKeychainStore class]); diff --git a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h index f1cb26c7..38e3d2ec 100644 --- a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h +++ b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h @@ -28,7 +28,15 @@ * @brief Creates a bundle whose `GIDClientID` Info.plist value is `clientID`. * @param clientID The fake client identifier for the app. */ -- (instancetype)initWithClientID:(NSString *)clientID; +- (instancetype)initWithClientID:(NSString *)clientID NS_DESIGNATED_INITIALIZER; + +/** Unavailable. Use `-initWithClientID:`. */ +- (instancetype)init NS_UNAVAILABLE; + +/** Unavailable. Use `-initWithClientID:`. */ +- (instancetype)initWithBundleIdentifier:(nullable NSString *)bundleIdentifier + infoDictionary:(NSDictionary *)infoDictionary + NS_UNAVAILABLE; #pragma mark - URL Schemes diff --git a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m index cc67afb2..0c656f13 100644 --- a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m +++ b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m @@ -28,11 +28,11 @@ @implementation GIDFakeMainBundle { NSString *_clientId; } -- (instancetype)initWithClientID:(NSString *)clientId { - self = [super init]; +- (instancetype)initWithClientID:(NSString *)clientID { + self = [super initWithBundleIdentifier:nil + infoDictionary:@{kConfigClientIDKey : clientID}]; if (self) { - _clientId = clientId; - [self setObject:clientId forInfoDictionaryKey:kConfigClientIDKey]; + _clientId = [clientID copy]; } return self; } @@ -76,34 +76,34 @@ - (NSString *)stringByFlippingCasesInString:(NSString *)original { #pragma mark - URL Schemes - (void)fakeAllSchemesSupported { - [self setObject:@[ + NSArray *> *urlTypes = @[ @{ kCFBundleURLSchemesKey : @[ [self reversedClientId] ] } - ] - forInfoDictionaryKey:kCFBundleURLTypesKey]; + ]; + [self setObject:urlTypes forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeAllSchemesSupportedAndMerged { - [self setObject:@[ + NSArray *> *urlTypes = @[ @{ kCFBundleURLSchemesKey : @[ [self reversedClientId] ] }, - ] - forInfoDictionaryKey:kCFBundleURLTypesKey]; + ]; + [self setObject:urlTypes forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeAllSchemesSupportedWithCasesMangled { NSString *caseFlippedReverseClientId = [self stringByFlippingCasesInString:[self reversedClientId]]; - [self setObject:@[ + NSArray *> *urlTypes = @[ @{ kCFBundleURLSchemesKey : @[ caseFlippedReverseClientId ] } - ] - forInfoDictionaryKey:kCFBundleURLTypesKey]; + ]; + [self setObject:urlTypes forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeMissingClientIdScheme { @@ -115,24 +115,24 @@ - (void)fakeMissingAllSchemes { } - (void)fakeOtherSchemes { - [self setObject:@[ + NSArray *> *urlTypes = @[ @{ kCFBundleURLSchemesKey : @[ @"junk" ] } - ] - forInfoDictionaryKey:kCFBundleURLTypesKey]; + ]; + [self setObject:urlTypes forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeOtherSchemesAndAllSchemes { - [self setObject:@[ + NSArray *> *urlTypes = @[ @{ kCFBundleURLSchemesKey : @[ @"junk" ] }, @{ kCFBundleURLSchemesKey : @[ [self reversedClientId] ] } - ] - forInfoDictionaryKey:kCFBundleURLTypesKey]; + ]; + [self setObject:urlTypes forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeWithClientID:(id)clientID From 721f4830cb1c0bc43601444a0ee30e16fe92f367 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:43:06 -0700 Subject: [PATCH 7/9] g-orchestrated: Pass the bundle into relevantURLSchemes explicitly --- GoogleSignIn/Sources/GIDSignInCallbackSchemes.m | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignInCallbackSchemes.m b/GoogleSignIn/Sources/GIDSignInCallbackSchemes.m index e20e1434..dee87a8a 100644 --- a/GoogleSignIn/Sources/GIDSignInCallbackSchemes.m +++ b/GoogleSignIn/Sources/GIDSignInCallbackSchemes.m @@ -25,16 +25,17 @@ @implementation GIDSignInCallbackSchemes { } /** - * @fn relevantURLSchemes - * @brief Extracts CFBundleURLSchemes from the injected bundle's Info.plist. + * @fn relevantURLSchemesInBundle: + * @brief Extracts CFBundleURLSchemes from a bundle's Info.plist. + * @param bundle The bundle whose Info.plist declares the URL schemes. * @return An array of lowercase NSString *'s representing the URL schemes the bundle has declared * support for. * @remarks Branched from google3/googlemac/iPhone/Firebase/Source/GGLBundleUtil.m */ -- (NSArray *)relevantURLSchemes { ++ (NSArray *)relevantURLSchemesInBundle:(id)bundle { NSMutableArray *result = [NSMutableArray array]; NSArray *> *urlTypes = - [_bundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]; + [bundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]; for (NSDictionary *urlType in urlTypes) { NSArray *urlTypeSchemes = urlType[@"CFBundleURLSchemes"]; for (NSString *urlTypeScheme in urlTypeSchemes) { @@ -76,7 +77,7 @@ - (NSString *)clientIdentifierScheme { - (NSMutableArray *)unsupportedSchemes { NSMutableArray *unsupportedSchemes = [NSMutableArray arrayWithArray:[self allSchemes]]; - NSArray *supportedSchemes = [self relevantURLSchemes]; + NSArray *supportedSchemes = [[self class] relevantURLSchemesInBundle:_bundle]; [unsupportedSchemes removeObjectsInArray:supportedSchemes]; return unsupportedSchemes; } From 0680509e9eaea0255474056f94719cb44a3ae723 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:40:44 -0700 Subject: [PATCH 8/9] g-orchestrated: Document that the injected bundle must be the running app's --- .../Sources/GIDAuthStateMigration/GIDAuthStateMigration.h | 4 ++++ GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h | 2 ++ GoogleSignIn/Sources/GIDSignIn_Private.h | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h b/GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h index 2559734f..987a582e 100644 --- a/GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h +++ b/GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h @@ -29,6 +29,10 @@ NS_ASSUME_NONNULL_BEGIN /// Creates an instance of this migration type with the keychain storage wrapper it will use, the /// user defaults in which it records that a migration check has run, and the bundle whose /// identifier was used to fingerprint legacy auth state. +/// +/// `bundle` must report the identifier of the app that wrote the legacy keychain entries, which in +/// production is `NSBundle.mainBundle`. With any other identifier the fingerprint never matches and +/// the migration silently finds nothing. - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore userDefaults:(NSUserDefaults *)userDefaults bundle:(id)bundle NS_DESIGNATED_INITIALIZER; diff --git a/GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h b/GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h index 2327a66b..1bd2ad0f 100644 --- a/GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h +++ b/GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h @@ -22,6 +22,8 @@ NS_ASSUME_NONNULL_BEGIN /// /// The SDK depends on this protocol rather than on `NSBundle` directly so that the host app's /// `Info.plist` can be substituted in tests. `NSBundle` conforms; see `NSBundle+GIDBundle.h`. +/// Production code always passes `NSBundle.mainBundle`: the URL schemes and bundle identifier the +/// SDK reads here must match the app the OS is actually running. @protocol GIDBundle /// The receiver's bundle identifier, if any. diff --git a/GoogleSignIn/Sources/GIDSignIn_Private.h b/GoogleSignIn/Sources/GIDSignIn_Private.h index ad19ee1e..9f9bfc13 100644 --- a/GoogleSignIn/Sources/GIDSignIn_Private.h +++ b/GoogleSignIn/Sources/GIDSignIn_Private.h @@ -66,6 +66,11 @@ API_AVAILABLE(ios(14)); /// Creates an instance with the keychain store, the migration service, the bundle whose Info.plist /// supplies the configuration and URL schemes, and the user defaults that record whether the app /// has run before. The designated initializer; the other initializers call through to it. +/// +/// `bundle` must describe the running app. The OS delivers sign-in callbacks to the URL schemes +/// declared in the main bundle's Info.plist, and `-signInWithOptions:` raises +/// `NSInvalidArgumentException` when the bundle it is given does not declare them. Production code +/// passes `NSBundle.mainBundle`; anything else is for tests only. - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore authStateMigrationService:(GIDAuthStateMigration *)authStateMigrationService bundle:(id)bundle From c4c5b42c8a97954bd50d744c4e29c4022194d6a9 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:38:36 -0700 Subject: [PATCH 9/9] g-orchestrated: annotate nullability in GIDFakeMainBundle.h --- GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h index 38e3d2ec..33cd7330 100644 --- a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h +++ b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h @@ -16,6 +16,8 @@ #import "GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.h" +NS_ASSUME_NONNULL_BEGIN + /** * @class GIDFakeMainBundle * @brief A `GIDFakeBundle` pre-populated the way a host app's Info.plist would be for Google @@ -90,9 +92,11 @@ * @param hostedDomain The fake hosted domain for the app. * @param openIDRealm The fake OpenID realm for the app. */ -- (void)fakeWithClientID:(id)clientID - serverClientID:(id)serverClientID - hostedDomain:(id)hostedDomain - openIDRealm:(id)openIDRealm; +- (void)fakeWithClientID:(nullable id)clientID + serverClientID:(nullable id)serverClientID + hostedDomain:(nullable id)hostedDomain + openIDRealm:(nullable id)openIDRealm; @end + +NS_ASSUME_NONNULL_END