diff --git a/GoogleSignIn/Sources/GIDProfileData.m b/GoogleSignIn/Sources/GIDProfileData.m index 09210836..50183596 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. @@ -109,21 +115,23 @@ + (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/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h b/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h new file mode 100644 index 00000000..47201c0f --- /dev/null +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h @@ -0,0 +1,48 @@ +/* + * 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 + +@class OIDAuthState; +@class OIDIDToken; +@class GIDProfileData; + +NS_ASSUME_NONNULL_BEGIN + +@protocol GIDProfileDataFetcher + +/// Fetches the latest `GIDProfileData` object. +/// +/// 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. +- (void)fetchProfileDataWithAuthState:(OIDAuthState *)authState + completion:(void (^)(GIDProfileData *_Nullable profileData, + NSError *_Nullable error))completion; + +/// 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; + +@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..03d48d1f --- /dev/null +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.h @@ -0,0 +1,35 @@ +/* + * 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 + +#import "GoogleSignIn/Sources/GIDProfileDataFetcher/API/GIDProfileDataFetcher.h" + +@protocol GIDHTTPFetcher; + +NS_ASSUME_NONNULL_BEGIN + +@interface GIDProfileDataFetcher : NSObject + +/// The convenience initializer. +- (instancetype)init; + +/// The initializer for unit test. +- (instancetype)initWithHTTPFetcher:(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..83ebbdc8 --- /dev/null +++ b/GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/GIDProfileDataFetcher.m @@ -0,0 +1,130 @@ +/* + * 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" +#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"; + +// 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; +} + +- (instancetype)init { + GIDHTTPFetcher *httpFetcher = [[GIDHTTPFetcher alloc] init]; + return [self initWithHTTPFetcher:httpFetcher]; +} + +- (instancetype)initWithHTTPFetcher:(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 (idToken) { + // If we have an ID token, try to extract profile data from it. + GIDProfileData *profileData = [self fetchProfileDataWithIDToken:idToken]; + if (profileData) { + completion(profileData, nil); + return; + } + } + + // 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]; + 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); + return; + } + NSError *jsonDeserializationError; + NSDictionary *profileDict = + [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&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); + }]; +} + +- (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 726df9c2..e85f196d 100644 --- a/GoogleSignIn/Sources/GIDProfileData_Private.h +++ b/GoogleSignIn/Sources/GIDProfileData_Private.h @@ -26,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN name:(NSString *)name givenName:(nullable NSString *)givenName familyName:(nullable NSString *)familyName - imageURL:(nullable NSURL *)imageURL; + imageURL:(nullable NSURL *)imageURL NS_DESIGNATED_INITIALIZER; @end diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index f450ee66..d1c0e9fc 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. @@ -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" @@ -74,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"; @@ -95,13 +94,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"; @@ -161,8 +153,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; } @@ -223,7 +219,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 = [_profileDataFetcher fetchProfileDataWithIDToken:idToken]; GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:authState profileData:profileData]; self.currentUser = user; @@ -455,12 +451,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. @@ -490,6 +489,7 @@ - (instancetype)initWithKeychainHandler:(id)keychainHandler _keychainHandler = keychainHandler; _httpFetcher = httpFetcher; + _profileDataFetcher = profileDataFetcher; } return self; } @@ -812,48 +812,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]; + }]; }]; } @@ -972,24 +944,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/Sources/Public/GoogleSignIn/GIDProfileData.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h index bf797362..8a362445 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h @@ -36,6 +36,10 @@ 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. diff --git a/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m b/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m new file mode 100644 index 00000000..1f3ba9bd --- /dev/null +++ b/GoogleSignIn/Tests/Unit/GIDProfileDataFetcherTest.m @@ -0,0 +1,183 @@ +// 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 + +// 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] initWithHTTPFetcher:_httpFetcher]; +} + +// Extracts the `GIDProfileData` out of a fat ID token. +- (void)testFetchProfileData_fatIDToken_success { + NSString *fatIDToken = [OIDTokenResponse fatIDToken]; + OIDTokenResponse *tokenResponse = + [OIDTokenResponse testInstanceWithIDToken: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:@"completion is invoked"]; + + [_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]; +} + +// 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 + 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]; +} + +// Fetch the NSData from the UserInfo endpoint successfully but can not parse it. +- (void)testFetchProfileData_noIDToken_parsingError { + OIDTokenResponse *tokenResponse = + [OIDTokenResponse testInstanceWithIDToken:nil + accessToken:kAccessToken + expiresIn:@(300) + refreshToken:kRefreshToken + tokenRequest:nil]; + OIDAuthState *authState = [OIDAuthState testInstanceWithTokenResponse:tokenResponse]; + + NSData *data = [[NSData alloc] init]; + [self setGIDHTTPFetcherTestBlockWithData:data error:nil]; + + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"completion is invoked"]; + + [_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]; +} + +/// 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 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;