diff --git a/GoogleSignIn/Sources/GIDVerifyAccountDetail/Implementations/GIDVerifyAccountDetail.m b/GoogleSignIn/Sources/GIDVerifyAccountDetail/Implementations/GIDVerifyAccountDetail.m index e1a83795..e7b6e7bf 100644 --- a/GoogleSignIn/Sources/GIDVerifyAccountDetail/Implementations/GIDVerifyAccountDetail.m +++ b/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 *)accountDetails presentingViewController:(UIViewController *)presentingViewController completion:(nullable void (^)(GIDVerifiedAccountDetailResult *_Nullable verifyResult, @@ -42,11 +66,6 @@ - (void)verifyAccountDetails:(NSArray *)accountDet 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 @@ - (void)verifyAccountDetails:(NSArray *)accountDet [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 *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 diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h index f077be4d..6fb68b24 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDVerifyAccountDetail.h +++ b/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 diff --git a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h index 3086880e..6ff486de 100644 --- a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h +++ b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h @@ -16,18 +16,45 @@ #import +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 diff --git a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m index d017e60c..13d88b9f 100644 --- a/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m +++ b/GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m @@ -37,16 +37,38 @@ @implementation GIDFakeMainBundle { 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 @@ - (void)startFakingWithClientID:(NSString *)clientId { }]; } +- (void)startFakingWithClientID:(nullable NSString *)clientId { + _clientId = clientId; + + _fakeConfig = clientId ? [@{ @"GIDClientID" : clientId } mutableCopy] : [@{} mutableCopy]; + + [self startFaking]; +} + - (void)stopFaking { [GULSwizzler unswizzleClass:[NSBundle class] selector:@selector(objectForInfoDictionaryKey:) diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index c64f1611..537375d0 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -286,12 +286,14 @@ - (void)setUp { #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] diff --git a/GoogleSignIn/Tests/Unit/GIDVerifyAccountDetailTest.m b/GoogleSignIn/Tests/Unit/GIDVerifyAccountDetailTest.m new file mode 100644 index 00000000..a9ea0369 --- /dev/null +++ b/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 + +#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 *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