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..987a582e 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,19 @@ 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. +/// +/// `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; + +/// 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..ee008829 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,31 @@ @interface GIDAuthStateMigration () @end -@implementation GIDAuthStateMigration +@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; +} -- (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,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 = [NSUserDefaults standardUserDefaults]; #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; } @@ -89,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 = [NSUserDefaults standardUserDefaults]; - if ([defaults boolForKey:kDataProtectedMigrationCheckPerformedKey]) { + if ([_userDefaults boolForKey:kDataProtectedMigrationCheckPerformedKey]) { return; } @@ -108,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 @@ -117,8 +132,7 @@ - (void)performDataProtectedMigrationIfNeeded { - (void)performGIDMigrationIfNeededWithTokenURL:(NSURL *)tokenURL callbackPath:(NSString *)callbackPath { // See if we've performed the migration check previously. - NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; - if ([defaults boolForKey:kGTMAppAuthMigrationCheckPerformedKey]) { + if ([_userDefaults boolForKey:kGTMAppAuthMigrationCheckPerformedKey]) { return; } @@ -133,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 @@ -155,7 +169,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 +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] 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/API/GIDBundle.h b/GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h new file mode 100644 index 00000000..1bd2ad0f --- /dev/null +++ b/GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h @@ -0,0 +1,37 @@ +/* + * 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`. +/// 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. +@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..b0f57b81 --- /dev/null +++ b/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.h @@ -0,0 +1,44 @@ +/* + * 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. 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; + +@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..077d653e --- /dev/null +++ b/GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.m @@ -0,0 +1,56 @@ +/* + * 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 { + // The entries `-objectForInfoDictionaryKey:` reads. + NSMutableDictionary *_infoDictionary; +} + +@synthesize bundleIdentifier = _bundleIdentifier; + +- (instancetype)initWithBundleIdentifier:(nullable NSString *)bundleIdentifier + infoDictionary:(NSDictionary *)infoDictionary { + self = [super init]; + if (self) { + _bundleIdentifier = [bundleIdentifier copy]; + _infoDictionary = [infoDictionary mutableCopy]; + } + return self; +} + +- (instancetype)init { + return [self initWithBundleIdentifier:nil infoDictionary:@{}]; +} + +- (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 diff --git a/GoogleSignIn/Sources/GIDDevice/API/GIDDevice.h b/GoogleSignIn/Sources/GIDDevice/API/GIDDevice.h new file mode 100644 index 00000000..c91ac861 --- /dev/null +++ b/GoogleSignIn/Sources/GIDDevice/API/GIDDevice.h @@ -0,0 +1,39 @@ +/* + * 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 + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// The subset of `UIDevice` that Google Sign-In reads when describing the device to Google's EMM +/// (Enterprise Mobility Management) backend. `UIDevice` conforms; see `UIDevice+GIDDevice.h`. +@protocol GIDDevice + +/// The name of the operating system, e.g. "iOS". +@property(nonatomic, readonly, copy) NSString *systemName; + +/// The version of the operating system, e.g. "17.0.1". +@property(nonatomic, readonly, copy) NSString *systemVersion; + +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDDevice/Fake/GIDFakeDevice.h b/GoogleSignIn/Sources/GIDDevice/Fake/GIDFakeDevice.h new file mode 100644 index 00000000..38ef5b72 --- /dev/null +++ b/GoogleSignIn/Sources/GIDDevice/Fake/GIDFakeDevice.h @@ -0,0 +1,47 @@ +/* + * 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 + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +#import + +#import "GoogleSignIn/Sources/GIDDevice/API/GIDDevice.h" + +NS_ASSUME_NONNULL_BEGIN + +/// An in-memory `GIDDevice` for tests. +@interface GIDFakeDevice : NSObject + +/// The operating system name to report. +@property(nonatomic, copy) NSString *systemName; + +/// The operating system version to report. +@property(nonatomic, copy) NSString *systemVersion; + +/// Initializes a fake device reporting `systemName` and `systemVersion`. +- (instancetype)initWithSystemName:(NSString *)systemName + systemVersion:(NSString *)systemVersion NS_DESIGNATED_INITIALIZER; + +/// Unavailable. Use `-initWithSystemName:systemVersion:`. +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDDevice/Fake/GIDFakeDevice.m b/GoogleSignIn/Sources/GIDDevice/Fake/GIDFakeDevice.m new file mode 100644 index 00000000..71fcf5d9 --- /dev/null +++ b/GoogleSignIn/Sources/GIDDevice/Fake/GIDFakeDevice.m @@ -0,0 +1,44 @@ +/* + * 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 + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +#import "GoogleSignIn/Sources/GIDDevice/Fake/GIDFakeDevice.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation GIDFakeDevice + +@synthesize systemName = _systemName; +@synthesize systemVersion = _systemVersion; + +- (instancetype)initWithSystemName:(NSString *)systemName + systemVersion:(NSString *)systemVersion { + self = [super init]; + if (self) { + _systemName = [systemName copy]; + _systemVersion = [systemVersion copy]; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDDevice/Implementation/UIDevice+GIDDevice.h b/GoogleSignIn/Sources/GIDDevice/Implementation/UIDevice+GIDDevice.h new file mode 100644 index 00000000..b7d98b56 --- /dev/null +++ b/GoogleSignIn/Sources/GIDDevice/Implementation/UIDevice+GIDDevice.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 + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +#import + +#import "GoogleSignIn/Sources/GIDDevice/API/GIDDevice.h" + +NS_ASSUME_NONNULL_BEGIN + +/// `UIDevice` already implements every `GIDDevice` requirement; this category only declares the +/// conformance. +@interface UIDevice (GIDDevice) +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDDevice/Implementation/UIDevice+GIDDevice.m b/GoogleSignIn/Sources/GIDDevice/Implementation/UIDevice+GIDDevice.m new file mode 100644 index 00000000..51237e61 --- /dev/null +++ b/GoogleSignIn/Sources/GIDDevice/Implementation/UIDevice+GIDDevice.m @@ -0,0 +1,30 @@ +/* + * 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 + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +#import "GoogleSignIn/Sources/GIDDevice/Implementation/UIDevice+GIDDevice.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation UIDevice (GIDDevice) +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDEMMSupport.h b/GoogleSignIn/Sources/GIDEMMSupport.h index 7a5de88f..e9c4f602 100644 --- a/GoogleSignIn/Sources/GIDEMMSupport.h +++ b/GoogleSignIn/Sources/GIDEMMSupport.h @@ -22,25 +22,54 @@ @import GTMAppAuth; +#import "GoogleSignIn/Sources/GIDDevice/API/GIDDevice.h" +#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" + NS_ASSUME_NONNULL_BEGIN /// A class to support EMM (Enterprise Mobility Management). @interface GIDEMMSupport : NSObject -- (instancetype)init NS_DESIGNATED_INITIALIZER; +/// Creates an instance that reports EMM errors through `errorHandler` and describes the device +/// to the EMM backend using `device`. +- (instancetype)initWithErrorHandler:(GIDEMMErrorHandler *)errorHandler + device:(id)device NS_DESIGNATED_INITIALIZER; + +/// Creates an instance using `+[GIDEMMErrorHandler sharedInstance]` and `[UIDevice currentDevice]`. +- (instancetype)init; + +/// Handles potential EMM error from token fetch response. +- (void)handleTokenFetchEMMError:(nullable NSError *)error + completion:(void (^)(NSError *_Nullable))completion; + +/// Returns a new set of URL parameters that contains updated EMM-related URL parameters if needed. +- (NSDictionary *)updatedEMMParametersWithParameters: + (NSDictionary *)parameters; + +/// Returns a new set of URL parameters that also contains EMM-related URL parameters if needed. +/// +/// @note This function only returns parameter keys in the `NSDictionary` for string/number/bools. +- (NSDictionary *)parametersWithParameters: + (NSDictionary *)parameters + emmSupport:(nullable NSString *)emmSupport + isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired; /// Handles potential EMM error from token fetch response. +/// Equivalent to calling the instance method on `[[GIDEMMSupport alloc] init]`. + (void)handleTokenFetchEMMError:(nullable NSError *)error completion:(void (^)(NSError *_Nullable))completion; -/// Gets a new set of URL parameters that contains updated EMM-related URL parameters if needed. +/// Returns a new set of URL parameters that contains updated EMM-related URL parameters if needed. +/// Equivalent to calling the instance method on `[[GIDEMMSupport alloc] init]`. + (NSDictionary *)updatedEMMParametersWithParameters: - (NSDictionary *)parameters; + (NSDictionary *)parameters; -/// Gets a new set of URL parameters that also contains EMM-related URL parameters if needed. +/// Returns a new set of URL parameters that also contains EMM-related URL parameters if needed. +/// Equivalent to calling the instance method on `[[GIDEMMSupport alloc] init]`. /// /// @note This function only returns parameter keys in the `NSDictionary` for string/number/bools. -+ (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters ++ (NSDictionary *)parametersWithParameters: + (NSDictionary *)parameters emmSupport:(nullable NSString *)emmSupport isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired; diff --git a/GoogleSignIn/Sources/GIDEMMSupport.m b/GoogleSignIn/Sources/GIDEMMSupport.m index 1940df4f..2f8f3ef3 100644 --- a/GoogleSignIn/Sources/GIDEMMSupport.m +++ b/GoogleSignIn/Sources/GIDEMMSupport.m @@ -22,6 +22,7 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" +#import "GoogleSignIn/Sources/GIDDevice/Implementation/UIDevice+GIDDevice.h" #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" #import "GoogleSignIn/Sources/GIDMDMPasscodeState.h" @@ -58,18 +59,34 @@ typedef NS_ENUM(NSInteger, ErrorCode) { ErrorCodeAppVerificationRequired, }; -@implementation GIDEMMSupport +@implementation GIDEMMSupport { + // Reports EMM errors from token fetch responses. + GIDEMMErrorHandler *_errorHandler; + // Describes the device to the EMM backend. + id _device; +} + +- (instancetype)initWithErrorHandler:(GIDEMMErrorHandler *)errorHandler + device:(id)device { + self = [super init]; + if (self) { + _errorHandler = errorHandler; + _device = device; + } + return self; +} - (instancetype)init { - return [super init]; + return [self initWithErrorHandler:[GIDEMMErrorHandler sharedInstance] + device:[UIDevice currentDevice]]; } -+ (void)handleTokenFetchEMMError:(nullable NSError *)error +- (void)handleTokenFetchEMMError:(nullable NSError *)error completion:(void (^)(NSError *_Nullable))completion { NSDictionary *errorJSON = error.userInfo[OIDOAuthErrorResponseErrorKey]; if (errorJSON) { - [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:errorJSON - completion:^(BOOL handled) { + [_errorHandler handleErrorFromResponse:errorJSON + completion:^(BOOL handled) { if (handled) { completion([NSError errorWithDomain:kGIDSignInErrorDomain code:kGIDSignInErrorCodeEMM @@ -83,14 +100,15 @@ + (void)handleTokenFetchEMMError:(nullable NSError *)error } } -+ (NSDictionary *)updatedEMMParametersWithParameters: - (NSDictionary *)parameters { +- (NSDictionary *)updatedEMMParametersWithParameters: + (NSDictionary *)parameters { return [self parametersWithParameters:parameters emmSupport:parameters[kEMMSupportParameterName] isPasscodeInfoRequired:parameters[kEMMPasscodeInfoParameterName] != nil]; } -+ (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters +- (NSDictionary *)parametersWithParameters: + (NSDictionary *)parameters emmSupport:(nullable NSString *)emmSupport isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired { if (!emmSupport) { @@ -98,31 +116,49 @@ + (void)handleTokenFetchEMMError:(nullable NSError *)error } NSMutableDictionary *allParameters = [(parameters ?: @{}) mutableCopy]; allParameters[kEMMSupportParameterName] = emmSupport; - UIDevice *device = [UIDevice currentDevice]; - NSString *systemName = device.systemName; + NSString *systemName = _device.systemName; if ([systemName isEqualToString:kOldIOSSystemName]) { systemName = kNewIOSSystemName; } allParameters[kEMMOSVersionParameterName] = - [NSString stringWithFormat:@"%@ %@", systemName, device.systemVersion]; + [NSString stringWithFormat:@"%@ %@", systemName, _device.systemVersion]; if (isPasscodeInfoRequired) { allParameters[kEMMPasscodeInfoParameterName] = [GIDMDMPasscodeState passcodeState].info; } return [GIDEMMSupport dictionaryWithStringValuesFromDictionary:allParameters]; } ++ (void)handleTokenFetchEMMError:(nullable NSError *)error + completion:(void (^)(NSError *_Nullable))completion { + [[[self alloc] init] handleTokenFetchEMMError:error completion:completion]; +} + ++ (NSDictionary *)updatedEMMParametersWithParameters: + (NSDictionary *)parameters { + return [[[self alloc] init] updatedEMMParametersWithParameters:parameters]; +} + ++ (NSDictionary *)parametersWithParameters: + (NSDictionary *)parameters + emmSupport:(nullable NSString *)emmSupport + isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired { + return [[[self alloc] init] parametersWithParameters:parameters + emmSupport:emmSupport + isPasscodeInfoRequired:isPasscodeInfoRequired]; +} + #pragma mark - GTMAuthSessionDelegate - (nullable NSDictionary *) additionalTokenRefreshParametersForAuthSession:(GTMAuthSession *)authSession { - return [GIDEMMSupport updatedEMMParametersWithParameters: + return [self updatedEMMParametersWithParameters: authSession.authState.lastTokenResponse.additionalParameters]; } - (void)updateErrorForAuthSession:(GTMAuthSession *)authSession originalError:(NSError *)originalError completion:(void (^)(NSError * _Nullable))completion { - [GIDEMMSupport handleTokenFetchEMMError:originalError completion:^(NSError *_Nullable error) { + [self handleTokenFetchEMMError:originalError completion:^(NSError *_Nullable error) { completion(error); }]; } diff --git a/GoogleSignIn/Sources/GIDLocalAuthentication/API/GIDLocalAuthenticationContext.h b/GoogleSignIn/Sources/GIDLocalAuthentication/API/GIDLocalAuthenticationContext.h new file mode 100644 index 00000000..d8322361 --- /dev/null +++ b/GoogleSignIn/Sources/GIDLocalAuthentication/API/GIDLocalAuthenticationContext.h @@ -0,0 +1,37 @@ +/* + * 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 + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/// The subset of `LAContext` that Google Sign-In uses to probe whether a device passcode is set. +/// `LAContext` conforms; see `LAContext+GIDLocalAuthenticationContext.h`. +@protocol GIDLocalAuthenticationContext + +/// Returns whether `policy` can be evaluated, populating `error` when it cannot. +- (BOOL)canEvaluatePolicy:(LAPolicy)policy error:(NSError * _Nullable * _Nullable)error; + +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDLocalAuthentication/Fake/GIDFakeLocalAuthenticationContext.h b/GoogleSignIn/Sources/GIDLocalAuthentication/Fake/GIDFakeLocalAuthenticationContext.h new file mode 100644 index 00000000..20f64269 --- /dev/null +++ b/GoogleSignIn/Sources/GIDLocalAuthentication/Fake/GIDFakeLocalAuthenticationContext.h @@ -0,0 +1,43 @@ +/* + * 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 + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +#import + +#import "GoogleSignIn/Sources/GIDLocalAuthentication/API/GIDLocalAuthenticationContext.h" + +NS_ASSUME_NONNULL_BEGIN + +/// An in-memory `GIDLocalAuthenticationContext` for tests. +@interface GIDFakeLocalAuthenticationContext : NSObject + +/// The value `-canEvaluatePolicy:error:` returns. Defaults to YES. +@property(nonatomic) BOOL canEvaluatePolicyResult; + +/// The error `-canEvaluatePolicy:error:` reports, if any. Defaults to nil. +@property(nonatomic, nullable) NSError *canEvaluatePolicyError; + +/// Every policy passed to `-canEvaluatePolicy:error:`, in call order, boxed as `NSNumber`. +@property(nonatomic, readonly) NSArray *evaluatedPolicies; + +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDLocalAuthentication/Fake/GIDFakeLocalAuthenticationContext.m b/GoogleSignIn/Sources/GIDLocalAuthentication/Fake/GIDFakeLocalAuthenticationContext.m new file mode 100644 index 00000000..e3aa0f5e --- /dev/null +++ b/GoogleSignIn/Sources/GIDLocalAuthentication/Fake/GIDFakeLocalAuthenticationContext.m @@ -0,0 +1,55 @@ +/* + * 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 + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +#import "GoogleSignIn/Sources/GIDLocalAuthentication/Fake/GIDFakeLocalAuthenticationContext.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation GIDFakeLocalAuthenticationContext { + // Every policy passed to `-canEvaluatePolicy:error:`, in call order. + NSMutableArray *_evaluatedPolicies; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _canEvaluatePolicyResult = YES; + _evaluatedPolicies = [NSMutableArray array]; + } + return self; +} + +- (NSArray *)evaluatedPolicies { + return [_evaluatedPolicies copy]; +} + +- (BOOL)canEvaluatePolicy:(LAPolicy)policy error:(NSError * _Nullable * _Nullable)error { + [_evaluatedPolicies addObject:@(policy)]; + if (error) { + *error = self.canEvaluatePolicyError; + } + return self.canEvaluatePolicyResult; +} + +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDLocalAuthentication/Implementation/LAContext+GIDLocalAuthenticationContext.h b/GoogleSignIn/Sources/GIDLocalAuthentication/Implementation/LAContext+GIDLocalAuthenticationContext.h new file mode 100644 index 00000000..da49c142 --- /dev/null +++ b/GoogleSignIn/Sources/GIDLocalAuthentication/Implementation/LAContext+GIDLocalAuthenticationContext.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 + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +#import + +#import "GoogleSignIn/Sources/GIDLocalAuthentication/API/GIDLocalAuthenticationContext.h" + +NS_ASSUME_NONNULL_BEGIN + +/// `LAContext` already implements every `GIDLocalAuthenticationContext` requirement; this category +/// only declares the conformance. +@interface LAContext (GIDLocalAuthenticationContext) +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDLocalAuthentication/Implementation/LAContext+GIDLocalAuthenticationContext.m b/GoogleSignIn/Sources/GIDLocalAuthentication/Implementation/LAContext+GIDLocalAuthenticationContext.m new file mode 100644 index 00000000..de2ccd75 --- /dev/null +++ b/GoogleSignIn/Sources/GIDLocalAuthentication/Implementation/LAContext+GIDLocalAuthenticationContext.m @@ -0,0 +1,30 @@ +/* + * 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 + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + +#import "GoogleSignIn/Sources/GIDLocalAuthentication/Implementation/LAContext+GIDLocalAuthenticationContext.h" // NOLINT + +NS_ASSUME_NONNULL_BEGIN + +@implementation LAContext (GIDLocalAuthenticationContext) +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/GIDMDMPasscodeCache.h b/GoogleSignIn/Sources/GIDMDMPasscodeCache.h index 569fc22a..4dcb1054 100644 --- a/GoogleSignIn/Sources/GIDMDMPasscodeCache.h +++ b/GoogleSignIn/Sources/GIDMDMPasscodeCache.h @@ -19,6 +19,8 @@ #import +#import "GoogleSignIn/Sources/GIDLocalAuthentication/API/GIDLocalAuthenticationContext.h" + @class GIDMDMPasscodeState; NS_ASSUME_NONNULL_BEGIN @@ -34,6 +36,17 @@ NS_ASSUME_NONNULL_BEGIN */ + (instancetype)sharedInstance; +/** + * Creates a cache that probes the passcode state through `localAuthenticationContext`. Pass nil + * when LocalAuthentication is unavailable; the cache then reports only the Keychain probe. + */ +- (instancetype)initWithLocalAuthenticationContext: + (nullable id)localAuthenticationContext + NS_DESIGNATED_INITIALIZER; + +/** Unavailable; use `+sharedInstance` or the designated initializer. */ +- (instancetype)init NS_UNAVAILABLE; + /** * Retrieves the current passcode state. */ diff --git a/GoogleSignIn/Sources/GIDMDMPasscodeCache.m b/GoogleSignIn/Sources/GIDMDMPasscodeCache.m index bedb3a31..97f083d5 100644 --- a/GoogleSignIn/Sources/GIDMDMPasscodeCache.m +++ b/GoogleSignIn/Sources/GIDMDMPasscodeCache.m @@ -22,6 +22,7 @@ #import #import +#import "GoogleSignIn/Sources/GIDLocalAuthentication/Implementation/LAContext+GIDLocalAuthenticationContext.h" // NOLINT #import "GoogleSignIn/Sources/GIDMDMPasscodeState.h" #import "GoogleSignIn/Sources/GIDMDMPasscodeState_Private.h" @@ -55,6 +56,9 @@ static const int64_t kObtainKeychainInfoWaitTime = 3 * NSEC_PER_SEC; @implementation GIDMDMPasscodeCache { + /** Probes the passcode state; nil when LocalAuthentication is unavailable. */ + id _localAuthenticationContext; + /** Whether or not LocalAuthentication API is available. */ BOOL _hasLocalAuthentication; @@ -77,10 +81,12 @@ @implementation GIDMDMPasscodeCache { GIDMDMPasscodeState *_cachedState; } -- (instancetype)init { +- (instancetype)initWithLocalAuthenticationContext: + (nullable id)localAuthenticationContext { self = [super init]; if (self) { - _hasLocalAuthentication = [self hasLocalAuthentication]; + _localAuthenticationContext = localAuthenticationContext; + _hasLocalAuthentication = (localAuthenticationContext != nil) && [self hasLocalAuthentication]; _hasKeychain = [self hasKeychain]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) @@ -98,7 +104,15 @@ + (instancetype)sharedInstance { static GIDMDMPasscodeCache *sharedInstance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - sharedInstance = [[GIDMDMPasscodeCache alloc] init]; + LAContext *context; + @try { + context = [[LAContext alloc] init]; + } @catch (NSException *) { + // In theory there should be no exceptions but in practice there may be: + // b/23200390, b/23218643. + context = nil; + } + sharedInstance = [[GIDMDMPasscodeCache alloc] initWithLocalAuthenticationContext:context]; }); return sharedInstance; } @@ -185,18 +199,11 @@ - (void)obtainLocalAuthenticationInfo { NSLog(@"Calling LocalAuthentication API for device passcode state..."); #endif _hasEnteredBackground = NO; - static LAContext *context; - @try { - if (!context) { - context = [[LAContext alloc] init]; - } - } @catch (NSException *) { - // In theory there should be no exceptions but in practice there may be: b/23200390, b/23218643. - return; - } int result; NSError *error; - result = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&error] ? 1 : 0; + result = + [_localAuthenticationContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication + error:&error] ? 1 : 0; if (error) { _localAuthenticationInfo = @{ kResultKey : @(result), diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index 38f4514f..31497531 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,10 @@ @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. GIDTimedLoader *_timedLoader; @@ -688,18 +693,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 +740,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 +790,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 +984,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 +1364,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 +1405,9 @@ - (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 { +// 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]; if ([configValue isKindOfClass:[NSString class]]) { @@ -1388,8 +1416,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 { +// 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; // 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..3719a500 100644 --- a/GoogleSignIn/Sources/GIDSignInCallbackSchemes.h +++ b/GoogleSignIn/Sources/GIDSignInCallbackSchemes.h @@ -16,27 +16,34 @@ #import +#import "GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h" + 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; -// 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; + +/// 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 7f5888f7..dee87a8a 100644 --- a/GoogleSignIn/Sources/GIDSignInCallbackSchemes.m +++ b/GoogleSignIn/Sources/GIDSignInCallbackSchemes.m @@ -14,23 +14,28 @@ #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h" +#import "GoogleSignIn/Sources/GIDBundle/Implementation/NSBundle+GIDBundle.h" + NS_ASSUME_NONNULL_BEGIN @implementation GIDSignInCallbackSchemes { NSString *_clientIdentifier; + // The bundle whose Info.plist declares the host app's URL schemes. + 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 + * @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 { - NSMutableArray *result = [NSMutableArray array]; - NSBundle *bundle = [NSBundle mainBundle]; - NSArray *urlTypes = [bundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]; ++ (NSArray *)relevantURLSchemesInBundle:(id)bundle { + NSMutableArray *result = [NSMutableArray array]; + NSArray *> *urlTypes = + [bundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]; for (NSDictionary *urlType in urlTypes) { NSArray *urlTypeSchemes = urlType[@"CFBundleURLSchemes"]; for (NSString *urlTypeScheme in urlTypeSchemes) { @@ -40,14 +45,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 = @@ -55,8 +66,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]; @@ -64,9 +75,9 @@ - (NSArray *)allSchemes { return schemes; } -- (NSMutableArray *)unsupportedSchemes { +- (NSMutableArray *)unsupportedSchemes { NSMutableArray *unsupportedSchemes = [NSMutableArray arrayWithArray:[self allSchemes]]; - NSArray *supportedSchemes = [[self class] relevantURLSchemes]; + NSArray *supportedSchemes = [[self class] relevantURLSchemesInBundle:_bundle]; [unsupportedSchemes removeObjectsInArray:supportedSchemes]; return unsupportedSchemes; } diff --git a/GoogleSignIn/Sources/GIDSignIn_Private.h b/GoogleSignIn/Sources/GIDSignIn_Private.h index 803cc10e..9f9bfc13 100644 --- a/GoogleSignIn/Sources/GIDSignIn_Private.h +++ b/GoogleSignIn/Sources/GIDSignIn_Private.h @@ -24,6 +24,8 @@ #import #endif +#import "GoogleSignIn/Sources/GIDBundle/API/GIDBundle.h" + NS_ASSUME_NONNULL_BEGIN @class GIDGoogleUser; @@ -61,6 +63,29 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); API_AVAILABLE(ios(14)); #endif // TARGET_OS_IOS || !TARGET_OS_MACCATALYST +/// 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 + userDefaults:(NSUserDefaults *)userDefaults NS_DESIGNATED_INITIALIZER; + +#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..501a50fb 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,14 @@ @interface GIDAuthStateMigrationTest : XCTestCase @end @implementation GIDAuthStateMigrationTest { - id _mockUserDefaults; + // Suite-scoped user defaults injected into the migration under test. + NSUserDefaults *_userDefaults; id _mockGTMAppAuthFetcherAuthorization; id _mockGIDAuthStateMigration; id _mockGTMKeychainStore; id _mockKeychainHelper; - id _mockNSBundle; + // The bundle injected into the migration under test. + GIDFakeBundle *_bundle; id _mockGIDSignInCallbackSchemes; id _mockGTMOAuth2Compatibility; #if TARGET_OS_OSX @@ -92,12 +96,13 @@ @implementation GIDAuthStateMigrationTest { - (void)setUp { [super setUp]; - _mockUserDefaults = OCMClassMock([NSUserDefaults class]); + _userDefaults = [[NSUserDefaults alloc] initWithSuiteName:kUserDefaultsSuiteName]; + [_userDefaults removePersistentDomainForName:kUserDefaultsSuiteName]; + _bundle = [[GIDFakeBundle alloc] initWithBundleIdentifier:kBundleID infoDictionary:@{}]; _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 +114,7 @@ - (void)setUp { } - (void)tearDown { - [_mockUserDefaults verify]; - [_mockUserDefaults stopMocking]; + [_userDefaults removePersistentDomainForName:kUserDefaultsSuiteName]; [_mockGTMAppAuthFetcherAuthorization verify]; [_mockGTMAppAuthFetcherAuthorization stopMocking]; [_mockGIDAuthStateMigration verify]; @@ -119,8 +123,6 @@ - (void)tearDown { [_mockGTMKeychainStore stopMocking]; [_mockKeychainHelper verify]; [_mockKeychainHelper stopMocking]; - [_mockNSBundle verify]; - [_mockNSBundle stopMocking]; [_mockGIDSignInCallbackSchemes verify]; [_mockGIDSignInCallbackSchemes stopMocking]; [_mockGTMOAuth2Compatibility verify]; @@ -136,10 +138,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 +149,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 +173,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 +235,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 +247,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,11 +299,9 @@ - (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]; + initWithClientIdentifier:kClientID bundle:OCMOCK_ANY]; [[[_mockGIDSignInCallbackSchemes expect] andReturn:kDotReversedClientID] clientIdentifierScheme]; [[[_mockGIDAuthStateMigration expect] andReturn:kAdditionalTokenRequestParameters] passwordForService:[self additionalTokenRequestParametersKeyFromFingerprint:fingerprint]]; diff --git a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h index 3086880e..33cd7330 100644 --- a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h +++ b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h @@ -14,26 +14,31 @@ * limitations under the License. */ -#import +#import "GoogleSignIn/Sources/GIDBundle/Fake/GIDFakeBundle.h" + +NS_ASSUME_NONNULL_BEGIN /** * @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. + * @fn initWithClientID: + * @brief Creates a bundle whose `GIDClientID` Info.plist value is `clientID`. + * @param clientID The fake client identifier for the app. */ -- (void)startFakingWithClientID:(NSString *)clientID; +- (instancetype)initWithClientID:(NSString *)clientID NS_DESIGNATED_INITIALIZER; -/** - * @fn stopFaking - * @brief Stops faking [NSBundle mainBundle] - */ -- (void)stopFaking; +/** Unavailable. Use `-initWithClientID:`. */ +- (instancetype)init NS_UNAVAILABLE; + +/** Unavailable. Use `-initWithClientID:`. */ +- (instancetype)initWithBundleIdentifier:(nullable NSString *)bundleIdentifier + infoDictionary:(NSDictionary *)infoDictionary + NS_UNAVAILABLE; #pragma mark - URL Schemes @@ -87,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 diff --git a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m index d017e60c..0c656f13 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 initWithBundleIdentifier:nil + infoDictionary:@{kConfigClientIDKey : clientID}]; + if (self) { + _clientId = [clientID copy]; + } + return self; } #pragma mark - Utilities @@ -108,31 +76,34 @@ - (NSString *)stringByFlippingCasesInString:(NSString *)original { #pragma mark - URL Schemes - (void)fakeAllSchemesSupported { - _fakeConfig[kCFBundleURLTypesKey] = @[ + NSArray *> *urlTypes = @[ @{ kCFBundleURLSchemesKey : @[ [self reversedClientId] ] } ]; + [self setObject:urlTypes forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeAllSchemesSupportedAndMerged { - _fakeConfig[kCFBundleURLTypesKey] = @[ + NSArray *> *urlTypes = @[ @{ kCFBundleURLSchemesKey : @[ [self reversedClientId] ] }, ]; + [self setObject:urlTypes forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeAllSchemesSupportedWithCasesMangled { NSString *caseFlippedReverseClientId = [self stringByFlippingCasesInString:[self reversedClientId]]; - _fakeConfig[kCFBundleURLTypesKey] = @[ + NSArray *> *urlTypes = @[ @{ kCFBundleURLSchemesKey : @[ caseFlippedReverseClientId ] } ]; + [self setObject:urlTypes forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeMissingClientIdScheme { @@ -140,19 +111,20 @@ - (void)fakeMissingClientIdScheme { } - (void)fakeMissingAllSchemes { - _fakeConfig[kCFBundleURLTypesKey] = nil; + [self setObject:nil forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeOtherSchemes { - _fakeConfig[kCFBundleURLTypesKey] = @[ + NSArray *> *urlTypes = @[ @{ kCFBundleURLSchemesKey : @[ @"junk" ] } ]; + [self setObject:urlTypes forInfoDictionaryKey:kCFBundleURLTypesKey]; } - (void)fakeOtherSchemesAndAllSchemes { - _fakeConfig[kCFBundleURLTypesKey] = @[ + NSArray *> *urlTypes = @[ @{ kCFBundleURLSchemesKey : @[ @"junk" ] }, @@ -160,16 +132,17 @@ - (void)fakeOtherSchemesAndAllSchemes { kCFBundleURLSchemesKey : @[ [self reversedClientId] ] } ]; + [self setObject:urlTypes 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/GIDMDMPasscodeStateTests.m b/GoogleSignIn/Tests/Unit/GIDMDMPasscodeStateTests.m index 785d841e..6c14dcd0 100644 --- a/GoogleSignIn/Tests/Unit/GIDMDMPasscodeStateTests.m +++ b/GoogleSignIn/Tests/Unit/GIDMDMPasscodeStateTests.m @@ -21,71 +21,34 @@ #import #import +#import "GoogleSignIn/Sources/GIDLocalAuthentication/Fake/GIDFakeLocalAuthenticationContext.h" +#import "GoogleSignIn/Sources/GIDMDMPasscodeCache.h" #import "GoogleSignIn/Sources/GIDMDMPasscodeState.h" -#import -#import - @interface GIDMDMPasscodeStateTests : XCTestCase @end @implementation GIDMDMPasscodeStateTests { - /** Whether or not the iOS version is equal or greater than 9.0. */ - BOOL _isIOS9orAbove; - - /** Whether or not `canEvaluatePolicy:error:` method has been called. */ - BOOL _canEvaluatePolicyCalled; - - /** The next result to be returned from the `canEvaluatePolicy:error:` method. */ - BOOL _nextCanEvaluatePolicyResult; - - /** The next error to be returned from the `canEvaluatePolicy:error:` method. */ - NSError *_nextCanEvaluatePolicyError; + /** Stands in for the `LAContext` the cache probes. */ + GIDFakeLocalAuthenticationContext *_context; + /** The cache under test. */ + GIDMDMPasscodeCache *_cache; } - (void)setUp { [super setUp]; - _isIOS9orAbove = [[NSProcessInfo processInfo] - isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 9}]; - if (!_isIOS9orAbove) { - return; - } - _canEvaluatePolicyCalled = NO; - id canEvaluatePolicyError = ^BOOL(id context, LAPolicy policy, NSError * _Nullable *error) { - self->_canEvaluatePolicyCalled = YES; - XCTAssertEqual(policy, LAPolicyDeviceOwnerAuthentication); - if (error) { - *error = self->_nextCanEvaluatePolicyError; - } - return self->_nextCanEvaluatePolicyResult; - }; - [GULSwizzler swizzleClass:[LAContext class] - selector:@selector(canEvaluatePolicy:error:) - isClassSelector:NO - withBlock:canEvaluatePolicyError]; - [self postApplicationDidEnterBackgroundNotification]; -} - -- (void)tearDown { - if (!_isIOS9orAbove) { - return; - } - [GULSwizzler unswizzleClass:[LAContext class] - selector:@selector(canEvaluatePolicy:error:) - isClassSelector:NO]; + _context = [[GIDFakeLocalAuthenticationContext alloc] init]; + _cache = [[GIDMDMPasscodeCache alloc] initWithLocalAuthenticationContext:_context]; } /** * Verifies the correct response when LocalAuthentication API returns without an error. */ - (void)testLocalAuthenticationNoError { - if (!_isIOS9orAbove) { - return; - } - _nextCanEvaluatePolicyResult = YES; - _nextCanEvaluatePolicyError = nil; - GIDMDMPasscodeState *passcodeState = [GIDMDMPasscodeState passcodeState]; - XCTAssertTrue(_canEvaluatePolicyCalled); + _context.canEvaluatePolicyResult = YES; + _context.canEvaluatePolicyError = nil; + GIDMDMPasscodeState *passcodeState = [_cache passcodeState]; + XCTAssertEqualObjects(_context.evaluatedPolicies, @[ @(LAPolicyDeviceOwnerAuthentication) ]); XCTAssertEqualObjects(passcodeState.status, @"YES"); NSDictionary *dict = [self dictWithEncodedString:passcodeState.info]; [self assertJSONNumber:dict[@"LocalAuthentication"][@"result"] isInteger:1]; @@ -97,17 +60,14 @@ - (void)testLocalAuthenticationNoError { * Verifies the correct response when LocalAuthentication API returns with an error. */ - (void)testLocalAuthenticationHasError { - if (!_isIOS9orAbove) { - return; - } NSString *fakeErrorDomain = @"asdf.hjkl"; NSInteger fakeErrorCode = -12345; - _nextCanEvaluatePolicyResult = NO; - _nextCanEvaluatePolicyError = [NSError errorWithDomain:fakeErrorDomain - code:fakeErrorCode - userInfo:nil]; - GIDMDMPasscodeState *passcodeState = [GIDMDMPasscodeState passcodeState]; - XCTAssertTrue(_canEvaluatePolicyCalled); + _context.canEvaluatePolicyResult = NO; + _context.canEvaluatePolicyError = [NSError errorWithDomain:fakeErrorDomain + code:fakeErrorCode + userInfo:nil]; + GIDMDMPasscodeState *passcodeState = [_cache passcodeState]; + XCTAssertEqualObjects(_context.evaluatedPolicies, @[ @(LAPolicyDeviceOwnerAuthentication) ]); XCTAssertEqualObjects(passcodeState.status, @"NO"); NSDictionary *dict = [self dictWithEncodedString:passcodeState.info]; [self assertJSONNumber:dict[@"LocalAuthentication"][@"result"] isInteger:0]; @@ -119,20 +79,16 @@ - (void)testLocalAuthenticationHasError { * Verifies caching behavior regarding to calling LocalAuthentication API. */ - (void)testLocalAuthenticationCache { - if (!_isIOS9orAbove) { - return; - } - GIDMDMPasscodeState *oldPasscodeState = [GIDMDMPasscodeState passcodeState]; - _canEvaluatePolicyCalled = false; - GIDMDMPasscodeState *newPasscodeState = [GIDMDMPasscodeState passcodeState]; - XCTAssertFalse(_canEvaluatePolicyCalled); + GIDMDMPasscodeState *oldPasscodeState = [_cache passcodeState]; + GIDMDMPasscodeState *newPasscodeState = [_cache passcodeState]; + XCTAssertEqual(_context.evaluatedPolicies.count, 1u); XCTAssertEqualObjects(oldPasscodeState.status, newPasscodeState.status); XCTAssertEqualObjects(oldPasscodeState.info, newPasscodeState.info); - // Verify that the cache is cleared after background notification. + // Verify that the cache re-probes LocalAuthentication after the app enters the background. [self postApplicationDidEnterBackgroundNotification]; - [GIDMDMPasscodeState passcodeState]; - XCTAssertTrue(_canEvaluatePolicyCalled); + [_cache passcodeState]; + XCTAssertEqual(_context.evaluatedPolicies.count, 2u); } /** @@ -140,11 +96,18 @@ - (void)testLocalAuthenticationCache { * Keychain API is in C thus there is no easy way to swizzler them. */ - (void)testKeychain { - GIDMDMPasscodeState *passcodeState = [GIDMDMPasscodeState passcodeState]; + GIDMDMPasscodeState *passcodeState = [_cache passcodeState]; NSDictionary *dict = [self dictWithEncodedString:passcodeState.info]; XCTAssertTrue([dict[@"Keychain"][@"result"] isKindOfClass:[NSNumber class]]); } +/** Verifies `+[GIDMDMPasscodeState passcodeState]` returns a state with a status and info. */ +- (void)testPasscodeStateClassMethod { + GIDMDMPasscodeState *passcodeState = [GIDMDMPasscodeState passcodeState]; + XCTAssertNotNil(passcodeState.status); + XCTAssertNotNil([self dictWithEncodedString:passcodeState.info]); +} + #pragma mark - Helpers /** 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"]]; diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index adcad2d5..acc147e1 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]; @@ -437,7 +422,9 @@ - (void)testConfigureFailsWithProviderError { GIDSignIn *signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore authStateMigrationService:_authStateMigrationService - appCheck:appCheck]; + appCheck:appCheck + bundle:_fakeMainBundle + userDefaults:_testUserDefaults]; // Should fail and stay unprepared when the provider returns an error [signIn configureWithCompletion:^(NSError * _Nullable error) { @@ -456,7 +443,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); @@ -472,7 +461,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); } @@ -485,7 +476,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); @@ -501,7 +494,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); } @@ -515,7 +510,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]; @@ -1423,11 +1420,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); @@ -1937,8 +1936,8 @@ - (void)testTokenEndpointEMMError { code:12345 userInfo:@{ OIDOAuthErrorFieldError : errorJSON }]; id emmSupport = OCMStrictClassMock([GIDEMMSupport class]); - [[emmSupport expect] handleTokenFetchEMMError:emmError - completion:SAVE_TO_ARG_BLOCK(completion)]; + [[[emmSupport expect] classMethod] handleTokenFetchEMMError:emmError + completion:SAVE_TO_ARG_BLOCK(completion)]; [self OAuthLoginWithAddScopesFlow:NO authError:nil