From 4c2172b90e10d8ab2570c7c4175925bc6e307cc6 Mon Sep 17 00:00:00 2001 From: pinlu Date: Thu, 29 Dec 2022 15:10:52 -0800 Subject: [PATCH 01/10] Add GIDProfileDataFetcher API, implementation and test. --- GoogleSignIn/Sources/GIDProfileData.m | 30 ++++ .../API/GIDProfileDataFetcher.h | 36 +++++ .../Implementations/GIDProfileDataFetcher.h | 37 +++++ .../Implementations/GIDProfileDataFetcher.m | 87 ++++++++++ GoogleSignIn/Sources/GIDProfileData_Private.h | 11 ++ GoogleSignIn/Sources/GIDSignIn.m | 100 ++++-------- GoogleSignIn/Sources/GIDSignIn_Private.h | 2 + .../Tests/Unit/GIDProfileDataFetcherTest.m | 150 ++++++++++++++++++ GoogleSignIn/Tests/Unit/GIDSignInTest.m | 6 +- 9 files changed, 385 insertions(+), 74 deletions(-) create mode 100644 GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h create mode 100644 GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h create mode 100644 GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m create mode 100644 GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m diff --git a/GoogleSignIn/Sources/GIDProfileData.m b/GoogleSignIn/Sources/GIDProfileData.m index 09210836..a72d4330 100644 --- a/GoogleSignIn/Sources/GIDProfileData.m +++ b/GoogleSignIn/Sources/GIDProfileData.m @@ -16,6 +16,12 @@ #import "GoogleSignIn/Sources/GIDProfileData_Private.h" +#ifdef SWIFT_PACKAGE +@import AppAuth; +#else +#import +#endif + NS_ASSUME_NONNULL_BEGIN // Key constants used for encode and decode. @@ -26,6 +32,13 @@ static NSString *const kImageURLKey = @"image_url"; static NSString *const kOldImageURLStringKey = @"picture"; +// Basic profile (Fat ID Token / userinfo endpoint) keys +NSString *const kBasicProfileEmailKey = @"email"; +NSString *const kBasicProfilePictureKey = @"picture"; +NSString *const kBasicProfileNameKey = @"name"; +NSString *const kBasicProfileGivenNameKey = @"given_name"; +NSString *const kBasicProfileFamilyNameKey = @"family_name"; + @implementation GIDProfileData { NSURL *_imageURL; } @@ -46,6 +59,23 @@ - (instancetype)initWithEmail:(NSString *)email return self; } +- (instancetype)initWithIDToken:(OIDIDToken *)idToken { + if (!idToken || + !idToken.claims[kBasicProfilePictureKey] || + !idToken.claims[kBasicProfileNameKey] || + !idToken.claims[kBasicProfileGivenNameKey] || + !idToken.claims[kBasicProfileFamilyNameKey]) { + return nil; + } + + return [[GIDProfileData alloc] + initWithEmail:idToken.claims[kBasicProfileEmailKey] + name:idToken.claims[kBasicProfileNameKey] + givenName:idToken.claims[kBasicProfileGivenNameKey] + familyName:idToken.claims[kBasicProfileFamilyNameKey] + imageURL:[NSURL URLWithString:idToken.claims[kBasicProfilePictureKey]]]; +} + - (BOOL)hasImage { return _imageURL != nil; } diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h b/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h new file mode 100644 index 00000000..e625b48d --- /dev/null +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h @@ -0,0 +1,36 @@ +/* + * Copyright 2022 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 + +@class OIDAuthState; +@class GIDProfileData; + +NS_ASSUME_NONNULL_BEGIN + +@protocol GIDProfileDataFetcher + +/// Fetches the latest @GIDProfileData object. +/// +/// @param authState The state of the current OAuth session. +/// @param completion The block that is called on completion asynchronously. +- (void)fetchProfileDataWithAuthState:(OIDAuthState *)authState + completion:(void (^)(GIDProfileData *_Nullable profileData, + NSError *_Nullable error))completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h new file mode 100644 index 00000000..9598443b --- /dev/null +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h @@ -0,0 +1,37 @@ +/* + * Copyright 2022 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/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h" + +@class OIDIDToken; + +@protocol GIDHTTPFetcher; + +NS_ASSUME_NONNULL_BEGIN + +@interface GIDProfileDataFetcher : NSObject + +/// The convenience initializer. +- (instancetype)init; + +/// The initializer for unit test. +- (instancetype)initWithDataFetcher:(id)httpFetcher NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m new file mode 100644 index 00000000..acc479b0 --- /dev/null +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m @@ -0,0 +1,87 @@ +#import "GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h" + +#import "GoogleSignIn/Sources/GIDHTTPFetcher/API/GIDHTTPFetcher.h" +#import "GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.h" +#import "GoogleSignIn/Sources/GIDProfileData_Private.h" +#import "GoogleSignIn/Sources/GIDSignInPreferences.h" + +#ifdef SWIFT_PACKAGE +@import AppAuth; +@import GTMAppAuth; +#else +#import +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +// The URL template for the URL to get user info. +static NSString *const kUserInfoURLTemplate = @"https://%@/oauth2/v3/userinfo"; + +@implementation GIDProfileDataFetcher { + id _httpFetcher; +} + +- (instancetype)init { + GIDHTTPFetcher *httpFetcher = [[GIDHTTPFetcher alloc] init]; + return [self initWithDataFetcher:httpFetcher]; +} + +- (instancetype)initWithDataFetcher: (id)httpFetcher { + self = [super init]; + if (self) { + _httpFetcher = httpFetcher; + } + return self; +} + +- (void)fetchProfileDataWithAuthState:(OIDAuthState *)authState + completion:(void (^)(GIDProfileData *_Nullable profileData, + NSError *_Nullable error))completion { + OIDIDToken *idToken = + [[OIDIDToken alloc] initWithIDTokenString:authState.lastTokenResponse.idToken]; + // If the profile data are present in the ID token, use them. + if (idToken) { + GIDProfileData *profileData = [[GIDProfileData alloc]initWithIDToken:idToken]; + completion(profileData, nil); + return; + } + // If we can't retrieve profile data from the ID token, make a userInfo request to fetch them. + NSString *infoString = [NSString stringWithFormat:kUserInfoURLTemplate, + [GIDSignInPreferences googleUserInfoServer]]; + NSURL *infoURL = [NSURL URLWithString:infoString]; + NSMutableURLRequest *infoRequest = [NSMutableURLRequest requestWithURL:infoURL]; + GTMAppAuthFetcherAuthorization *authorization = + [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState]; + + [_httpFetcher fetchURLRequest:infoRequest + withAuthorizer:authorization + withComment:@"GIDSignIn: fetch basic profile info" + completion:^(NSData *data, NSError *error) { + if (error) { + completion(nil, error); + } else { + NSError *jsonDeserializationError; + NSDictionary *profileDict = + [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&jsonDeserializationError]; + if (profileDict) { + GIDProfileData *profileData = [[GIDProfileData alloc] + initWithEmail:idToken.claims[kBasicProfileEmailKey] + name:profileDict[kBasicProfileNameKey] + givenName:profileDict[kBasicProfileGivenNameKey] + familyName:profileDict[kBasicProfileFamilyNameKey] + imageURL:[NSURL URLWithString:profileDict[kBasicProfilePictureKey]]]; + completion(profileData, nil); + } + else { + completion(nil, jsonDeserializationError); + } + } + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDProfileData_Private.h b/GoogleSignIn/Sources/GIDProfileData_Private.h index 726df9c2..38a5864a 100644 --- a/GoogleSignIn/Sources/GIDProfileData_Private.h +++ b/GoogleSignIn/Sources/GIDProfileData_Private.h @@ -16,8 +16,16 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h" +@class OIDIDToken; + NS_ASSUME_NONNULL_BEGIN +extern NSString *const kBasicProfileEmailKey; +extern NSString *const kBasicProfilePictureKey; +extern NSString *const kBasicProfileNameKey; +extern NSString *const kBasicProfileGivenNameKey; +extern NSString *const kBasicProfileFamilyNameKey; + // Private |GIDProfileData| methods that are used in this SDK. @interface GIDProfileData () @@ -28,6 +36,9 @@ NS_ASSUME_NONNULL_BEGIN familyName:(nullable NSString *)familyName imageURL:(nullable NSURL *)imageURL; +/// Initialize with id token. +- (instancetype)initWithIDToken:(OIDIDToken *)idToken; + @end NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index 7904608d..e9a716dc 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -26,6 +26,8 @@ #import "GoogleSignIn/Sources/GIDEMMSupport.h" #import "GoogleSignIn/Sources/GIDKeychainHandler/API/GIDKeychainHandler.h" #import "GoogleSignIn/Sources/GIDKeychainHandler/Implementations/GIDKeychainHandler.h" +#import "GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h" +#import "GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h" #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h" #import "GoogleSignIn/Sources/GIDSignInPreferences.h" #import "GoogleSignIn/Sources/GIDCallbackQueue.h" @@ -80,9 +82,6 @@ // The URL template for the token endpoint. static NSString *const kTokenURLTemplate = @"https://%@/token"; -// The URL template for the URL to get user info. -static NSString *const kUserInfoURLTemplate = @"https://%@/oauth2/v3/userinfo"; - // The URL template for the URL to revoke the token. static NSString *const kRevokeTokenURLTemplate = @"https://%@/o/oauth2/revoke"; @@ -101,13 +100,6 @@ // Keychain constants for saving state in the authentication flow. static NSString *const kGTMAppAuthKeychainName = @"auth"; -// Basic profile (Fat ID Token / userinfo endpoint) keys -static NSString *const kBasicProfileEmailKey = @"email"; -static NSString *const kBasicProfilePictureKey = @"picture"; -static NSString *const kBasicProfileNameKey = @"name"; -static NSString *const kBasicProfileGivenNameKey = @"given_name"; -static NSString *const kBasicProfileFamilyNameKey = @"family_name"; - // Parameters in the callback URL coming back from browser. static NSString *const kAuthorizationCodeKeyName = @"code"; static NSString *const kOAuth2ErrorKeyName = @"error"; @@ -168,8 +160,12 @@ @implementation GIDSignIn { // Flag to indicate that the auth flow is restarting. BOOL _restarting; + // The class to execute keychain operations. id _keychainHandler; + // The class to fetch GIDProfileData object. + id _profileDataFetcher; + // The class to fetches data from a url end point. id _httpFetcher; } @@ -230,7 +226,7 @@ - (BOOL)restorePreviousSignInNoRefresh { // Restore current user without refreshing the access token. OIDIDToken *idToken = [[OIDIDToken alloc] initWithIDTokenString:authState.lastTokenResponse.idToken]; - GIDProfileData *profileData = [self profileDataWithIDToken:idToken]; + GIDProfileData *profileData = [[GIDProfileData alloc] initWithIDToken:idToken]; GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:authState profileData:profileData]; self.currentUser = user; @@ -462,12 +458,15 @@ + (GIDSignIn *)sharedInstance { - (id)initPrivate { id keychainHandler = [[GIDKeychainHandler alloc] init]; id httpFetcher = [[GIDHTTPFetcher alloc] init]; + id profileDataFetcher = [[GIDProfileDataFetcher alloc] init]; return [self initWithKeychainHandler:keychainHandler - httpFetcher:httpFetcher]; + httpFetcher:httpFetcher + profileDataFetcher:profileDataFetcher]; } - (instancetype)initWithKeychainHandler:(id)keychainHandler - httpFetcher:(id)httpFetcher{ + httpFetcher:(id)httpFetcher + profileDataFetcher:(id)profileDataFetcher{ self = [super init]; if (self) { // Get the bundle of the current executable. @@ -504,6 +503,7 @@ - (instancetype)initWithKeychainHandler:(id)keychainHandler _keychainHandler = keychainHandler; _httpFetcher = httpFetcher; + _profileDataFetcher = profileDataFetcher; } return self; } @@ -820,48 +820,20 @@ - (void)addDecodeIdTokenCallback:(GIDAuthFlow *)authFlow { if (!authState || handlerAuthFlow.error) { return; } - OIDIDToken *idToken = - [[OIDIDToken alloc] initWithIDTokenString: authState.lastTokenResponse.idToken]; - // If the profile data are present in the ID token, use them. - if (idToken) { - handlerAuthFlow.profileData = [self profileDataWithIDToken:idToken]; - } - - // If we can't retrieve profile data from the ID token, make a userInfo request to fetch them. - if (!handlerAuthFlow.profileData) { - [handlerAuthFlow wait]; - NSString *infoString = [NSString stringWithFormat:kUserInfoURLTemplate, - [GIDSignInPreferences googleUserInfoServer]]; - NSURL *infoURL = [NSURL URLWithString:infoString]; - NSMutableURLRequest *infoRequest = [NSMutableURLRequest requestWithURL:infoURL]; - GTMAppAuthFetcherAuthorization *authorization = - [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState]; - - [self->_httpFetcher fetchURLRequest:infoRequest - withAuthorizer:authorization - withComment:@"GIDSignIn: fetch basic profile info" - completion:^(NSData *data, NSError *error) { - if (data && !error) { - NSError *jsonDeserializationError; - NSDictionary *profileDict = - [NSJSONSerialization JSONObjectWithData:data - options:NSJSONReadingMutableContainers - error:&jsonDeserializationError]; - if (profileDict) { - handlerAuthFlow.profileData = [[GIDProfileData alloc] - initWithEmail:idToken.claims[kBasicProfileEmailKey] - name:profileDict[kBasicProfileNameKey] - givenName:profileDict[kBasicProfileGivenNameKey] - familyName:profileDict[kBasicProfileFamilyNameKey] - imageURL:[NSURL URLWithString:profileDict[kBasicProfilePictureKey]]]; - } - } - if (error) { - handlerAuthFlow.error = error; - } - [handlerAuthFlow next]; - }]; - } + + [handlerAuthFlow wait]; + + [self->_profileDataFetcher + fetchProfileDataWithAuthState:(OIDAuthState *)authState + completion:^(GIDProfileData *_Nullable profileData, + NSError *_Nullable error) { + if (error) { + handlerAuthFlow.error = error; + } else { + handlerAuthFlow.profileData = profileData; + } + [handlerAuthFlow next]; + }]; }]; } @@ -980,24 +952,6 @@ - (BOOL)isFreshInstall { return YES; } -// Generates user profile from OIDIDToken. -- (GIDProfileData *)profileDataWithIDToken:(OIDIDToken *)idToken { - if (!idToken || - !idToken.claims[kBasicProfilePictureKey] || - !idToken.claims[kBasicProfileNameKey] || - !idToken.claims[kBasicProfileGivenNameKey] || - !idToken.claims[kBasicProfileFamilyNameKey]) { - return nil; - } - - return [[GIDProfileData alloc] - initWithEmail:idToken.claims[kBasicProfileEmailKey] - name:idToken.claims[kBasicProfileNameKey] - givenName:idToken.claims[kBasicProfileGivenNameKey] - familyName:idToken.claims[kBasicProfileFamilyNameKey] - 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 { NSString *value; diff --git a/GoogleSignIn/Sources/GIDSignIn_Private.h b/GoogleSignIn/Sources/GIDSignIn_Private.h index 287e8cbd..94368cfc 100644 --- a/GoogleSignIn/Sources/GIDSignIn_Private.h +++ b/GoogleSignIn/Sources/GIDSignIn_Private.h @@ -31,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol GIDHTTPFetcher; @protocol GIDKeychainHandler; +@protocol GIDProfileDataFetcher; /// Represents a completion block that takes a `GIDSignInResult` on success or an error if the /// operation was unsuccessful. @@ -52,6 +53,7 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); /// The designated initializer. - (instancetype)initWithKeychainHandler:(id)keychainHandler httpFetcher:(id)HTTPFetcher + profileDataFetcher:(id)profileDataFetcher NS_DESIGNATED_INITIALIZER; /// Authenticates with extra options. diff --git a/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m b/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m new file mode 100644 index 00000000..a92dde6d --- /dev/null +++ b/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m @@ -0,0 +1,150 @@ +// Copyright 2022 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 + +// Test module imports +@import GoogleSignIn; + +#import "GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/Fakes/GIDFakeHTTPFetcher.h" +#import "GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h" +#import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h" +#import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h" + +static NSString *const kErrorDomain = @"ERROR_DOMAIN"; +static NSInteger const kErrorCode = 400; + +@interface GIDProfileDataFetcherTest : XCTestCase +@end + +@implementation GIDProfileDataFetcherTest { + GIDProfileDataFetcher *_profileDataFetcher; + GIDFakeHTTPFetcher *_httpFetcher; +} + +- (void)setUp { + [super setUp]; + _httpFetcher = [[GIDFakeHTTPFetcher alloc] init]; + _profileDataFetcher = [[GIDProfileDataFetcher alloc] initWithDataFetcher:_httpFetcher]; +} + +- (void)testFetchProfileData_outOfAuthState_success { + OIDTokenResponse *tokenResponse = + [OIDTokenResponse testInstanceWithIDToken:[OIDTokenResponse fatIDToken] + accessToken:kAccessToken + expiresIn:@(300) + refreshToken:kRefreshToken + tokenRequest:nil]; + OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse]; + + GIDHTTPFetcherTestBlock testBlock = + ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) { + XCTFail(@"_httpFetcher should not be invoked"); + }; + [_httpFetcher setTestBlock:testBlock]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Callback called with no error"]; + + [_profileDataFetcher + fetchProfileDataWithAuthState:authState + completion:^(GIDProfileData *profileData, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(profileData.name, kFatName); + XCTAssertEqualObjects(profileData.givenName, kFatGivenName); + XCTAssertEqualObjects(profileData.familyName, kFatFamilyName); + XCTAssertTrue(profileData.hasImage); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testFetchProfileData_fromInfoURL_fetchingError { + OIDTokenResponse *tokenResponse = + [OIDTokenResponse testInstanceWithIDToken:nil + accessToken:kAccessToken + expiresIn:@(300) + refreshToken:kRefreshToken + tokenRequest:nil]; + OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse]; + + XCTestExpectation *fetcherExpectation = + [self expectationWithDescription:@"_httpFetcher is invoked"]; + GIDHTTPFetcherTestBlock testBlock = + ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) { + NSError *fetchingError = [self error]; + responseProvider(nil, fetchingError); + [fetcherExpectation fulfill]; + }; + [_httpFetcher setTestBlock:testBlock]; + + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Callback called with error"]; + + [_profileDataFetcher + fetchProfileDataWithAuthState:authState + completion:^(GIDProfileData *profileData, NSError *error) { + XCTAssertNil(profileData); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, kErrorDomain); + XCTAssertEqual(error.code, kErrorCode); + [completionExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +// Fetch the NSData successfully but can not parse it. +- (void)testFetchProfileData_fromInfoURL_parsingError { + OIDTokenResponse *tokenResponse = + [OIDTokenResponse testInstanceWithIDToken:nil + accessToken:kAccessToken + expiresIn:@(300) + refreshToken:kRefreshToken + tokenRequest:nil]; + OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse]; + + XCTestExpectation *fetcherExpectation = + [self expectationWithDescription:@"_httpFetcher is invoked"]; + GIDHTTPFetcherTestBlock testBlock = + ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) { + NSData *data = [[NSData alloc] init]; + responseProvider(data, nil); + [fetcherExpectation fulfill]; + }; + [_httpFetcher setTestBlock:testBlock]; + + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Callback called with error"]; + + [_profileDataFetcher + fetchProfileDataWithAuthState:authState + completion:^(GIDProfileData *profileData, NSError *error) { + XCTAssertNil(profileData); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, NSCocoaErrorDomain); + [completionExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +#pragma mark - Helpers + +- (NSError *)error { + return [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:nil]; +} + +@end diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index fd13de71..fe1de7fd 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -33,6 +33,8 @@ #import "GoogleSignIn/Sources/GIDKeychainHandler/Implementations/Fakes/GIDFakeKeychainHandler.h" #import "GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/Fakes/GIDFakeHTTPFetcher.h" #import "GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.h" +#import "GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h" + #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" @@ -311,8 +313,10 @@ - (void)setUp { _httpFetcher = [[GIDFakeHTTPFetcher alloc] init]; + id profileDataFetcher = [[GIDProfileDataFetcher alloc] init]; _signIn = [[GIDSignIn alloc] initWithKeychainHandler:_keychainHandler - httpFetcher:_httpFetcher]; + httpFetcher:_httpFetcher + profileDataFetcher:profileDataFetcher]; _hint = nil; __weak GIDSignInTest *weakSelf = self; From 4d13ed2a2d3813e567a61f55e4784d89ef4c13f8 Mon Sep 17 00:00:00 2001 From: pinlu Date: Mon, 2 Jan 2023 11:51:21 -0800 Subject: [PATCH 02/10] Style improvement --- GoogleSignIn/Sources/GIDProfileData.m | 13 ++++--- .../Implementations/GIDProfileDataFetcher.h | 2 -- .../Implementations/GIDProfileDataFetcher.m | 34 +++++++++---------- GoogleSignIn/Sources/GIDProfileData_Private.h | 4 +-- GoogleSignIn/Sources/GIDSignIn.m | 6 ++-- 5 files changed, 28 insertions(+), 31 deletions(-) diff --git a/GoogleSignIn/Sources/GIDProfileData.m b/GoogleSignIn/Sources/GIDProfileData.m index a72d4330..99dad9c7 100644 --- a/GoogleSignIn/Sources/GIDProfileData.m +++ b/GoogleSignIn/Sources/GIDProfileData.m @@ -59,7 +59,7 @@ - (instancetype)initWithEmail:(NSString *)email return self; } -- (instancetype)initWithIDToken:(OIDIDToken *)idToken { +- (nullable instancetype)initWithIDToken:(OIDIDToken *)idToken { if (!idToken || !idToken.claims[kBasicProfilePictureKey] || !idToken.claims[kBasicProfileNameKey] || @@ -68,12 +68,11 @@ - (instancetype)initWithIDToken:(OIDIDToken *)idToken { return nil; } - return [[GIDProfileData alloc] - initWithEmail:idToken.claims[kBasicProfileEmailKey] - name:idToken.claims[kBasicProfileNameKey] - givenName:idToken.claims[kBasicProfileGivenNameKey] - familyName:idToken.claims[kBasicProfileFamilyNameKey] - imageURL:[NSURL URLWithString:idToken.claims[kBasicProfilePictureKey]]]; + return [self initWithEmail:idToken.claims[kBasicProfileEmailKey] + name:idToken.claims[kBasicProfileNameKey] + givenName:idToken.claims[kBasicProfileGivenNameKey] + familyName:idToken.claims[kBasicProfileFamilyNameKey] + imageURL:[NSURL URLWithString:idToken.claims[kBasicProfilePictureKey]]]; } - (BOOL)hasImage { diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h index 9598443b..a9d1c1f0 100644 --- a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h @@ -18,8 +18,6 @@ #import "GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h" -@class OIDIDToken; - @protocol GIDHTTPFetcher; NS_ASSUME_NONNULL_BEGIN diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m index acc479b0..b984ba1c 100644 --- a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m @@ -27,7 +27,7 @@ - (instancetype)init { return [self initWithDataFetcher:httpFetcher]; } -- (instancetype)initWithDataFetcher: (id)httpFetcher { +- (instancetype)initWithDataFetcher:(id)httpFetcher { self = [super init]; if (self) { _httpFetcher = httpFetcher; @@ -42,10 +42,11 @@ - (void)fetchProfileDataWithAuthState:(OIDAuthState *)authState [[OIDIDToken alloc] initWithIDTokenString:authState.lastTokenResponse.idToken]; // If the profile data are present in the ID token, use them. if (idToken) { - GIDProfileData *profileData = [[GIDProfileData alloc]initWithIDToken:idToken]; + GIDProfileData *profileData = [[GIDProfileData alloc] initWithIDToken:idToken]; completion(profileData, nil); return; } + // If we can't retrieve profile data from the ID token, make a userInfo request to fetch them. NSString *infoString = [NSString stringWithFormat:kUserInfoURLTemplate, [GIDSignInPreferences googleUserInfoServer]]; @@ -60,25 +61,24 @@ - (void)fetchProfileDataWithAuthState:(OIDAuthState *)authState completion:^(NSData *data, NSError *error) { if (error) { completion(nil, error); - } else { - NSError *jsonDeserializationError; - NSDictionary *profileDict = + return; + } + NSError *jsonDeserializationError; + NSDictionary *profileDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&jsonDeserializationError]; - if (profileDict) { - GIDProfileData *profileData = [[GIDProfileData alloc] - initWithEmail:idToken.claims[kBasicProfileEmailKey] - name:profileDict[kBasicProfileNameKey] - givenName:profileDict[kBasicProfileGivenNameKey] - familyName:profileDict[kBasicProfileFamilyNameKey] - imageURL:[NSURL URLWithString:profileDict[kBasicProfilePictureKey]]]; - completion(profileData, nil); - } - else { - completion(nil, jsonDeserializationError); - } + if (jsonDeserializationError) { + completion(nil, jsonDeserializationError); + return; } + GIDProfileData *profileData = [[GIDProfileData alloc] + initWithEmail:idToken.claims[kBasicProfileEmailKey] + name:profileDict[kBasicProfileNameKey] + givenName:profileDict[kBasicProfileGivenNameKey] + familyName:profileDict[kBasicProfileFamilyNameKey] + imageURL:[NSURL URLWithString:profileDict[kBasicProfilePictureKey]]]; + completion(profileData, nil); }]; } diff --git a/GoogleSignIn/Sources/GIDProfileData_Private.h b/GoogleSignIn/Sources/GIDProfileData_Private.h index 38a5864a..6b1ba5ad 100644 --- a/GoogleSignIn/Sources/GIDProfileData_Private.h +++ b/GoogleSignIn/Sources/GIDProfileData_Private.h @@ -34,10 +34,10 @@ extern NSString *const kBasicProfileFamilyNameKey; name:(NSString *)name givenName:(nullable NSString *)givenName familyName:(nullable NSString *)familyName - imageURL:(nullable NSURL *)imageURL; + imageURL:(nullable NSURL *)imageURL NS_DESIGNATED_INITIALIZER; /// Initialize with id token. -- (instancetype)initWithIDToken:(OIDIDToken *)idToken; +- (nullable instancetype)initWithIDToken:(OIDIDToken *)idToken; @end diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index e9a716dc..92b37a71 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -824,9 +824,9 @@ - (void)addDecodeIdTokenCallback:(GIDAuthFlow *)authFlow { [handlerAuthFlow wait]; [self->_profileDataFetcher - fetchProfileDataWithAuthState:(OIDAuthState *)authState - completion:^(GIDProfileData *_Nullable profileData, - NSError *_Nullable error) { + fetchProfileDataWithAuthState:(OIDAuthState *)authState + completion:^(GIDProfileData *_Nullable profileData, + NSError *_Nullable error) { if (error) { handlerAuthFlow.error = error; } else { From 9996646d2022ab041333a6f15d8a32a826569da2 Mon Sep 17 00:00:00 2001 From: pinlu Date: Tue, 3 Jan 2023 10:44:53 -0800 Subject: [PATCH 03/10] FIx the NS_DESIGNATED_INITIALIZER warning. --- GoogleSignIn/Sources/GIDProfileData.m | 29 ++++++++++--------- GoogleSignIn/Sources/GIDProfileData_Private.h | 2 ++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/GoogleSignIn/Sources/GIDProfileData.m b/GoogleSignIn/Sources/GIDProfileData.m index 99dad9c7..f156d2a4 100644 --- a/GoogleSignIn/Sources/GIDProfileData.m +++ b/GoogleSignIn/Sources/GIDProfileData.m @@ -138,21 +138,22 @@ + (BOOL)supportsSecureCoding { } - (nullable instancetype)initWithCoder:(NSCoder *)decoder { - self = [super init]; - if (self) { - _email = [decoder decodeObjectOfClass:[NSString class] forKey:kEmailKey]; - _name = [decoder decodeObjectOfClass:[NSString class] forKey:kNameKey]; - _givenName = [decoder decodeObjectOfClass:[NSString class] forKey:kGivenNameKey]; - _familyName = [decoder decodeObjectOfClass:[NSString class] forKey:kFamilyNameKey]; - _imageURL = [decoder decodeObjectOfClass:[NSURL class] forKey:kImageURLKey]; - - // Check to see if this is an old archive, if so, try decoding the old image URL string key. - if ([decoder containsValueForKey:kOldImageURLStringKey]) { - _imageURL = [NSURL URLWithString:[decoder decodeObjectOfClass:[NSString class] - forKey:kOldImageURLStringKey]]; - } + NSString *email = [decoder decodeObjectOfClass:[NSString class] forKey:kEmailKey]; + NSString *name = [decoder decodeObjectOfClass:[NSString class] forKey:kNameKey]; + NSString *givenName = [decoder decodeObjectOfClass:[NSString class] forKey:kGivenNameKey]; + NSString *familyName = [decoder decodeObjectOfClass:[NSString class] forKey:kFamilyNameKey]; + NSURL *imageURL = [decoder decodeObjectOfClass:[NSURL class] forKey:kImageURLKey]; + + // Check to see if this is an old archive, if so, try decoding the old image URL string key. + if ([decoder containsValueForKey:kOldImageURLStringKey]) { + imageURL = [NSURL URLWithString:[decoder decodeObjectOfClass:[NSString class] + forKey:kOldImageURLStringKey]]; } - return self; + + return [self initWithEmail:email + name:name givenName:givenName + familyName:familyName + imageURL:imageURL]; } - (void)encodeWithCoder:(NSCoder *)encoder { diff --git a/GoogleSignIn/Sources/GIDProfileData_Private.h b/GoogleSignIn/Sources/GIDProfileData_Private.h index 6b1ba5ad..7b84c701 100644 --- a/GoogleSignIn/Sources/GIDProfileData_Private.h +++ b/GoogleSignIn/Sources/GIDProfileData_Private.h @@ -39,6 +39,8 @@ extern NSString *const kBasicProfileFamilyNameKey; /// Initialize with id token. - (nullable instancetype)initWithIDToken:(OIDIDToken *)idToken; +- (instancetype)init NS_UNAVAILABLE; + @end NS_ASSUME_NONNULL_END From cb17e5f885d9381cb0a806231aba96dd4b6c2553 Mon Sep 17 00:00:00 2001 From: pinlu Date: Tue, 3 Jan 2023 15:04:07 -0800 Subject: [PATCH 04/10] Use better name and fix style. --- .../GIDProfileDataFetcher/API/GIDProfileDataFetcher.h | 5 ++++- .../Implementations/GIDProfileDataFetcher.h | 6 +++--- .../Implementations/GIDProfileDataFetcher.m | 4 ++-- GoogleSignIn/Sources/GIDSignIn.m | 4 ++-- GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h b/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h index e625b48d..453ca821 100644 --- a/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,9 @@ NS_ASSUME_NONNULL_BEGIN /// Fetches the latest @GIDProfileData object. /// +/// This method either extracts the profile data out of the OIDAuthState object or fetches it +/// from the Google user Info server. +/// /// @param authState The state of the current OAuth session. /// @param completion The block that is called on completion asynchronously. - (void)fetchProfileDataWithAuthState:(OIDAuthState *)authState diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h index a9d1c1f0..03d48d1f 100644 --- a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,13 +22,13 @@ NS_ASSUME_NONNULL_BEGIN -@interface GIDProfileDataFetcher : NSObject +@interface GIDProfileDataFetcher : NSObject /// The convenience initializer. - (instancetype)init; /// The initializer for unit test. -- (instancetype)initWithDataFetcher:(id)httpFetcher NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithHTTPFetcher:(id)httpFetcher NS_DESIGNATED_INITIALIZER; @end diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m index b984ba1c..01a0b912 100644 --- a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m @@ -24,10 +24,10 @@ @implementation GIDProfileDataFetcher { - (instancetype)init { GIDHTTPFetcher *httpFetcher = [[GIDHTTPFetcher alloc] init]; - return [self initWithDataFetcher:httpFetcher]; + return [self initWithHTTPFetcher:httpFetcher]; } -- (instancetype)initWithDataFetcher:(id)httpFetcher { +- (instancetype)initWithHTTPFetcher:(id)httpFetcher { self = [super init]; if (self) { _httpFetcher = httpFetcher; diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index 92b37a71..d22af2dd 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -1,4 +1,4 @@ -// Copyright 2021 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -466,7 +466,7 @@ - (id)initPrivate { - (instancetype)initWithKeychainHandler:(id)keychainHandler httpFetcher:(id)httpFetcher - profileDataFetcher:(id)profileDataFetcher{ + profileDataFetcher:(id)profileDataFetcher { self = [super init]; if (self) { // Get the bundle of the current executable. diff --git a/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m b/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m index a92dde6d..e4081266 100644 --- a/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m +++ b/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m @@ -36,7 +36,7 @@ @implementation GIDProfileDataFetcherTest { - (void)setUp { [super setUp]; _httpFetcher = [[GIDFakeHTTPFetcher alloc] init]; - _profileDataFetcher = [[GIDProfileDataFetcher alloc] initWithDataFetcher:_httpFetcher]; + _profileDataFetcher = [[GIDProfileDataFetcher alloc] initWithHTTPFetcher:_httpFetcher]; } - (void)testFetchProfileData_outOfAuthState_success { From 8566e797d0a5adf0b2b2b2d434f19a4d8d68921b Mon Sep 17 00:00:00 2001 From: pinlu Date: Wed, 4 Jan 2023 10:34:23 -0800 Subject: [PATCH 05/10] Improve wording. --- .../GIDProfileDataFetcher/API/GIDProfileDataFetcher.h | 2 +- GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h b/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h index 453ca821..28e47f4e 100644 --- a/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h @@ -26,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN /// Fetches the latest @GIDProfileData object. /// /// This method either extracts the profile data out of the OIDAuthState object or fetches it -/// from the Google user Info server. +/// from the Google user info server. /// /// @param authState The state of the current OAuth session. /// @param completion The block that is called on completion asynchronously. diff --git a/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m b/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m index e4081266..17bd54ad 100644 --- a/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m +++ b/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m @@ -55,7 +55,7 @@ - (void)testFetchProfileData_outOfAuthState_success { [_httpFetcher setTestBlock:testBlock]; XCTestExpectation *expectation = - [self expectationWithDescription:@"Callback called with no error"]; + [self expectationWithDescription:@"completion is invoked"]; [_profileDataFetcher fetchProfileDataWithAuthState:authState @@ -91,7 +91,7 @@ - (void)testFetchProfileData_fromInfoURL_fetchingError { [_httpFetcher setTestBlock:testBlock]; XCTestExpectation *completionExpectation = - [self expectationWithDescription:@"Callback called with error"]; + [self expectationWithDescription:@"completion is invoked"]; [_profileDataFetcher fetchProfileDataWithAuthState:authState @@ -127,7 +127,7 @@ - (void)testFetchProfileData_fromInfoURL_parsingError { [_httpFetcher setTestBlock:testBlock]; XCTestExpectation *completionExpectation = - [self expectationWithDescription:@"Callback called with error"]; + [self expectationWithDescription:@"completion is invoked"]; [_profileDataFetcher fetchProfileDataWithAuthState:authState From 4ec4c4434f5a66d87de6b7483989464b3421d48a Mon Sep 17 00:00:00 2001 From: pinlu Date: Wed, 4 Jan 2023 18:22:16 -0800 Subject: [PATCH 06/10] Improve documentation. --- .../API/GIDProfileDataFetcher.h | 4 ++-- .../Implementations/GIDProfileDataFetcher.m | 21 +++++++++++++++++-- GoogleSignIn/Sources/GIDProfileData_Private.h | 4 +--- .../Public/GoogleSignIn/GIDProfileData.h | 4 ++++ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h b/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h index 28e47f4e..94df8c0d 100644 --- a/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h @@ -25,8 +25,8 @@ NS_ASSUME_NONNULL_BEGIN /// Fetches the latest @GIDProfileData object. /// -/// This method either extracts the profile data out of the OIDAuthState object or fetches it -/// from the Google user info server. +/// "This method either extracts profile data from `OIDIDToken` in `OIDAuthState` or fetches it +/// from the UserInfo endpoint." /// /// @param authState The state of the current OAuth session. /// @param completion The block that is called on completion asynchronously. diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m index 01a0b912..feba81c1 100644 --- a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h" #import "GoogleSignIn/Sources/GIDHTTPFetcher/API/GIDHTTPFetcher.h" @@ -40,14 +56,15 @@ - (void)fetchProfileDataWithAuthState:(OIDAuthState *)authState NSError *_Nullable error))completion { OIDIDToken *idToken = [[OIDIDToken alloc] initWithIDTokenString:authState.lastTokenResponse.idToken]; - // If the profile data are present in the ID token, use them. + // If profile data is present in the ID token, use it. if (idToken) { GIDProfileData *profileData = [[GIDProfileData alloc] initWithIDToken:idToken]; completion(profileData, nil); return; } - // If we can't retrieve profile data from the ID token, make a userInfo request to fetch them. + // If we can't retrieve profile data from the ID token, make a UserInfo endpoint request to + // fetch it. NSString *infoString = [NSString stringWithFormat:kUserInfoURLTemplate, [GIDSignInPreferences googleUserInfoServer]]; NSURL *infoURL = [NSURL URLWithString:infoString]; diff --git a/GoogleSignIn/Sources/GIDProfileData_Private.h b/GoogleSignIn/Sources/GIDProfileData_Private.h index 7b84c701..5d947c4b 100644 --- a/GoogleSignIn/Sources/GIDProfileData_Private.h +++ b/GoogleSignIn/Sources/GIDProfileData_Private.h @@ -36,11 +36,9 @@ extern NSString *const kBasicProfileFamilyNameKey; familyName:(nullable NSString *)familyName imageURL:(nullable NSURL *)imageURL NS_DESIGNATED_INITIALIZER; -/// Initialize with id token. +/// Initialize with ID token. - (nullable instancetype)initWithIDToken:(OIDIDToken *)idToken; -- (instancetype)init NS_UNAVAILABLE; - @end NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h index bf797362..b44c8961 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h @@ -42,6 +42,10 @@ NS_ASSUME_NONNULL_BEGIN /// @return The URL of the user's profile image. - (nullable NSURL *)imageURLWithDimension:(NSUInteger)dimension; +- (instancetype)init NS_UNAVAILABLE; + ++ (instancetype)new NS_UNAVAILABLE; + @end NS_ASSUME_NONNULL_END From 4ac5a72d0173cff481bd195356937dd42e82f779 Mon Sep 17 00:00:00 2001 From: pinlu Date: Thu, 5 Jan 2023 13:50:42 -0800 Subject: [PATCH 07/10] Remove unused const. --- GoogleSignIn/Sources/GIDSignIn.m | 3 --- 1 file changed, 3 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index 643b2be9..21711280 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -76,9 +76,6 @@ // The name of the query parameter used for logging the restart of auth from EMM callback. static NSString *const kEMMRestartAuthParameter = @"emmres"; -// The URL template for the URL to get user info. -static NSString *const kUserInfoURLTemplate = @"https://%@/oauth2/v3/userinfo"; - // The URL template for the URL to revoke the token. static NSString *const kRevokeTokenURLTemplate = @"https://%@/o/oauth2/revoke"; From 3c9e88315b4a83a9c7f6922baccb30e3908ddb7e Mon Sep 17 00:00:00 2001 From: pinlu Date: Mon, 9 Jan 2023 13:20:28 -0800 Subject: [PATCH 08/10] Move the new GIDProfileData initializer to GIDProfileDataFetcher The profile data items in the ID token and UserInfo endpoint response share keys so we decide to put them together in GIDProfileDataFetcher. --- GoogleSignIn/Sources/GIDProfileData.m | 23 ---------------- .../API/GIDProfileDataFetcher.h | 8 +++++- .../Implementations/GIDProfileDataFetcher.m | 26 ++++++++++++++++++- GoogleSignIn/Sources/GIDProfileData_Private.h | 11 -------- GoogleSignIn/Sources/GIDSignIn.m | 2 +- 5 files changed, 33 insertions(+), 37 deletions(-) diff --git a/GoogleSignIn/Sources/GIDProfileData.m b/GoogleSignIn/Sources/GIDProfileData.m index f156d2a4..53f1a89b 100644 --- a/GoogleSignIn/Sources/GIDProfileData.m +++ b/GoogleSignIn/Sources/GIDProfileData.m @@ -32,13 +32,6 @@ static NSString *const kImageURLKey = @"image_url"; static NSString *const kOldImageURLStringKey = @"picture"; -// Basic profile (Fat ID Token / userinfo endpoint) keys -NSString *const kBasicProfileEmailKey = @"email"; -NSString *const kBasicProfilePictureKey = @"picture"; -NSString *const kBasicProfileNameKey = @"name"; -NSString *const kBasicProfileGivenNameKey = @"given_name"; -NSString *const kBasicProfileFamilyNameKey = @"family_name"; - @implementation GIDProfileData { NSURL *_imageURL; } @@ -59,22 +52,6 @@ - (instancetype)initWithEmail:(NSString *)email return self; } -- (nullable instancetype)initWithIDToken:(OIDIDToken *)idToken { - if (!idToken || - !idToken.claims[kBasicProfilePictureKey] || - !idToken.claims[kBasicProfileNameKey] || - !idToken.claims[kBasicProfileGivenNameKey] || - !idToken.claims[kBasicProfileFamilyNameKey]) { - return nil; - } - - return [self initWithEmail:idToken.claims[kBasicProfileEmailKey] - name:idToken.claims[kBasicProfileNameKey] - givenName:idToken.claims[kBasicProfileGivenNameKey] - familyName:idToken.claims[kBasicProfileFamilyNameKey] - imageURL:[NSURL URLWithString:idToken.claims[kBasicProfilePictureKey]]]; -} - - (BOOL)hasImage { return _imageURL != nil; } diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h b/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h index 94df8c0d..f1e5d5a2 100644 --- a/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h @@ -17,13 +17,14 @@ #import @class OIDAuthState; +@class OIDIDToken; @class GIDProfileData; NS_ASSUME_NONNULL_BEGIN @protocol GIDProfileDataFetcher -/// Fetches the latest @GIDProfileData object. +/// Fetches the latest `GIDProfileData` object. /// /// "This method either extracts profile data from `OIDIDToken` in `OIDAuthState` or fetches it /// from the UserInfo endpoint." @@ -34,6 +35,11 @@ NS_ASSUME_NONNULL_BEGIN completion:(void (^)(GIDProfileData *_Nullable profileData, NSError *_Nullable error))completion; +/// Fetches the latest `GIDProfileData` object. +/// +/// @param idToken The ID token. +- (nullable GIDProfileData*)fetchProfileDataWithIDToken:(OIDIDToken *)idToken; + @end NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m index feba81c1..7ae8a874 100644 --- a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m @@ -34,6 +34,13 @@ // The URL template for the URL to get user info. static NSString *const kUserInfoURLTemplate = @"https://%@/oauth2/v3/userinfo"; +// Basic profile (Fat ID Token / userinfo endpoint) keys +static NSString *const kBasicProfileEmailKey = @"email"; +static NSString *const kBasicProfilePictureKey = @"picture"; +static NSString *const kBasicProfileNameKey = @"name"; +static NSString *const kBasicProfileGivenNameKey = @"given_name"; +static NSString *const kBasicProfileFamilyNameKey = @"family_name"; + @implementation GIDProfileDataFetcher { id _httpFetcher; } @@ -58,7 +65,7 @@ - (void)fetchProfileDataWithAuthState:(OIDAuthState *)authState [[OIDIDToken alloc] initWithIDTokenString:authState.lastTokenResponse.idToken]; // If profile data is present in the ID token, use it. if (idToken) { - GIDProfileData *profileData = [[GIDProfileData alloc] initWithIDToken:idToken]; + GIDProfileData *profileData = [self fetchProfileDataWithIDToken:idToken]; completion(profileData, nil); return; } @@ -99,6 +106,23 @@ - (void)fetchProfileDataWithAuthState:(OIDAuthState *)authState }]; } +- (nullable GIDProfileData*)fetchProfileDataWithIDToken:(OIDIDToken *)idToken { + if (!idToken || + !idToken.claims[kBasicProfilePictureKey] || + !idToken.claims[kBasicProfileNameKey] || + !idToken.claims[kBasicProfileGivenNameKey] || + !idToken.claims[kBasicProfileFamilyNameKey]) { + return nil; + } + + return [[GIDProfileData alloc] + initWithEmail:idToken.claims[kBasicProfileEmailKey] + name:idToken.claims[kBasicProfileNameKey] + givenName:idToken.claims[kBasicProfileGivenNameKey] + familyName:idToken.claims[kBasicProfileFamilyNameKey] + imageURL:[NSURL URLWithString:idToken.claims[kBasicProfilePictureKey]]]; +} + @end NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDProfileData_Private.h b/GoogleSignIn/Sources/GIDProfileData_Private.h index 5d947c4b..e85f196d 100644 --- a/GoogleSignIn/Sources/GIDProfileData_Private.h +++ b/GoogleSignIn/Sources/GIDProfileData_Private.h @@ -16,16 +16,8 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h" -@class OIDIDToken; - NS_ASSUME_NONNULL_BEGIN -extern NSString *const kBasicProfileEmailKey; -extern NSString *const kBasicProfilePictureKey; -extern NSString *const kBasicProfileNameKey; -extern NSString *const kBasicProfileGivenNameKey; -extern NSString *const kBasicProfileFamilyNameKey; - // Private |GIDProfileData| methods that are used in this SDK. @interface GIDProfileData () @@ -36,9 +28,6 @@ extern NSString *const kBasicProfileFamilyNameKey; familyName:(nullable NSString *)familyName imageURL:(nullable NSURL *)imageURL NS_DESIGNATED_INITIALIZER; -/// Initialize with ID token. -- (nullable instancetype)initWithIDToken:(OIDIDToken *)idToken; - @end NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index 21711280..d1c0e9fc 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -219,7 +219,7 @@ - (BOOL)restorePreviousSignInNoRefresh { // Restore current user without refreshing the access token. OIDIDToken *idToken = [[OIDIDToken alloc] initWithIDTokenString:authState.lastTokenResponse.idToken]; - GIDProfileData *profileData = [[GIDProfileData alloc] initWithIDToken:idToken]; + GIDProfileData *profileData = [_profileDataFetcher fetchProfileDataWithIDToken:idToken]; GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:authState profileData:profileData]; self.currentUser = user; From 6b0456615020b6c7aec8a55582210dd50c1845dd Mon Sep 17 00:00:00 2001 From: pinlu Date: Tue, 10 Jan 2023 10:22:57 -0800 Subject: [PATCH 09/10] Improve style and documentation. --- GoogleSignIn/Sources/GIDProfileData.m | 3 ++- .../GIDProfileDataFetcher/API/GIDProfileDataFetcher.h | 11 +++++++---- .../Implementations/GIDProfileDataFetcher.m | 6 ++++-- .../Sources/Public/GoogleSignIn/GIDProfileData.h | 8 ++++---- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/GoogleSignIn/Sources/GIDProfileData.m b/GoogleSignIn/Sources/GIDProfileData.m index 53f1a89b..50183596 100644 --- a/GoogleSignIn/Sources/GIDProfileData.m +++ b/GoogleSignIn/Sources/GIDProfileData.m @@ -128,7 +128,8 @@ - (nullable instancetype)initWithCoder:(NSCoder *)decoder { } return [self initWithEmail:email - name:name givenName:givenName + name:name + givenName:givenName familyName:familyName imageURL:imageURL]; } diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h b/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h index f1e5d5a2..47201c0f 100644 --- a/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h @@ -26,8 +26,8 @@ NS_ASSUME_NONNULL_BEGIN /// Fetches the latest `GIDProfileData` object. /// -/// "This method either extracts profile data from `OIDIDToken` in `OIDAuthState` or fetches it -/// from the UserInfo endpoint." +/// This method either extracts profile data from `OIDIDToken` in `OIDAuthState` or fetches it +/// from the UserInfo endpoint. /// /// @param authState The state of the current OAuth session. /// @param completion The block that is called on completion asynchronously. @@ -35,10 +35,13 @@ NS_ASSUME_NONNULL_BEGIN completion:(void (^)(GIDProfileData *_Nullable profileData, NSError *_Nullable error))completion; -/// Fetches the latest `GIDProfileData` object. +/// Fetches a `GIDProfileData` object from an ID token. +/// +/// This method returns a `GIDProfileData` object if the ID token is a fat one. Otherwise, returns +/// nil. /// /// @param idToken The ID token. -- (nullable GIDProfileData*)fetchProfileDataWithIDToken:(OIDIDToken *)idToken; +- (nullable GIDProfileData *)fetchProfileDataWithIDToken:(OIDIDToken *)idToken; @end diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m index 7ae8a874..d9923020 100644 --- a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m @@ -66,8 +66,10 @@ - (void)fetchProfileDataWithAuthState:(OIDAuthState *)authState // If profile data is present in the ID token, use it. if (idToken) { GIDProfileData *profileData = [self fetchProfileDataWithIDToken:idToken]; - completion(profileData, nil); - return; + if (profileData) { + completion(profileData, nil); + return; + } } // If we can't retrieve profile data from the ID token, make a UserInfo endpoint request to diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h index b44c8961..8a362445 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h @@ -36,16 +36,16 @@ NS_ASSUME_NONNULL_BEGIN /// Whether or not the user has profile image. @property(nonatomic, readonly) BOOL hasImage; ++ (instancetype)new NS_UNAVAILABLE; + +- (instancetype)init NS_UNAVAILABLE; + /// Gets the user's profile image URL for the given dimension in pixels for each side of the square. /// /// @param dimension The desired height (and width) of the profile image. /// @return The URL of the user's profile image. - (nullable NSURL *)imageURLWithDimension:(NSUInteger)dimension; -- (instancetype)init NS_UNAVAILABLE; - -+ (instancetype)new NS_UNAVAILABLE; - @end NS_ASSUME_NONNULL_END From 20719d552898a19b6f596e4e941ccb7534c0ca9b Mon Sep 17 00:00:00 2001 From: pinlu Date: Tue, 10 Jan 2023 14:26:22 -0800 Subject: [PATCH 10/10] Add more unit tests --- .../Implementations/GIDProfileDataFetcher.m | 2 +- .../Tests/Unit/GIDProfileDataFetcherTest.m | 81 +++++++++++++------ 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m index d9923020..83ebbdc8 100644 --- a/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m @@ -63,8 +63,8 @@ - (void)fetchProfileDataWithAuthState:(OIDAuthState *)authState NSError *_Nullable error))completion { OIDIDToken *idToken = [[OIDIDToken alloc] initWithIDTokenString:authState.lastTokenResponse.idToken]; - // If profile data is present in the ID token, use it. if (idToken) { + // If we have an ID token, try to extract profile data from it. GIDProfileData *profileData = [self fetchProfileDataWithIDToken:idToken]; if (profileData) { completion(profileData, nil); diff --git a/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m b/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m index 17bd54ad..1f3ba9bd 100644 --- a/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m +++ b/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -39,9 +39,11 @@ - (void)setUp { _profileDataFetcher = [[GIDProfileDataFetcher alloc] initWithHTTPFetcher:_httpFetcher]; } -- (void)testFetchProfileData_outOfAuthState_success { +// Extracts the `GIDProfileData` out of a fat ID token. +- (void)testFetchProfileData_fatIDToken_success { + NSString *fatIDToken = [OIDTokenResponse fatIDToken]; OIDTokenResponse *tokenResponse = - [OIDTokenResponse testInstanceWithIDToken:[OIDTokenResponse fatIDToken] + [OIDTokenResponse testInstanceWithIDToken:fatIDToken accessToken:kAccessToken expiresIn:@(300) refreshToken:kRefreshToken @@ -71,7 +73,39 @@ - (void)testFetchProfileData_outOfAuthState_success { [self waitForExpectationsWithTimeout:1 handler:nil]; } -- (void)testFetchProfileData_fromInfoURL_fetchingError { +// Can not extracts the `GIDProfileData` out of a non-fat ID token. Try to fetch it from the +// UserInfo endpoint and get an error. +- (void)testFetchProfileData_nonFatIDToken_fetchingError { + NSString *nonFatIDToken = [OIDTokenResponse idToken]; + OIDTokenResponse *tokenResponse = + [OIDTokenResponse testInstanceWithIDToken:nonFatIDToken + accessToken:kAccessToken + expiresIn:@(300) + refreshToken:kRefreshToken + tokenRequest:nil]; + OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse]; + + NSError *fetchingError = [self error]; + [self setGIDHTTPFetcherTestBlockWithData:nil error:fetchingError]; + + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"completion is invoked"]; + + [_profileDataFetcher + fetchProfileDataWithAuthState:authState + completion:^(GIDProfileData *profileData, NSError *error) { + XCTAssertNil(profileData); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, kErrorDomain); + XCTAssertEqual(error.code, kErrorCode); + [completionExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +// There is no ID token. Try to fetch it from the UserInfo endpoint and get an error. +- (void)testFetchProfileData_noIDToken_fetchingError { OIDTokenResponse *tokenResponse = [OIDTokenResponse testInstanceWithIDToken:nil accessToken:kAccessToken @@ -80,15 +114,8 @@ - (void)testFetchProfileData_fromInfoURL_fetchingError { tokenRequest:nil]; OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse]; - XCTestExpectation *fetcherExpectation = - [self expectationWithDescription:@"_httpFetcher is invoked"]; - GIDHTTPFetcherTestBlock testBlock = - ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) { - NSError *fetchingError = [self error]; - responseProvider(nil, fetchingError); - [fetcherExpectation fulfill]; - }; - [_httpFetcher setTestBlock:testBlock]; + NSError *fetchingError = [self error]; + [self setGIDHTTPFetcherTestBlockWithData:nil error:fetchingError]; XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completion is invoked"]; @@ -106,8 +133,8 @@ - (void)testFetchProfileData_fromInfoURL_fetchingError { [self waitForExpectationsWithTimeout:1 handler:nil]; } -// Fetch the NSData successfully but can not parse it. -- (void)testFetchProfileData_fromInfoURL_parsingError { +// Fetch the NSData from the UserInfo endpoint successfully but can not parse it. +- (void)testFetchProfileData_noIDToken_parsingError { OIDTokenResponse *tokenResponse = [OIDTokenResponse testInstanceWithIDToken:nil accessToken:kAccessToken @@ -116,15 +143,8 @@ - (void)testFetchProfileData_fromInfoURL_parsingError { tokenRequest:nil]; OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse]; - XCTestExpectation *fetcherExpectation = - [self expectationWithDescription:@"_httpFetcher is invoked"]; - GIDHTTPFetcherTestBlock testBlock = - ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) { - NSData *data = [[NSData alloc] init]; - responseProvider(data, nil); - [fetcherExpectation fulfill]; - }; - [_httpFetcher setTestBlock:testBlock]; + NSData *data = [[NSData alloc] init]; + [self setGIDHTTPFetcherTestBlockWithData:data error:nil]; XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completion is invoked"]; @@ -147,4 +167,17 @@ - (NSError *)error { return [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:nil]; } +/// Set the return value from the UserInfo endpoint. +- (void)setGIDHTTPFetcherTestBlockWithData:(NSData *)data + error:(NSError *)error { + XCTestExpectation *fetcherExpectation = + [self expectationWithDescription:@"_httpFetcher is invoked"]; + GIDHTTPFetcherTestBlock testBlock = + ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) { + responseProvider(data, error); + [fetcherExpectation fulfill]; + }; + [_httpFetcher setTestBlock:testBlock]; +} + @end