diff --git a/GoogleSignIn/Sources/GIDAuthStateMigration/Fake/GIDFakeAuthStateMigration.h b/GoogleSignIn/Sources/GIDAuthStateMigration/Fake/GIDFakeAuthStateMigration.h new file mode 100644 index 00000000..fc611e53 --- /dev/null +++ b/GoogleSignIn/Sources/GIDAuthStateMigration/Fake/GIDFakeAuthStateMigration.h @@ -0,0 +1,27 @@ +/* + * Copyright 2025 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/GIDAuthStateMigration/GIDAuthStateMigration.h" + +/// A fake |GIDAuthStateMigration| for testing. +@interface GIDFakeAuthStateMigration : GIDAuthStateMigration + +/// Callback that is called when `migrateIfNeededWithTokenURL` is invoked. +@property (nonatomic, nullable) void (^migrationInvokedCallback) + (NSURL * _Nullable tokenURL, NSString * _Nullable callbackPath, NSString * _Nullable keychainName, + BOOL isFreshInstall); + +@end diff --git a/GoogleSignIn/Sources/GIDAuthStateMigration/Fake/GIDFakeAuthStateMigration.m b/GoogleSignIn/Sources/GIDAuthStateMigration/Fake/GIDFakeAuthStateMigration.m new file mode 100644 index 00000000..0346dba1 --- /dev/null +++ b/GoogleSignIn/Sources/GIDAuthStateMigration/Fake/GIDFakeAuthStateMigration.m @@ -0,0 +1,42 @@ +/* + * Copyright 2025 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/GIDAuthStateMigration/Fake/GIDFakeAuthStateMigration.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation GIDFakeAuthStateMigration + +@synthesize migrationInvokedCallback = _migrationInvokedCallback; + +- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore { + self = [super initWithKeychainStore:keychainStore]; + return self; +} + +- (void)migrateIfNeededWithTokenURL:(NSURL *)tokenURL + callbackPath:(NSString *)callbackPath + keychainName:(NSString *)keychainName + isFreshInstall:(BOOL)isFreshInstall { + if (_migrationInvokedCallback) { + _migrationInvokedCallback(tokenURL, callbackPath, keychainName, isFreshInstall); + } + return; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDAuthStateMigration.h b/GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h similarity index 100% rename from GoogleSignIn/Sources/GIDAuthStateMigration.h rename to GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h diff --git a/GoogleSignIn/Sources/GIDAuthStateMigration.m b/GoogleSignIn/Sources/GIDAuthStateMigration/Implementation/GIDAuthStateMigration.m similarity index 99% rename from GoogleSignIn/Sources/GIDAuthStateMigration.m rename to GoogleSignIn/Sources/GIDAuthStateMigration/Implementation/GIDAuthStateMigration.m index cb4cfbe6..b5c21773 100644 --- a/GoogleSignIn/Sources/GIDAuthStateMigration.m +++ b/GoogleSignIn/Sources/GIDAuthStateMigration/Implementation/GIDAuthStateMigration.m @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#import "GoogleSignIn/Sources/GIDAuthStateMigration.h" - +#import "GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h" #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h" @import GTMAppAuth; diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index 640761e1..ef7ac3eb 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -21,7 +21,7 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInResult.h" -#import "GoogleSignIn/Sources/GIDAuthStateMigration.h" +#import "GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h" #import "GoogleSignIn/Sources/GIDEMMSupport.h" #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h" #import "GoogleSignIn/Sources/GIDSignInPreferences.h" @@ -490,15 +490,19 @@ + (GIDSignIn *)sharedInstance { dispatch_once(&once, ^{ GTMKeychainStore *keychainStore = [[GTMKeychainStore alloc] initWithItemName:kGTMAppAuthKeychainName]; + GIDAuthStateMigration *authStateMigrationService = + [[GIDAuthStateMigration alloc] initWithKeychainStore:keychainStore]; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST if (@available(iOS 14.0, *)) { GIDAppCheck *appCheck = [GIDAppCheck appCheckUsingAppAttestProvider]; sharedInstance = [[self alloc] initWithKeychainStore:keychainStore + authStateMigrationService:authStateMigrationService appCheck:appCheck]; } #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST if (!sharedInstance) { - sharedInstance = [[self alloc] initWithKeychainStore:keychainStore]; + sharedInstance = [[self alloc] initWithKeychainStore:keychainStore + authStateMigrationService:authStateMigrationService]; } }); return sharedInstance; @@ -533,7 +537,8 @@ - (void)configureDebugProviderWithAPIKey:(NSString *)APIKey #pragma mark - Private methods -- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore { +- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore + authStateMigrationService:(GIDAuthStateMigration *)authStateMigrationService { self = [super init]; if (self) { // Get the bundle of the current executable. @@ -561,20 +566,20 @@ - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore { tokenEndpoint:[NSURL URLWithString:tokenEndpointURL]]; _keychainStore = keychainStore; // Perform migration of auth state from old versions of the SDK if needed. - GIDAuthStateMigration *migration = - [[GIDAuthStateMigration alloc] initWithKeychainStore:_keychainStore]; - [migration migrateIfNeededWithTokenURL:_appAuthConfiguration.tokenEndpoint - callbackPath:kBrowserCallbackPath - keychainName:kGTMAppAuthKeychainName - isFreshInstall:isFreshInstall]; + [authStateMigrationService migrateIfNeededWithTokenURL:_appAuthConfiguration.tokenEndpoint + callbackPath:kBrowserCallbackPath + keychainName:kGTMAppAuthKeychainName + isFreshInstall:isFreshInstall]; } return self; } #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore + authStateMigrationService:(GIDAuthStateMigration *)authStateMigrationService appCheck:(GIDAppCheck *)appCheck { - self = [self initWithKeychainStore:keychainStore]; + self = [self initWithKeychainStore:keychainStore + authStateMigrationService:authStateMigrationService]; if (self) { _appCheck = appCheck; _configureAppCheckCalled = NO; diff --git a/GoogleSignIn/Sources/GIDSignIn_Private.h b/GoogleSignIn/Sources/GIDSignIn_Private.h index 4072a4a0..bb642cb7 100644 --- a/GoogleSignIn/Sources/GIDSignIn_Private.h +++ b/GoogleSignIn/Sources/GIDSignIn_Private.h @@ -30,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN @class GIDSignInInternalOptions; @class GTMKeychainStore; @class GIDAppCheck; +@class GIDAuthStateMigration; /// Represents a completion block that takes a `GIDSignInResult` on success or an error if the /// operation was unsuccessful. @@ -46,11 +47,13 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); @property(nonatomic, readwrite, nullable) GIDGoogleUser *currentUser; /// Private initializer taking a `GTMKeychainStore`. -- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore; +- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore + authStateMigrationService:(GIDAuthStateMigration *)authStateMigrationService; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST /// Private initializer taking a `GTMKeychainStore` and `GIDAppCheckProvider`. - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore + authStateMigrationService:(GIDAuthStateMigration *)authStateMigrationService appCheck:(GIDAppCheck *)appCheck API_AVAILABLE(ios(14)); #endif // TARGET_OS_IOS || !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m b/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m index 457b84de..20b16451 100644 --- a/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m +++ b/GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m @@ -14,7 +14,7 @@ #import -#import "GoogleSignIn/Sources/GIDAuthStateMigration.h" +#import "GoogleSignIn/Sources/GIDAuthStateMigration/GIDAuthStateMigration.h" #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h" #if TARGET_OS_OSX #import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h" diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 7d5195d8..eb367322 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -40,6 +40,7 @@ #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST +#import "GoogleSignIn/Sources/GIDAuthStateMigration/Fake/GIDFakeAuthStateMigration.h" #import "GoogleSignIn/Tests/Unit/GIDFakeFetcher.h" #import "GoogleSignIn/Tests/Unit/GIDFakeFetcherService.h" #import "GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h" @@ -221,6 +222,9 @@ @interface GIDSignInTest : XCTestCase { // Whether callback block has been called. BOOL _completionCalled; + // Fake for |GIDAuthStateMigration|. + GIDFakeAuthStateMigration *_authStateMigrationService; + // Fake fetcher service to emulate network requests. GIDFakeFetcherService *_fetcherService; @@ -331,6 +335,7 @@ - (void)setUp { callback:COPY_TO_ARG_BLOCK(self->_savedTokenCallback)]); // Fakes + _authStateMigrationService = [[GIDFakeAuthStateMigration alloc] init]; _fetcherService = [[GIDFakeFetcherService alloc] init]; _fakeMainBundle = [[GIDFakeMainBundle alloc] init]; [_fakeMainBundle startFakingWithClientID:kClientId]; @@ -339,7 +344,8 @@ - (void)setUp { // Object under test [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kAppHasRunBeforeKey]; - _signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore]; + _signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore + authStateMigrationService:_authStateMigrationService]; _hint = nil; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST @@ -398,6 +404,7 @@ - (void)testConfigureSucceeds { userDefaults:_testUserDefaults]; GIDSignIn *signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore + authStateMigrationService:_authStateMigrationService appCheck:appCheck]; [signIn configureWithCompletion:^(NSError * _Nullable error) { XCTAssertNil(error); @@ -421,6 +428,7 @@ - (void)testConfigureFailsNoTokenOrError { userDefaults:_testUserDefaults]; GIDSignIn *signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore + authStateMigrationService:_authStateMigrationService appCheck:appCheck]; // Should fail if missing both token and error @@ -439,7 +447,8 @@ - (void)testConfigureFailsNoTokenOrError { - (void)testInitWithKeychainStore { GTMKeychainStore *store = [[GTMKeychainStore alloc] initWithItemName:@"foo"]; GIDSignIn *signIn; - signIn = [[GIDSignIn alloc] initWithKeychainStore:store]; + signIn = [[GIDSignIn alloc] initWithKeychainStore:store + authStateMigrationService:_authStateMigrationService]; XCTAssertNotNil(signIn.configuration); XCTAssertEqual(signIn.configuration.clientID, kClientId); XCTAssertNil(signIn.configuration.serverClientID); @@ -454,7 +463,8 @@ - (void)testInitWithKeychainStore_noConfig { openIDRealm:nil]; GTMKeychainStore *store = [[GTMKeychainStore alloc] initWithItemName:@"foo"]; GIDSignIn *signIn; - signIn = [[GIDSignIn alloc] initWithKeychainStore:store]; + signIn = [[GIDSignIn alloc] initWithKeychainStore:store + authStateMigrationService:_authStateMigrationService]; XCTAssertNil(signIn.configuration); } @@ -466,7 +476,8 @@ - (void)testInitWithKeychainStore_fullConfig { GTMKeychainStore *store = [[GTMKeychainStore alloc] initWithItemName:@"foo"]; GIDSignIn *signIn; - signIn = [[GIDSignIn alloc] initWithKeychainStore:store]; + signIn = [[GIDSignIn alloc] initWithKeychainStore:store + authStateMigrationService:_authStateMigrationService]; XCTAssertNotNil(signIn.configuration); XCTAssertEqual(signIn.configuration.clientID, kClientId); XCTAssertEqual(signIn.configuration.serverClientID, kServerClientId); @@ -481,10 +492,29 @@ - (void)testInitWithKeychainStore_invalidConfig { openIDRealm:nil]; GTMKeychainStore *store = [[GTMKeychainStore alloc] initWithItemName:@"foo"]; GIDSignIn *signIn; - signIn = [[GIDSignIn alloc] initWithKeychainStore:store]; + signIn = [[GIDSignIn alloc] initWithKeychainStore:store + authStateMigrationService:_authStateMigrationService]; XCTAssertNil(signIn.configuration); } +- (void)testInitWithKeychainStore_attemptsMigration { + NSString *expectedKeychainName = @"foo"; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called."]; + _authStateMigrationService.migrationInvokedCallback = + ^(NSURL *tokenURL, NSString *callbackPath, NSString *keychainName, BOOL isFreshInstall) { + XCTAssertFalse(isFreshInstall); + [expectation fulfill]; + }; + + GTMKeychainStore *store = [[GTMKeychainStore alloc] initWithItemName:expectedKeychainName]; + GIDSignIn *signIn = [[GIDSignIn alloc] initWithKeychainStore:store + authStateMigrationService:_authStateMigrationService]; + + XCTAssertNotNil(signIn.configuration); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + - (void)testRestorePreviousSignInNoRefresh_hasPreviousUser { [[[_authorization stub] andReturn:_authState] authState]; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST