Przeglądaj źródła

Add sanity checks for passed in options and initializers to GIDVerifyAccountDetail. (#401)

Brianna Morales 1 rok temu
rodzic
commit
4b7802d349

+ 86 - 6
GoogleSignIn/Sources/GIDVerifyAccountDetail/Implementations/GIDVerifyAccountDetail.m

@@ -27,6 +27,30 @@
 
 @implementation GIDVerifyAccountDetail
 
+- (instancetype)initWithConfig:(GIDConfiguration *)configuration {
+  self = [super init];
+  if (self) {
+    _configuration = configuration;
+  }
+  return self;
+}
+
+- (instancetype)init {
+  GIDConfiguration *configuration;
+  NSBundle *bundle = NSBundle.mainBundle;
+  if (bundle) {
+    configuration = [GIDConfiguration configurationFromBundle:bundle];
+  }
+
+  if (!configuration) {
+    return nil;
+  }
+
+  return [self initWithConfig:configuration];
+}
+
+#pragma mark - Public methods
+
 - (void)verifyAccountDetails:(NSArray<GIDVerifiableAccountDetail *> *)accountDetails
     presentingViewController:(UIViewController *)presentingViewController
                   completion:(nullable void (^)(GIDVerifiedAccountDetailResult *_Nullable verifyResult,
@@ -42,11 +66,6 @@
                         hint:(nullable NSString *)hint
                   completion:(nullable void (^)(GIDVerifiedAccountDetailResult *_Nullable verifyResult,
                                                 NSError *_Nullable error))completion {
-  NSBundle *bundle = NSBundle.mainBundle;
-  if (bundle) {
-    _configuration = [GIDConfiguration configurationFromBundle:bundle];
-  }
-
   GIDSignInInternalOptions *options =
   [GIDSignInInternalOptions defaultOptionsWithConfiguration:_configuration
                                    presentingViewController:presentingViewController
@@ -58,8 +77,69 @@
   [self verifyAccountDetailsInteractivelyWithOptions:options];
 }
 
+#pragma mark - Authentication flow
+
 - (void)verifyAccountDetailsInteractivelyWithOptions:(GIDSignInInternalOptions *)options {
-  // TODO(#397): Sanity checks and start the incremental authorization flow.
+  if (!options.interactive) {
+    return;
+  }
+
+  // Ensure that a configuration is set.
+  if (!_configuration) {
+    // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
+    [NSException raise:NSInvalidArgumentException
+                format:@"No active configuration. Make sure GIDClientID is set in Info.plist."];
+    return;
+  }
+
+  [self assertValidCurrentUser];
+
+  // Explicitly throw exception for missing client ID here. This must come before
+  // scheme check because schemes rely on reverse client IDs.
+  [self assertValidParameters:options];
+
+  [self assertValidPresentingViewController:options];
+
+  // If the application does not support the required URL schemes tell the developer so.
+  GIDSignInCallbackSchemes *schemes =
+  [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:options.configuration.clientID];
+  NSArray<NSString *> *unsupportedSchemes = [schemes unsupportedSchemes];
+  if (unsupportedSchemes.count != 0) {
+    // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
+    [NSException raise:NSInvalidArgumentException
+                format:@"Your app is missing support for the following URL schemes: %@",
+     [unsupportedSchemes componentsJoinedByString:@", "]];
+  }
+  // TODO(#397): Start the incremental authorization flow.
+}
+
+#pragma mark - Helpers
+
+// Assert that a current user exists.
+- (void)assertValidCurrentUser {
+  if (!GIDSignIn.sharedInstance.currentUser) {
+    // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
+    [NSException raise:NSInvalidArgumentException
+                format:@"|currentUser| must be set to verify."];
+  }
+}
+
+// Asserts the parameters being valid.
+- (void)assertValidParameters:(GIDSignInInternalOptions *)options {
+  if (![options.configuration.clientID length]) {
+    // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
+    [NSException raise:NSInvalidArgumentException
+                format:@"You must specify |clientID| in |GIDConfiguration|"];
+  }
+}
+
+// Assert that the presenting view controller has been set.
+- (void)assertValidPresentingViewController:(GIDSignInInternalOptions *)options {
+  if (!options.presentingViewController) {
+    // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
+    [NSException raise:NSInvalidArgumentException
+                format:@"|presentingViewController| must be set."];
+  }
 }
 
 @end

+ 15 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h

@@ -43,6 +43,21 @@ typedef void (^GIDVerifyCompletion)(GIDVerifiedAccountDetailResult *_Nullable ve
 /// The active configuration for this instance of `GIDVerifyAccountDetail`.
 @property(nonatomic, nullable) GIDConfiguration *configuration;
 
+/// Initialize a `GIDVerifyAccountDetail` object by specifying all available properties.
+///
+/// @param config The configuration to be used.
+/// @return An initialized `GIDVerifyAccountDetail` instance.
+- (instancetype)initWithConfig:(GIDConfiguration *)config
+    NS_DESIGNATED_INITIALIZER;
+
+
+/// Initialize a `GIDVerifyAccountDetail` object by calling the designated initializer 
+/// with the default configuration from the bundle's Info.plist.
+///
+/// @return An initialized `GIDVerifyAccountDetail` instance.
+///     Otherwise, `nil` if the configuration cannot be automatically generated from your app's Info.plist.
+- (instancetype)init;
+
 /// Starts an interactive verification flow.
 ///
 /// The completion will be called at the end of this process.  Any saved verification

+ 34 - 5
GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h

@@ -16,18 +16,45 @@
 
 #import <Foundation/Foundation.h>
 
+NS_ASSUME_NONNULL_BEGIN
+
 /**
  * @class GIDFakeMainBundle
  * @brief Helps fake [NSBundle mainBundle]
  */
 @interface GIDFakeMainBundle : NSObject
 
+/**
+ * @fn initWithClientID:serverClientID:hostedDomain:openIDRealm:
+ * @brief Initializes a GIDFakeMainBundle object.
+ * @param clientID The fake client idenfitier for the app.
+ * @param serverClientID The fake server client idenfitier for the app.
+ * @param hostedDomain The fake hosted domain for the app.
+ * @param openIDRealm The fake OpenID realm for the app.
+ */
+- (instancetype)initWithClientID:(nullable id)clientID
+                  serverClientID:(nullable id)serverClientID
+                    hostedDomain:(nullable id)hostedDomain
+                     openIDRealm:(nullable id)openIDRealm
+    NS_DESIGNATED_INITIALIZER;
+
+/**
+ * @fn init:
+ * @brief Initializes a GIDFakeMainBundle object with `nil` to all of the designated initializer's parameters.
+ */
+- (instancetype)init;
+/**
+ * @fn startFaking:
+ * @brief Starts faking [NSBundle mainBundle]
+ */
+- (void)startFaking;
+
 /**
  * @fn startFakingWithClientID:
  * @brief Starts faking [NSBundle mainBundle]
  * @param clientID The fake client idenfitier for the app.
  */
-- (void)startFakingWithClientID:(NSString *)clientID;
+- (void)startFakingWithClientID:(nullable NSString *)clientID;
 
 /**
  * @fn stopFaking
@@ -87,9 +114,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

+ 34 - 4
GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m

@@ -37,16 +37,38 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   NSMutableDictionary *_fakeConfig;
 }
 
-- (void)startFakingWithClientID:(NSString *)clientId {
-  _clientId = clientId;
+- (instancetype)initWithClientID:(nullable id)clientID
+                  serverClientID:(nullable id)serverClientID
+                    hostedDomain:(nullable id)hostedDomain
+                     openIDRealm:(nullable id)openIDRealm {
+  self = [super init];
+
+  if (self) {
+    _clientId = clientID;
 
+    _fakeConfig = clientID ? [@{ @"GIDClientID" : clientID } mutableCopy] : [@{} mutableCopy];
+
+    [self fakeWithClientID:clientID
+            serverClientID:serverClientID
+              hostedDomain:hostedDomain
+               openIDRealm:openIDRealm];
+  }
+  return self;
+}
+
+- (instancetype)init {
+  return [self initWithClientID:nil
+                 serverClientID:nil
+                   hostedDomain:nil
+                    openIDRealm:nil];
+}
+
+- (void)startFaking {
   _fakedKeys = @[ kCFBundleURLTypesKey,
                   kConfigClientIDKey,
                   kConfigServerClientIDKey,
                   kConfigHostedDomainKey,
                   kConfigOpenIDRealmKey ];
-  
-  _fakeConfig = [@{ @"GIDClientID" : clientId } mutableCopy];
 
   [GULSwizzler swizzleClass:[NSBundle class]
                    selector:@selector(objectForInfoDictionaryKey:)
@@ -62,6 +84,14 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   }];
 }
 
+- (void)startFakingWithClientID:(nullable NSString *)clientId {
+  _clientId = clientId;
+
+  _fakeConfig = clientId ? [@{ @"GIDClientID" : clientId } mutableCopy] : [@{} mutableCopy];
+
+  [self startFaking];
+}
+
 - (void)stopFaking {
   [GULSwizzler unswizzleClass:[NSBundle class]
                      selector:@selector(objectForInfoDictionaryKey:)

+ 4 - 2
GoogleSignIn/Tests/Unit/GIDSignInTest.m

@@ -286,12 +286,14 @@ static NSString *const kNewScope = @"newScope";
 #elif TARGET_OS_OSX
   _presentingWindow = OCMStrictClassMock([NSWindow class]);
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
-  _authState = OCMStrictClassMock([OIDAuthState class]);
+
+  // TODO: Fix strict mocks from carrying forward to subsequent tests. (#410)
+  _authState = OCMClassMock([OIDAuthState class]);
   OCMStub([_authState alloc]).andReturn(_authState);
   OCMStub([_authState initWithAuthorizationResponse:OCMOCK_ANY]).andReturn(_authState);
   _tokenResponse = OCMStrictClassMock([OIDTokenResponse class]);
   _tokenRequest = OCMStrictClassMock([OIDTokenRequest class]);
-  _authorization = OCMStrictClassMock([GTMAuthSession class]);
+  _authorization = OCMClassMock([GTMAuthSession class]);
   _keychainStore = OCMStrictClassMock([GTMKeychainStore class]);
   OCMStub(
     [_keychainStore retrieveAuthSessionWithItemName:OCMOCK_ANY error:OCMArg.anyObjectRef]

+ 224 - 0
GoogleSignIn/Tests/Unit/GIDVerifyAccountDetailTest.m

@@ -0,0 +1,224 @@
+// Copyright 2024 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 <XCTest/XCTest.h>
+
+#if TARGET_OS_IOS
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifiableAccountDetail.h"
+
+#import "GoogleSignIn/Sources/GIDSignIn_Private.h"
+#import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
+
+#import "GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h"
+#import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h"
+
+// Exception Reasons
+static NSString * const kSchemesNotSupportedExceptionReason =
+    @"Your app is missing support for the following URL schemes: fakeclientid";
+static NSString * const kClientIDMissingExceptionReason =
+    @"You must specify |clientID| in |GIDConfiguration|";
+static NSString * const kMissingCurrentUserExceptionReason =
+    @"|currentUser| must be set to verify.";
+static NSString * const kMissingPresentingViewControllerExceptionReason =
+    @"|presentingViewController| must be set.";
+
+static NSString * const kClientId = @"FakeClientID";
+static NSString * const kServerClientId = @"FakeServerClientID";
+static NSString * const kOpenIDRealm = @"FakeRealm";
+static NSString * const kFakeHostedDomain = @"fakehosteddomain.com";
+
+@interface GIDVerifyAccountDetailTests : XCTestCase
+/// The |UIViewController| object being tested.
+@property UIViewController *presentingViewController;
+
+/// Fake [NSBundle mainBundle].
+@property GIDFakeMainBundle *fakeMainBundle;
+
+/// The |GIDVerifyAccountDetail| object being tested.
+@property GIDVerifyAccountDetail *verifyAccountDetail;
+
+/// The list of account details when testing [GIDVerifiableAccountDetail].
+@property NSArray<GIDVerifiableAccountDetail *> *verifiableAccountDetails;
+@end
+
+@implementation GIDVerifyAccountDetailTests
+
+#pragma mark - Lifecycle
+
+- (void)setUp {
+  [super setUp];
+
+  _presentingViewController = [[UIViewController alloc] init];
+
+  _verifyAccountDetail = [[GIDVerifyAccountDetail alloc] init];
+
+  GIDVerifiableAccountDetail *ageOver18Detail = 
+      [[GIDVerifiableAccountDetail alloc] initWithAccountDetailType:GIDAccountDetailTypeAgeOver18];
+  _verifiableAccountDetails = @[ageOver18Detail];
+
+  _fakeMainBundle = [[GIDFakeMainBundle alloc] initWithClientID:kClientId
+                                                 serverClientID:kServerClientId
+                                                   hostedDomain:kFakeHostedDomain
+                                                    openIDRealm:kOpenIDRealm];
+}
+
+#pragma mark - Tests
+
+- (void)testInit {
+  [_fakeMainBundle startFakingWithClientID:kClientId];
+
+  GIDVerifyAccountDetail *verifyAccountDetail = [[GIDVerifyAccountDetail alloc] init];
+  XCTAssertNotNil(verifyAccountDetail.configuration);
+  XCTAssertEqual(verifyAccountDetail.configuration.clientID, kClientId);
+  XCTAssertNil(verifyAccountDetail.configuration.serverClientID);
+  XCTAssertNil(verifyAccountDetail.configuration.hostedDomain);
+  XCTAssertNil(verifyAccountDetail.configuration.openIDRealm);
+
+  [_fakeMainBundle stopFaking];
+}
+
+- (void)testInit_noConfig {
+  [_fakeMainBundle startFakingWithClientID:nil];
+  GIDVerifyAccountDetail *verifyAccountDetail = [[GIDVerifyAccountDetail alloc] init];
+
+  XCTAssertNil(verifyAccountDetail.configuration);
+
+  [_fakeMainBundle stopFaking];
+}
+
+
+- (void)testInit_fullConfig {
+  [_fakeMainBundle startFaking];
+
+  GIDVerifyAccountDetail *verifyAccountDetail = [[GIDVerifyAccountDetail alloc] init];
+  XCTAssertNotNil(verifyAccountDetail.configuration);
+  XCTAssertEqual(verifyAccountDetail.configuration.clientID, kClientId);
+  XCTAssertEqual(verifyAccountDetail.configuration.serverClientID, kServerClientId);
+  XCTAssertEqual(verifyAccountDetail.configuration.hostedDomain, kFakeHostedDomain);
+  XCTAssertEqual(verifyAccountDetail.configuration.openIDRealm, kOpenIDRealm);
+
+  [_fakeMainBundle stopFaking];
+}
+
+- (void)testInit_invalidConfig {
+  _fakeMainBundle = [[GIDFakeMainBundle alloc] initWithClientID:@[ @"bad", @"config", @"values" ]
+                                                 serverClientID:nil
+                                                   hostedDomain:nil
+                                                    openIDRealm:nil];
+  [_fakeMainBundle startFaking];
+
+  GIDVerifyAccountDetail *verifyAccountDetail = [[GIDVerifyAccountDetail alloc] init];
+  XCTAssertNil(verifyAccountDetail.configuration);
+
+  [_fakeMainBundle stopFaking];
+}
+
+- (void)testInitWithConfig_fullConfig {
+  GIDConfiguration *configuration = [[GIDConfiguration alloc] initWithClientID:kClientId
+                                                                serverClientID:kServerClientId
+                                                                  hostedDomain:kFakeHostedDomain
+                                                                   openIDRealm:kOpenIDRealm];
+
+  GIDVerifyAccountDetail *verifyAccountDetail =
+      [[GIDVerifyAccountDetail alloc] initWithConfig:configuration];
+  XCTAssertNotNil(verifyAccountDetail.configuration);
+  XCTAssertEqual(verifyAccountDetail.configuration.clientID, kClientId);
+  XCTAssertEqual(verifyAccountDetail.configuration.serverClientID, kServerClientId);
+  XCTAssertEqual(verifyAccountDetail.configuration.hostedDomain, kFakeHostedDomain);
+  XCTAssertEqual(verifyAccountDetail.configuration.openIDRealm, kOpenIDRealm);
+}
+
+- (void)testCurrentUserException {
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wnonnull"
+  _verifyAccountDetail.configuration = [[GIDConfiguration alloc] initWithClientID:nil];
+#pragma GCC diagnostic pop
+
+  GIDSignIn.sharedInstance.currentUser = nil;
+
+  @try {
+    [_verifyAccountDetail verifyAccountDetails:_verifiableAccountDetails
+                      presentingViewController:_presentingViewController
+                                    completion:nil];
+  } @catch (NSException *exception) {
+    XCTAssertEqual(exception.name, NSInvalidArgumentException);
+    XCTAssertEqualObjects(exception.reason, kMissingCurrentUserExceptionReason);
+  }
+}
+
+- (void)testPresentingViewControllerException {
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wnonnull"
+  _verifyAccountDetail.configuration = [[GIDConfiguration alloc] initWithClientID:kClientId];
+#pragma GCC diagnostic pop
+  _presentingViewController = nil;
+
+  OIDAuthState *authState = [OIDAuthState testInstance];
+  GIDSignIn.sharedInstance.currentUser = [[GIDGoogleUser alloc] initWithAuthState:authState
+                                                                      profileData:nil];
+  @try {
+    [_verifyAccountDetail verifyAccountDetails:_verifiableAccountDetails
+                      presentingViewController:_presentingViewController
+                                    completion:nil];
+  } @catch (NSException *exception) {
+    XCTAssertEqual(exception.name, NSInvalidArgumentException);
+    XCTAssertEqualObjects(exception.reason, kMissingPresentingViewControllerExceptionReason);
+  }
+}
+
+- (void)testClientIDMissingException {
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wnonnull"
+  _verifyAccountDetail.configuration = [[GIDConfiguration alloc] initWithClientID:nil];
+#pragma GCC diagnostic pop
+
+  OIDAuthState *authState = [OIDAuthState testInstance];
+  GIDSignIn.sharedInstance.currentUser = [[GIDGoogleUser alloc] initWithAuthState:authState
+                                                                      profileData:nil];
+
+  @try {
+    [_verifyAccountDetail verifyAccountDetails:_verifiableAccountDetails
+                      presentingViewController:_presentingViewController
+                                    completion:nil];
+  } @catch (NSException *exception) {
+    XCTAssertEqual(exception.name, NSInvalidArgumentException);
+    XCTAssertEqualObjects(exception.reason, kClientIDMissingExceptionReason);
+  }
+}
+
+- (void)testSchemesNotSupportedException {
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wnonnull"
+  _verifyAccountDetail.configuration = [[GIDConfiguration alloc] initWithClientID:kClientId];
+#pragma GCC diagnostic pop
+
+  OIDAuthState *authState = [OIDAuthState testInstance];
+  GIDSignIn.sharedInstance.currentUser = [[GIDGoogleUser alloc] initWithAuthState:authState
+                                                                      profileData:nil];
+
+  @try {
+    [_verifyAccountDetail verifyAccountDetails:_verifiableAccountDetails
+                      presentingViewController:_presentingViewController
+                                    completion:nil];
+  } @catch (NSException *exception) {
+    XCTAssertEqual(exception.name, NSInvalidArgumentException);
+    XCTAssertEqualObjects(exception.reason, kSchemesNotSupportedExceptionReason);
+  }
+}
+
+@end
+
+#endif // TARGET_OS_IOS