Просмотр исходного кода

Add an authorizeInteractively test

Matt Mathias 1 год назад
Родитель
Сommit
857f0463ca

+ 12 - 2
GoogleSignIn/Sources/GIDAuthorization.m

@@ -138,6 +138,7 @@ static NSString *const kTokenURLTemplate = @"https://%@/token";
 #pragma mark - Signing In
 
 // FIXME: Do not pass options here; put this on `GIDAuthorizationFlow`
+// But perhaps `options` are needed because the presenting vc could change
 - (void)signInWithOptions:(GIDSignInInternalOptions *)options {
   // Options for continuation are not the options we want to cache. The purpose of caching the
   // options in the first place is to provide continuation flows with a starting place from which to
@@ -161,9 +162,12 @@ static NSString *const kTokenURLTemplate = @"https://%@/token";
     
     [self assertValidPresentingController];
     
+    id<GIDBundle> bundle = self.authFlow.options.bundle;
+    NSString *clientID = self.currentOptions.configuration.clientID;
+    
     // 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:clientID bundle:bundle];
     NSArray<NSString *> *unsupportedSchemes = [schemes unsupportedSchemes];
     if (unsupportedSchemes.count != 0) {
       // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
@@ -183,7 +187,7 @@ static NSString *const kTokenURLTemplate = @"https://%@/token";
           self->_currentOptions = nil;
           dispatch_async(dispatch_get_main_queue(), ^{
             GIDSignInResult *signInResult =
-            [[GIDSignInResult alloc] initWithGoogleUser:self->_currentUser serverAuthCode:nil];
+              [[GIDSignInResult alloc] initWithGoogleUser:[self currentUser] serverAuthCode:nil];
             options.completion(signInResult, nil);
           });
         }
@@ -255,4 +259,10 @@ static NSString *const kTokenURLTemplate = @"https://%@/token";
   }
 }
 
+#pragma mark - Current User
+
+- (nullable GIDGoogleUser *)currentUser {
+  return self.authFlow.googleUser;
+}
+
 @end

+ 5 - 1
GoogleSignIn/Sources/GIDAuthorizationFlow/Implementations/Fake/GIDAuthorizationFlowFake.m

@@ -16,6 +16,8 @@
 
 #import "GIDAuthorizationFlowFake.h"
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInResult.h"
+#import "GoogleSignIn/Sources/GIDSignInResult_Private.h"
 #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
 #import "GoogleSignIn/Sources/GIDConfiguration_Private.h"
 
@@ -45,7 +47,9 @@
 }
 
 - (void)authorizeInteractively {
-  // TODO: Implement
+  GIDSignInResult *result = [[GIDSignInResult alloc] initWithGoogleUser:self.googleUser
+                                                         serverAuthCode:@"abcd"];
+  self.options.completion(result, self.error);
 }
 
 @end

+ 2 - 1
GoogleSignIn/Sources/GIDAuthorizationFlow/Implementations/Operations/GIDTokenFetchOperation.m

@@ -51,7 +51,8 @@
         kMinimumRestoredAccessTokenTimeToExpire)) {
     return;
   }
-  NSMutableDictionary<NSString *, NSString *> *additionalParameters = [@{} mutableCopy];
+  NSMutableDictionary<NSString *, NSString *> *additionalParameters =
+    [[NSMutableDictionary alloc] init];
   if (self.options.configuration.serverClientID) {
     additionalParameters[kAudienceParameter] = self.options.configuration.serverClientID;
   }

+ 1 - 0
GoogleSignIn/Sources/GIDAuthorization_Private.h

@@ -16,6 +16,7 @@
 
 #import <Foundation/Foundation.h>
 #import <TargetConditionals.h>
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthorization.h"
 
 @class GIDConfiguration;
 @class GTMKeychainStore;

+ 3 - 0
GoogleSignIn/Sources/GIDSignIn.m

@@ -264,6 +264,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
                                        presentingViewController:presentingViewController
                                                       loginHint:hint
                                                   addScopesFlow:NO
+                                                         bundle:nil
                                                      completion:completion];
   [self signInWithOptions:options];
 }
@@ -289,6 +290,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
                                      presentingViewController:presentingViewController
                                                     loginHint:hint
                                                 addScopesFlow:NO
+                                                       bundle:nil
                                                        scopes:additionalScopes
                                                         nonce:nonce
                                                    completion:completion];
@@ -311,6 +313,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
                                        presentingViewController:presentingViewController
                                                       loginHint:self.currentUser.profile.email
                                                   addScopesFlow:YES
+                                                         bundle:nil
                                                      completion:completion];
 
   NSSet<NSString *> *requestedScopes = [NSSet setWithArray:scopes];

+ 21 - 8
GoogleSignIn/Sources/GIDSignInCallbackSchemes.h

@@ -18,25 +18,38 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
-// A utility class for dealing with callback schemes.
+@protocol GIDBundle <NSObject>
+
+- (nullable id)objectForInfoDictionaryKey:(NSString *)key;
+
+@end
+
+@interface NSBundle (NSBundleGIDBundle) <GIDBundle>
+@end
+
+/// A utility class for dealing with callback schemes.
 @interface GIDSignInCallbackSchemes : NSObject
 
-// Please call the designated initializer.
+/// Please call the designated initializer.
 - (instancetype)init NS_UNAVAILABLE;
 
-// The designated initializer.
-- (instancetype)initWithClientIdentifier:(NSString *)clientIdentifier NS_DESIGNATED_INITIALIZER;
+/// The designated initializer.
+- (instancetype)initWithClientIdentifier:(NSString *)clientIdentifier;
+
+/// The designated initializer.
+- (instancetype)initWithClientIdentifier:(NSString *)clientIdentifier
+                                  bundle:(nullable id<GIDBundle>)bundle NS_DESIGNATED_INITIALIZER;
 
-// 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.
+/// 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.
+/// 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

+ 17 - 5
GoogleSignIn/Sources/GIDSignInCallbackSchemes.m

@@ -13,11 +13,13 @@
 // limitations under the License.
 
 #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
+#import "GoogleSignIn/Tests/Unit/GIDAuthorizationTests/GIDBundleFake.h"
 
 NS_ASSUME_NONNULL_BEGIN
 
 @implementation GIDSignInCallbackSchemes {
   NSString *_clientIdentifier;
+  id<GIDBundle> _bundle;
 }
 
 /**
@@ -27,10 +29,9 @@ NS_ASSUME_NONNULL_BEGIN
  *     support for.
  * @remarks Branched from google3/googlemac/iPhone/Firebase/Source/GGLBundleUtil.m
  */
-+ (NSArray *)relevantURLSchemes {
+- (NSArray *)relevantURLSchemes {
   NSMutableArray *result = [NSMutableArray array];
-  NSBundle *bundle = [NSBundle mainBundle];
-  NSArray *urlTypes = [bundle objectForInfoDictionaryKey:@"CFBundleURLTypes"];
+  NSArray *urlTypes = [_bundle objectForInfoDictionaryKey:@"CFBundleURLTypes"];
   for (NSDictionary *urlType in urlTypes) {
     NSArray *urlTypeSchemes = urlType[@"CFBundleURLSchemes"];
     for (NSString *urlTypeScheme in urlTypeSchemes) {
@@ -41,9 +42,20 @@ NS_ASSUME_NONNULL_BEGIN
 }
 
 - (instancetype)initWithClientIdentifier:(NSString *)clientIdentifier {
+  return [self initWithClientIdentifier:clientIdentifier bundle:nil];
+}
+
+- (instancetype)initWithClientIdentifier:(NSString *)clientIdentifier
+                                  bundle:(nullable id<GIDBundle>)bundle {
   self = [super init];
   if (self) {
-    _clientIdentifier = [clientIdentifier copy];
+    _clientIdentifier = clientIdentifier;
+    if (!bundle) {
+      _bundle = [NSBundle mainBundle];
+    } else {
+      _bundle = bundle;
+    }
+//    _bundle = bundle ?: [NSBundle mainBundle];
   }
   return self;
 }
@@ -66,7 +78,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (NSMutableArray *)unsupportedSchemes {
   NSMutableArray *unsupportedSchemes = [NSMutableArray arrayWithArray:[self allSchemes]];
-  NSArray *supportedSchemes = [[self class] relevantURLSchemes];
+  NSArray *supportedSchemes = [self relevantURLSchemes];
   [unsupportedSchemes removeObjectsInArray:supportedSchemes];
   return unsupportedSchemes;
 }

+ 9 - 0
GoogleSignIn/Sources/GIDSignInInternalOptions.h

@@ -27,6 +27,8 @@
 @class GIDConfiguration;
 @class GIDSignInResult;
 
+@protocol GIDBundle;
+
 NS_ASSUME_NONNULL_BEGIN
 
 /// The options used internally for aspects of the sign-in flow.
@@ -68,18 +70,23 @@ NS_ASSUME_NONNULL_BEGIN
 /// and to mitigate replay attacks.
 @property(nonatomic, readonly, copy, nullable) NSString *nonce;
 
+/// The bundle used for the sign in flow.
+@property(nonatomic, readonly, nonnull) id<GIDBundle> bundle;
+
 /// Creates the default options.
 #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
 + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
                        presentingViewController:(nullable UIViewController *)presentingViewController
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
+                                         bundle:(nullable id<GIDBundle>)bundle
                                      completion:(nullable GIDSignInCompletion)completion;
 
 + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
                        presentingViewController:(nullable UIViewController *)presentingViewController
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
+                                         bundle:(nullable id<GIDBundle>)bundle
                                          scopes:(nullable NSArray *)scopes
                                           nonce:(nullable NSString *)nonce
                                      completion:(nullable GIDSignInCompletion)completion;
@@ -89,12 +96,14 @@ NS_ASSUME_NONNULL_BEGIN
                                presentingWindow:(nullable NSWindow *)presentingWindow
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
+                                         bundle:(nullable id<GIDBundle>)bundle
                                      completion:(nullable GIDSignInCompletion)completion;
 
 + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
                                presentingWindow:(nullable NSWindow *)presentingWindow
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
+                                         bundle:(nullable id<GIDBundle>)bundle
                                          scopes:(nullable NSArray *)scopes
                                           nonce:(nullable NSString *)nonce
                                      completion:(nullable GIDSignInCompletion)completion;

+ 9 - 0
GoogleSignIn/Sources/GIDSignInInternalOptions.m

@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
+#import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
 
 #if __has_include(<UIKit/UIKit.h>)
 #import <UIKit/UIKit.h>
@@ -30,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN
                        presentingViewController:(nullable UIViewController *)presentingViewController
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
+                                         bundle:(nullable id<GIDBundle>)bundle
                                          scopes:(nullable NSArray *)scopes
                                           nonce:(nullable NSString *)nonce
                                      completion:(nullable GIDSignInCompletion)completion {
@@ -38,6 +40,7 @@ NS_ASSUME_NONNULL_BEGIN
                                presentingWindow:(nullable NSWindow *)presentingWindow
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
+                                         bundle:(nullable id<GIDBundle>)bundle
                                          scopes:(nullable NSArray *)scopes
                                           nonce:(nullable NSString *)nonce
                                      completion:(nullable GIDSignInCompletion)completion {
@@ -55,6 +58,7 @@ NS_ASSUME_NONNULL_BEGIN
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
     options->_loginHint = loginHint;
     options->_completion = completion;
+    options->_bundle = bundle ?: [NSBundle mainBundle];
     options->_scopes = [GIDScopes scopesWithBasicProfile:scopes];
     options->_nonce = nonce;
   }
@@ -66,12 +70,14 @@ NS_ASSUME_NONNULL_BEGIN
                        presentingViewController:(nullable UIViewController *)presentingViewController
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
+                                         bundle:(nullable id<GIDBundle>)bundle
                                      completion:(nullable GIDSignInCompletion)completion {
 #elif TARGET_OS_OSX
 + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
                                presentingWindow:(nullable NSWindow *)presentingWindow
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
+                                         bundle:(nullable id<GIDBundle>)bundle
                                      completion:(nullable GIDSignInCompletion)completion {
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
     GIDSignInInternalOptions *options = [self defaultOptionsWithConfiguration:configuration
@@ -82,6 +88,7 @@ NS_ASSUME_NONNULL_BEGIN
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
                                                                     loginHint:loginHint
                                                                 addScopesFlow:addScopesFlow
+                                                                       bundle:bundle
                                                                        scopes:@[]
                                                                         nonce:nil
                                                                    completion:completion];
@@ -97,6 +104,7 @@ NS_ASSUME_NONNULL_BEGIN
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
                                                                   loginHint:nil
                                                               addScopesFlow:NO
+                                                                     bundle:nil
                                                                  completion:completion];
   if (options) {
     options->_interactive = NO;
@@ -121,6 +129,7 @@ NS_ASSUME_NONNULL_BEGIN
     options->_completion = _completion;
     options->_scopes = _scopes;
     options->_extraParams = [extraParams copy];
+    options->_bundle = _bundle ?: [NSBundle mainBundle];
   }
   return options;
 }

+ 79 - 5
GoogleSignIn/Tests/Unit/GIDAuthorizationTests/GIDAuthorizationTest.m

@@ -1,18 +1,36 @@
+// Copyright 2025 Google LLC
 //
-//  GIDAuthorizationTest.m
-//  GoogleSignIn
+// 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
 //
-//  Created by Matt Mathias on 4/7/25.
+//      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 <XCTest/XCTest.h>
-#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthorization.h"
 #import "GoogleSignIn/Sources/GIDAuthorization_Private.h"
+#import "GoogleSignIn/Sources/GIDAuthorizationFlow/Implementations/Fake/GIDAuthorizationFlowFake.h"
+#import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
+#import "GoogleSignIn/Sources/GIDSignIn_Private.h"
 #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
+#import "GoogleSignIn/Sources/GIDSignInResult_Private.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthorization.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInResult.h"
+#import "GoogleSignIn/Tests/Unit/GIDAuthorizationTests/GIDKeychainHelperFake.h"
+#import "GoogleSignIn/Tests/Unit/GIDAuthorizationTests/GIDBundleFake.h"
 #import "GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.h"
+#import "GoogleSignIn/Tests/Unit/GIDProfileData+Testing.h"
+#import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h"
 #import "GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.h"
 
-#import "GoogleSignIn/Sources/GIDAuthorizationFlow/Implementations/Fake/GIDAuthorizationFlowFake.h"
+static NSString *const kKeychainItemName = @"test_keychain_name";
+
+@import GTMAppAuth;
 
 @interface GIDAuthorizationTest : XCTestCase
 
@@ -31,6 +49,7 @@
                                                                     presentingViewController:nil
                                                                                    loginHint:nil
                                                                                addScopesFlow:NO
+                                                                                      bundle:nil
                                                                                   completion:nil];
   GIDAuthorizationFlowFake *fakeFlow = [[GIDAuthorizationFlowFake alloc] initWithSignInOptions:opts
                                                                                      authState:nil
@@ -58,6 +77,7 @@
                                                                     presentingViewController:vc
                                                                                    loginHint:nil
                                                                                addScopesFlow:NO
+                                                                                      bundle:nil
                                                                                   completion:nil];
   GIDAuthorizationFlowFake *fakeFlow = [[GIDAuthorizationFlowFake alloc] initWithSignInOptions:opts
                                                                                      authState:nil
@@ -78,4 +98,58 @@
   @finally {}
 }
 
+- (void)testThatAuthorizeInteractivelySetsCurrentUser {
+  XCTestExpectation *currentUserExpectation =
+    [self expectationWithDescription:@"Current user expectation"];
+  GIDKeychainHelperFake *keychainFake =
+    [[GIDKeychainHelperFake alloc] initWithKeychainAttributes:[NSSet setWithArray:@[]]];
+  GTMKeychainStore *keychainStore = [[GTMKeychainStore alloc] initWithItemName:kKeychainItemName
+                                                                keychainHelper:keychainFake];
+  
+  GIDConfiguration *config =
+    [[GIDConfiguration alloc] initWithClientID:OIDAuthorizationRequestTestingClientID
+                                serverClientID:kServerClientID
+                                  hostedDomain:kHostedDomain
+                                   openIDRealm:kOpenIDRealm];
+  UIViewController *vc = [[UIViewController alloc] init];
+  
+  OIDAuthState *expectedAuthState = [OIDAuthState testInstance];
+  GIDProfileData *expectedProfileData = [GIDProfileData testInstanceWithImageURL:@"test.com"];
+  GIDGoogleUser *expectedGoogleUser = [[GIDGoogleUser alloc] initWithAuthState:expectedAuthState
+                                                                   profileData:expectedProfileData];
+  
+  GIDSignInCompletion comp = ^(GIDSignInResult *_Nullable result, NSError *_Nullable error) {
+    XCTAssertNotNil(result, @"The sign in result should be non-nil.");
+    XCTAssertNil(error, @"There should be no error from authorizing.");
+    XCTAssertEqualObjects(expectedGoogleUser, result.user);
+    XCTAssertEqualObjects(expectedAuthState, result.user.authState);
+    XCTAssertEqualObjects(expectedProfileData, result.user.profile);
+    [currentUserExpectation fulfill];
+  };
+  
+  GIDBundleFake *bFake = [[GIDBundleFake alloc] init];
+  GIDSignInInternalOptions *opts = [GIDSignInInternalOptions defaultOptionsWithConfiguration:config
+                                                                    presentingViewController:vc
+                                                                                   loginHint:nil
+                                                                               addScopesFlow:NO
+                                                                                      bundle:bFake
+                                                                                  completion:comp];
+  GIDAuthorizationFlowFake *fakeFlow =
+    [[GIDAuthorizationFlowFake alloc] initWithSignInOptions:opts
+                                                  authState:expectedAuthState
+                                                profileData:expectedProfileData
+                                                 googleUser:expectedGoogleUser
+                                   externalUserAgentSession:nil
+                                                 emmSupport:nil
+                                                      error:nil];
+  
+  GIDAuthorization *auth = [[GIDAuthorization alloc] initWithKeychainStore:keychainStore
+                                                             configuration:config
+                                              authorizationFlowCoordinator:fakeFlow];
+  [auth signInWithOptions:opts];
+  [self waitForExpectations:@[currentUserExpectation] timeout:5];
+  XCTAssertNotNil(auth.currentUser);
+  XCTAssertEqualObjects(expectedGoogleUser, auth.currentUser);
+}
+
 @end

+ 24 - 0
GoogleSignIn/Tests/Unit/GIDAuthorizationTests/GIDBundleFake.h

@@ -0,0 +1,24 @@
+// 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 <Foundation/Foundation.h>
+#import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface GIDBundleFake : NSObject <GIDBundle>
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 28 - 0
GoogleSignIn/Tests/Unit/GIDAuthorizationTests/GIDBundleFake.m

@@ -0,0 +1,28 @@
+// 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 "GIDBundleFake.h"
+
+@implementation GIDBundleFake
+
+- (nullable id)objectForInfoDictionaryKey:(NSString *)key {
+  if (![key isEqualToString:@"CFBundleURLTypes"]) {
+    return nil;
+  }
+  // Reversed version of the below
+  //NSString *const OIDAuthorizationRequestTestingClientID = @"87654321.googleusercontent.com";
+  return @[@{@"CFBundleURLSchemes": @[@"com.googleusercontent.87654321"]}];
+}
+
+@end

+ 28 - 0
GoogleSignIn/Tests/Unit/GIDAuthorizationTests/GIDKeychainHelperFake.h

@@ -0,0 +1,28 @@
+// 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 <Foundation/Foundation.h>
+
+@import GTMAppAuth;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface GIDKeychainHelperFake : NSObject <GTMKeychainHelper>
+
+@property(nonatomic, readonly, copy) NSString *accountName;
+@property(nonatomic, readonly, copy) NSSet<GTMKeychainAttribute *> *keychainAttributes;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 136 - 0
GoogleSignIn/Tests/Unit/GIDAuthorizationTests/GIDKeychainHelperFake.m

@@ -0,0 +1,136 @@
+// 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 "GIDKeychainHelperFake.h"
+
+static NSString *const kAccountName = @"OAuthTest";
+
+@interface GIDKeychainHelperFake ()
+
+@property(nonatomic, copy) NSMutableDictionary<NSString *, NSData *> *passwordStore;
+
+@end
+
+@implementation GIDKeychainHelperFake
+
+- (instancetype)initWithKeychainAttributes:(NSSet<GTMKeychainAttribute *> *)keychainAttributes {
+  self = [super init];
+  if (self) {
+    _keychainAttributes = keychainAttributes;
+    _accountName = kAccountName;
+  }
+  return self;
+}
+
+- (NSDictionary<NSString *,id> * _Nonnull)keychainQueryForService:(NSString * _Nonnull)service { 
+  [NSException raise:@"Not Implemented" format:@"This method is not implemented"];
+}
+
+
+- (NSData * _Nullable)passwordDataForService:(NSString * _Nonnull)service
+                                       error:(NSError * _Nullable __autoreleasing * _Nullable)error {
+  if (service.length == 0) {
+    *error = [NSError errorWithDomain:@"GTMAppAuthKeychainErrorDomain"
+                                code:GTMKeychainStoreErrorCodeNoService
+                            userInfo:nil];
+    return;
+  }
+  
+  NSString *passwordKey = [service stringByAppendingString:self.accountName];
+  NSData *passwordData = self.passwordStore[passwordKey];
+  if (!passwordData) {
+    *error = [NSError errorWithDomain:@"GTMAppAuthKeychainErrorDomain"
+                                 code:GTMKeychainStoreErrorCodePasswordNotFound
+                             userInfo:nil];
+    return;
+  }
+  
+  return passwordData;
+}
+
+- (NSString * _Nullable)passwordForService:(NSString * _Nonnull)service
+                                     error:(NSError * _Nullable __autoreleasing * _Nullable)error {
+  NSData *passwordData = [self passwordDataForService:service error:error];
+  NSString *passwordString = [[NSString alloc] initWithData:passwordData
+                                                   encoding:NSUTF8StringEncoding];
+  return passwordString;
+}
+
+- (BOOL)removePasswordForService:(NSString * _Nonnull)service
+                           error:(NSError * _Nullable __autoreleasing * _Nullable)error {
+  if (service.length == 0) {
+    *error = [NSError errorWithDomain:@"GTMAppAuthKeychainErrorDomain"
+                                 code:GTMKeychainStoreErrorCodeNoService
+                             userInfo:nil];
+    return;
+  }
+  
+  NSString *passwordKey = [service stringByAppendingString:self.accountName];
+  [self.passwordStore removeObjectForKey:passwordKey];
+  return self.passwordStore[passwordKey] != nil;
+}
+
+- (BOOL)setPassword:(NSString * _Nonnull)password
+         forService:(NSString * _Nonnull)service
+      accessibility:(CFTypeRef _Nonnull)accessibility
+              error:(NSError * _Nullable __autoreleasing * _Nullable)error {
+  NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
+  if (!passwordData) {
+    *error = [NSError errorWithDomain:@"GTMAppAuthKeychainErrorDomain"
+                                 code:GTMKeychainStoreErrorCodeUnexpectedPasswordData
+                             userInfo:nil];
+    return NO;
+  }
+  return [self setPasswordWithData:passwordData
+                        forService:service
+                     accessibility:accessibility
+                             error:error];
+}
+
+- (BOOL)setPassword:(NSString * _Nonnull)password
+         forService:(NSString * _Nonnull)service
+              error:(NSError * _Nullable __autoreleasing * _Nullable)error {
+  NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
+  if (!passwordData) {
+    *error = [NSError errorWithDomain:@"GTMAppAuthKeychainErrorDomain"
+                                 code:GTMKeychainStoreErrorCodeUnexpectedPasswordData
+                             userInfo:nil];
+    return NO;
+  }
+  return [self setPasswordWithData:passwordData forService:service accessibility:nil error:error];
+}
+
+- (BOOL)setPasswordWithData:(NSData * _Nonnull)data
+                 forService:(NSString * _Nonnull)service
+              accessibility:(CFTypeRef _Nullable)accessibility
+                      error:(NSError * _Nullable __autoreleasing * _Nullable)error {
+  if (service.length == 0) {
+    *error = [NSError errorWithDomain:@"GTMAppAuthKeychainErrorDomain"
+                                 code:GTMKeychainStoreErrorCodeNoService
+                             userInfo:nil];
+    return;
+  }
+  NSString *passwordKey = [service stringByAppendingString:self.accountName];
+  [self.passwordStore setValue:data forKey:passwordKey];
+  if (self.passwordStore[passwordKey] != nil) {
+    return YES;
+  } else {
+    *error = [NSError errorWithDomain:@"GTMAppAuthKeychainErrorDomain"
+                                 code:GTMKeychainStoreErrorCodeFailedToSetPassword
+                             userInfo:nil];
+    return NO;
+  }
+}
+
+@end

+ 1 - 0
GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m

@@ -49,6 +49,7 @@
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
                                                       loginHint:loginHint
                                                   addScopesFlow:NO
+                                                         bundle:nil
                                                      completion:completion];
   XCTAssertTrue(options.interactive);
   XCTAssertFalse(options.continuation);