From 3a396b66367127006f10a14c16ceacb845ec0a94 Mon Sep 17 00:00:00 2001 From: henryhl22321 <130406709+henryhl22321@users.noreply.github.com> Date: Mon, 17 Jul 2023 10:59:40 -0700 Subject: [PATCH] Add HttpFetcher component Add class HttpFetcher fetches data from URL endpoint. Add the FakeHttpFetcher. Use HttpFetcher on GIDSignIn for revoking the grant scope. --- .../GIDHTTPFetcher/API/GIDHttpFetcher.h | 41 ++++ .../Fakes/GIDFakeHTTPFetcher.h | 46 +++++ .../Fakes/GIDFakeHTTPFetcher.m | 23 +++ .../Implementations/GIDHTTPFetcher.h | 26 +++ .../Implementations/GIDHTTPFetcher.m | 37 ++++ GoogleSignIn/Sources/GIDSignIn.m | 73 +++++--- GoogleSignIn/Sources/GIDSignIn_Private.h | 5 +- GoogleSignIn/Tests/Unit/GIDHTTPFetcherTest.m | 106 +++++++++++ GoogleSignIn/Tests/Unit/GIDSignInTest.m | 176 ++++++++---------- 9 files changed, 403 insertions(+), 130 deletions(-) create mode 100644 GoogleSignIn/Sources/GIDHTTPFetcher/API/GIDHttpFetcher.h create mode 100644 GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/Fakes/GIDFakeHTTPFetcher.h create mode 100644 GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/Fakes/GIDFakeHTTPFetcher.m create mode 100644 GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.h create mode 100644 GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.m create mode 100644 GoogleSignIn/Tests/Unit/GIDHTTPFetcherTest.m diff --git a/GoogleSignIn/Sources/GIDHTTPFetcher/API/GIDHttpFetcher.h b/GoogleSignIn/Sources/GIDHTTPFetcher/API/GIDHttpFetcher.h new file mode 100644 index 00000000..35e9136f --- /dev/null +++ b/GoogleSignIn/Sources/GIDHTTPFetcher/API/GIDHttpFetcher.h @@ -0,0 +1,41 @@ +/* + * 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 + +@protocol GTMSessionFetcherServiceProtocol; + +NS_ASSUME_NONNULL_BEGIN + +@protocol GIDHTTPFetcher + +/// Fetches the data from an URL request. +/// +/// @param urlRequest The url request to fetch data. +/// @param authorizer The object to add authorization to the request. +/// @param comment The comment for logging purpose. +/// @param completion The block that is called on completion asynchronously. +- (void)fetchURLRequest:(NSURLRequest *)urlRequest +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + withFetcherService:(id)fetcherService +#pragma clang diagnostic pop + withComment:(NSString *)comment + completion:(void (^)(NSData *_Nullable, NSError *_Nullable))completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/Fakes/GIDFakeHTTPFetcher.h b/GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/Fakes/GIDFakeHTTPFetcher.h new file mode 100644 index 00000000..beda1c36 --- /dev/null +++ b/GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/Fakes/GIDFakeHTTPFetcher.h @@ -0,0 +1,46 @@ +/* + * 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/GIDHTTPFetcher/API/GIDHTTPFetcher.h" + +NS_ASSUME_NONNULL_BEGIN + +/// The block which provides the response for the method +/// fetchURLRequest:withAuthorizer:withComment:completion:`. +/// +/// @param data The NSData returned if succeed, +/// @param error The error returned if failed. +typedef void(^GIDHTTPFetcherFakeResponseProviderBlock)(NSData *_Nullable data, + NSError *_Nullable error); + +/// The block to set up data based on the input request for the method +/// fetchURLRequest:withAuthorizer:withComment:completion:`. +/// +/// @param request The request from input. +/// @param responseProvider The block which provides the response. +typedef void (^GIDHTTPFetcherTestBlock)(NSURLRequest *request, + GIDHTTPFetcherFakeResponseProviderBlock responseProvider); + +@interface GIDFakeHTTPFetcher : NSObject + +/// Set the test block which provides the response value. +- (void)setTestBlock:(GIDHTTPFetcherTestBlock)block; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/Fakes/GIDFakeHTTPFetcher.m b/GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/Fakes/GIDFakeHTTPFetcher.m new file mode 100644 index 00000000..91583c8d --- /dev/null +++ b/GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/Fakes/GIDFakeHTTPFetcher.m @@ -0,0 +1,23 @@ +#import "GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/Fakes/GIDFakeHTTPFetcher.h" + +@interface GIDFakeHTTPFetcher () + +@property(nonatomic) GIDHTTPFetcherTestBlock testBlock; + +@end + +@implementation GIDFakeHTTPFetcher + +- (void)fetchURLRequest:(NSURLRequest *)urlRequest +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + withFetcherService:(id)fetcherService +#pragma clang diagnostic pop + withComment:(NSString *)comment + completion:(void (^)(NSData *_Nullable, NSError *_Nullable))completion { + NSAssert(self.testBlock != nil, @"Set the test block before invoking this method."); + self.testBlock(urlRequest, ^(NSData *_Nullable data, NSError *_Nullable error) { + completion(data, error); + }); +} + +@end diff --git a/GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.h b/GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.h new file mode 100644 index 00000000..72fcc993 --- /dev/null +++ b/GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.h @@ -0,0 +1,26 @@ +/* + * 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/GIDHTTPFetcher/API/GIDHTTPFetcher.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface GIDHTTPFetcher : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.m b/GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.m new file mode 100644 index 00000000..4df439fc --- /dev/null +++ b/GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.m @@ -0,0 +1,37 @@ +#import "GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.h" + +//#ifdef SWIFT_PACKAGE +@import GTMAppAuth; +//#else +//#import +//#endif +#import + +NS_ASSUME_NONNULL_BEGIN + +// Maximum retry interval in seconds for the fetcher. +static const NSTimeInterval kFetcherMaxRetryInterval = 15.0; + +@implementation GIDHTTPFetcher + +- (void)fetchURLRequest:(NSURLRequest *)urlRequest +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + withFetcherService:(id)fetcherService +#pragma clang diagnostic pop + withComment:(NSString *)comment + completion:(void (^)(NSData *_Nullable, NSError *_Nullable))completion { + GTMSessionFetcher *fetcher; + if (fetcherService) { + fetcher = [fetcherService fetcherWithRequest:urlRequest]; + } else { + fetcher = [GTMSessionFetcher fetcherWithRequest:urlRequest]; + } + fetcher.retryEnabled = YES; + fetcher.maxRetryInterval = kFetcherMaxRetryInterval; + fetcher.comment = comment; + [fetcher beginFetchWithCompletionHandler:completion]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index 4b942ed9..cb44e176 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -21,6 +21,8 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInResult.h" +#import "GoogleSignIn/Sources/GIDHTTPFetcher/API/GIDHTTPFetcher.h" +#import "GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.h" #import "GoogleSignIn/Sources/GIDEMMSupport.h" #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h" #import "GoogleSignIn/Sources/GIDSignInPreferences.h" @@ -118,9 +120,6 @@ // User preference key to detect fresh install of the app. static NSString *const kAppHasRunBeforeKey = @"GID_AppHasRunBefore"; -// Maximum retry interval in seconds for the fetcher. -static const NSTimeInterval kFetcherMaxRetryInterval = 15.0; - // The delay before the new sign-in flow can be presented after the existing one is cancelled. static const NSTimeInterval kPresentationDelayAfterCancel = 1.0; @@ -163,6 +162,8 @@ @implementation GIDSignIn { OIDServiceConfiguration *_appAuthConfiguration; // AppAuth external user-agent session state. id _currentAuthorizationFlow; + // The class to fetches data from a url end point. + id _httpFetcher; // Flag to indicate that the auth flow is restarting. BOOL _restarting; // Keychain manager for GTMAppAuth @@ -421,10 +422,13 @@ - (void)disconnectWithCompletion:(nullable GIDDisconnectCompletion)completion { kEnvironmentLoggingParameter, GIDEnvironment()]; NSURL *revokeURL = [NSURL URLWithString:revokeURLString]; - [self startFetchURL:revokeURL - fromAuthState:authState - withComment:@"GIDSignIn: revoke tokens" - withCompletionHandler:^(NSData *data, NSError *error) { + NSMutableURLRequest *revokeRequest = [NSMutableURLRequest requestWithURL:revokeURL]; + GTMAuthSession *authorization = [[GTMAuthSession alloc] initWithAuthState:authState]; + id fetcherService = authorization.fetcherService; + [self->_httpFetcher fetchURLRequest:revokeRequest + withFetcherService:fetcherService + withComment:@"GIDSignIn: revoke tokens" + completion:^(NSData *data, NSError *error) { // Revoking an already revoked token seems always successful, which helps us here. if (!error) { [self signOut]; @@ -450,7 +454,8 @@ + (GIDSignIn *)sharedInstance { #pragma mark - Private methods -- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore { +- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore + httpFetcher:(id)httpFetcher { self = [super init]; if (self) { // Get the bundle of the current executable. @@ -477,6 +482,7 @@ - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore { initWithAuthorizationEndpoint:[NSURL URLWithString:authorizationEnpointURL] tokenEndpoint:[NSURL URLWithString:tokenEndpointURL]]; _keychainStore = keychainStore; + _httpFetcher = httpFetcher; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST // Perform migration of auth state from old (before 5.0) versions of the SDK if needed. @@ -494,7 +500,9 @@ - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore { - (instancetype)initPrivate { GTMKeychainStore *keychainStore = [[GTMKeychainStore alloc] initWithItemName:kGTMAppAuthKeychainName]; - return [self initWithKeychainStore:keychainStore]; + id httpFetcher = [[GIDHTTPFetcher alloc] init]; + return [self initWithKeychainStore:keychainStore + httpFetcher:httpFetcher]; } // Does sanity check for parameters and then authenticates if necessary. @@ -823,10 +831,13 @@ - (void)addDecodeIdTokenCallback:(GIDAuthFlow *)authFlow { [NSString stringWithFormat:kUserInfoURLTemplate, [GIDSignInPreferences googleUserInfoServer], authState.lastTokenResponse.accessToken]]; - [self startFetchURL:infoURL - fromAuthState:authState - withComment:@"GIDSignIn: fetch basic profile info" - withCompletionHandler:^(NSData *data, NSError *error) { + NSMutableURLRequest *infoRequest = [NSMutableURLRequest requestWithURL:infoURL]; + GTMAuthSession *authorization = [[GTMAuthSession alloc] initWithAuthState:authState]; + id fetcherService = authorization.fetcherService; + [self->_httpFetcher fetchURLRequest:infoRequest + withFetcherService:fetcherService + withComment:@"GIDSignIn: fetch basic profile info" + completion:^(NSData *data, NSError *error) { if (data && !error) { NSError *jsonDeserializationError; NSDictionary *profileDict = @@ -876,24 +887,24 @@ - (void)addCompletionCallback:(GIDAuthFlow *)authFlow { }]; } -- (void)startFetchURL:(NSURL *)URL - fromAuthState:(OIDAuthState *)authState - withComment:(NSString *)comment - withCompletionHandler:(void (^)(NSData *, NSError *))handler { - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; - GTMSessionFetcher *fetcher; - GTMAuthSession *authorization = [[GTMAuthSession alloc] initWithAuthState:authState]; - id fetcherService = authorization.fetcherService; - if (fetcherService) { - fetcher = [fetcherService fetcherWithRequest:request]; - } else { - fetcher = [GTMSessionFetcher fetcherWithRequest:request]; - } - fetcher.retryEnabled = YES; - fetcher.maxRetryInterval = kFetcherMaxRetryInterval; - fetcher.comment = comment; - [fetcher beginFetchWithCompletionHandler:handler]; -} +//- (void)startFetchURL:(NSURL *)URL +// fromAuthState:(OIDAuthState *)authState +// withComment:(NSString *)comment +// withCompletionHandler:(void (^)(NSData *, NSError *))handler { +// NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; +// GTMSessionFetcher *fetcher; +// GTMAuthSession *authorization = [[GTMAuthSession alloc] initWithAuthState:authState]; +// id fetcherService = authorization.fetcherService; +// if (fetcherService) { +// fetcher = [fetcherService fetcherWithRequest:request]; +// } else { +// fetcher = [GTMSessionFetcher fetcherWithRequest:request]; +// } +// fetcher.retryEnabled = YES; +// fetcher.maxRetryInterval = kFetcherMaxRetryInterval; +// fetcher.comment = comment; +// [fetcher beginFetchWithCompletionHandler:handler]; +//} // Parse incoming URL from the Google Device Policy app. - (BOOL)handleDevicePolicyAppURL:(NSURL *)url { diff --git a/GoogleSignIn/Sources/GIDSignIn_Private.h b/GoogleSignIn/Sources/GIDSignIn_Private.h index 2fb71ae4..4a9f3106 100644 --- a/GoogleSignIn/Sources/GIDSignIn_Private.h +++ b/GoogleSignIn/Sources/GIDSignIn_Private.h @@ -30,6 +30,8 @@ NS_ASSUME_NONNULL_BEGIN @class GIDSignInInternalOptions; @class GTMKeychainStore; +@protocol GIDHTTPFetcher; + /// Represents a completion block that takes a `GIDSignInResult` on success or an error if the /// operation was unsuccessful. typedef void (^GIDSignInCompletion)(GIDSignInResult *_Nullable signInResult, @@ -48,7 +50,8 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error); - (instancetype)initPrivate; /// Private initializer taking a `GTMKeychainStore` to use during tests. -- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore; +- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore + httpFetcher:(id)httpFetcher; /// Authenticates with extra options. - (void)signInWithOptions:(GIDSignInInternalOptions *)options; diff --git a/GoogleSignIn/Tests/Unit/GIDHTTPFetcherTest.m b/GoogleSignIn/Tests/Unit/GIDHTTPFetcherTest.m new file mode 100644 index 00000000..1933fd77 --- /dev/null +++ b/GoogleSignIn/Tests/Unit/GIDHTTPFetcherTest.m @@ -0,0 +1,106 @@ +// 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 "GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.h" + +#import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h" + +#import + +#ifdef SWIFT_PACKAGE +@import GTMAppAuth; +#else +#import +#endif + +static NSString *const kTestURL = @"https://testURL.com"; +static NSString *const kErrorDomain = @"ERROR_DOMAIN"; +static NSInteger const kErrorCode = 400; + +@interface GIDHTTPFetcherTest : XCTestCase { + GIDHTTPFetcher *_httpFetcher; +} + +@end + +@implementation GIDHTTPFetcherTest + +- (void)setUp { + [super setUp]; + _httpFetcher = [[GIDHTTPFetcher alloc] init]; +} + +- (void)testFetchData_success { + NSURL *url = [NSURL URLWithString:kTestURL]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + OIDAuthState *authState = [OIDAuthState testInstance]; + GTMAuthSession *authorization = [[GTMAuthSession alloc] initWithAuthState:authState]; + id fetcherService = authorization.fetcherService; + + GTMSessionFetcherTestBlock block = + ^(GTMSessionFetcher *fetcherToTest, GTMSessionFetcherTestResponse testResponse) { + NSData *data = [[NSData alloc] init]; + testResponse(nil, data, nil); + }; + [GTMSessionFetcher setGlobalTestBlock:block]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Callback called with no error"]; + + [_httpFetcher fetchURLRequest:request + withFetcherService:fetcherService + withComment:@"Test data fetcher." + completion:^(NSData *data, NSError *error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testFetchData_error { + NSURL *url = [NSURL URLWithString:kTestURL]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + OIDAuthState *authState = [OIDAuthState testInstance]; + GTMAuthSession *authorization = [[GTMAuthSession alloc] initWithAuthState:authState]; + id fetcherService = authorization.fetcherService; + + GTMSessionFetcherTestBlock block = + ^(GTMSessionFetcher *fetcherToTest, GTMSessionFetcherTestResponse testResponse) { + NSData *data = [[NSData alloc] init]; + NSError *error = [self error]; + testResponse(nil, data, error); + }; + [GTMSessionFetcher setGlobalTestBlock:block]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Callback called with an error"]; + + [_httpFetcher fetchURLRequest:request + withFetcherService:fetcherService + withComment:@"Test data fetcher." + completion:^(NSData *data, NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqual(error.code, kErrorCode); + [expectation 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 c64f1611..c197a72a 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -32,13 +32,13 @@ #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h" #import "GoogleSignIn/Sources/GIDSignIn_Private.h" #import "GoogleSignIn/Sources/GIDSignInPreferences.h" +#import "GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/Fakes/GIDFakeHTTPFetcher.h" +#import "GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.h" #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST -#import "GoogleSignIn/Tests/Unit/GIDFakeFetcher.h" -#import "GoogleSignIn/Tests/Unit/GIDFakeFetcherService.h" #import "GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h" #import "GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.h" #import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h" @@ -195,6 +195,12 @@ @interface GIDSignInTest : XCTestCase { // Mock |GTMKeychainStore|. id _keychainStore; + // Mock |GTMSessionFetcherServiceProtocol|. + id _fetcherService; + + // Fake for |GIDHTTPFetcher|. + GIDFakeHTTPFetcher *_httpFetcher; + #if TARGET_OS_IOS || TARGET_OS_MACCATALYST // Mock |UIViewController|. id _presentingViewController; @@ -215,9 +221,6 @@ @interface GIDSignInTest : XCTestCase { // Whether callback block has been called. BOOL _completionCalled; - // Fake fetcher service to emulate network requests. - GIDFakeFetcherService *_fetcherService; - // Fake [NSBundle mainBundle]; GIDFakeMainBundle *_fakeMainBundle; @@ -319,7 +322,6 @@ - (void)setUp { callback:COPY_TO_ARG_BLOCK(self->_savedTokenCallback)]); // Fakes - _fetcherService = [[GIDFakeFetcherService alloc] init]; _fakeMainBundle = [[GIDFakeMainBundle alloc] init]; [_fakeMainBundle startFakingWithClientID:kClientId]; [_fakeMainBundle fakeAllSchemesSupported]; @@ -328,9 +330,14 @@ - (void)setUp { [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kAppHasRunBeforeKey]; - _signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore]; + _httpFetcher = [[GIDFakeHTTPFetcher alloc] init]; + + _signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore + httpFetcher:_httpFetcher]; _hint = nil; + _fetcherService = nil; + __weak GIDSignInTest *weakSelf = self; _completion = ^(GIDSignInResult *_Nullable signInResult, NSError * _Nullable error) { GIDSignInTest *strongSelf = weakSelf; @@ -869,60 +876,58 @@ - (void)testNotHandleWrongPath { #pragma mark - Tests - disconnectWithCallback: // Verifies disconnect calls callback with no errors if access token is present. -- (void)testDisconnect_accessToken { +- (void)testDisconnect_accessTokenIsPresent { [[[_authorization expect] andReturn:_authState] authState]; [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; [[[_tokenResponse expect] andReturn:kAccessToken] accessToken]; - [[[_authorization expect] andReturn:_fetcherService] fetcherService]; + OCMStub([_authorization fetcherService]).andReturn(_fetcherService); + NSData *data = [[NSData alloc] init]; + [self didFetch:data withToken:kAccessToken error:nil]; + XCTestExpectation *accessTokenExpectation = [self expectationWithDescription:@"Callback called with nil error"]; [_signIn disconnectWithCompletion:^(NSError * _Nullable error) { - if (error == nil) { - [accessTokenExpectation fulfill]; - } + XCTAssertNil(error); + [accessTokenExpectation fulfill]; }]; - [self verifyAndRevokeToken:kAccessToken - hasCallback:YES - waitingForExpectations:@[accessTokenExpectation]]; - [_authorization verify]; - [_authState verify]; - [_tokenResponse verify]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(_keychainRemoved, @"should clear saved keychain name"); } // Verifies disconnect if access token is present. -- (void)testDisconnectNoCallback_accessToken { +- (void)testDisconnectNoCallback_accessTokenIsPresent { [[[_authorization expect] andReturn:_authState] authState]; [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; [[[_tokenResponse expect] andReturn:kAccessToken] accessToken]; - [[[_authorization expect] andReturn:_fetcherService] fetcherService]; + OCMStub([_authorization fetcherService]).andReturn(_fetcherService); + NSData *data = [[NSData alloc] init]; + [self didFetch:data withToken:kAccessToken error:nil]; + [_signIn disconnectWithCompletion:nil]; - [self verifyAndRevokeToken:kAccessToken hasCallback:NO waitingForExpectations:@[]]; - [_authorization verify]; - [_authState verify]; - [_tokenResponse verify]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(_keychainRemoved, @"should clear saved keychain name"); } // Verifies disconnect calls callback with no errors if refresh token is present. -- (void)testDisconnect_refreshToken { +- (void)testDisconnect_refreshTokenIsPresent { [[[_authorization expect] andReturn:_authState] authState]; [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; [[[_tokenResponse expect] andReturn:nil] accessToken]; [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; [[[_tokenResponse expect] andReturn:kRefreshToken] refreshToken]; - [[[_authorization expect] andReturn:_fetcherService] fetcherService]; + OCMStub([_authorization fetcherService]).andReturn(_fetcherService); + NSData *data = [[NSData alloc] init]; + [self didFetch:data withToken:kRefreshToken error:nil]; + XCTestExpectation *refreshTokenExpectation = [self expectationWithDescription:@"Callback called with nil error"]; [_signIn disconnectWithCompletion:^(NSError * _Nullable error) { - if (error == nil) { - [refreshTokenExpectation fulfill]; - } + XCTAssertNil(error); + [refreshTokenExpectation fulfill]; }]; - [self verifyAndRevokeToken:kRefreshToken - hasCallback:YES - waitingForExpectations:@[refreshTokenExpectation]]; - [_authorization verify]; - [_authState verify]; + [self waitForExpectationsWithTimeout:1 handler:nil]; [_tokenResponse verify]; + XCTAssertTrue(_keychainRemoved, @"should clear saved keychain name"); } // Verifies disconnect errors are passed along to the callback. @@ -930,22 +935,17 @@ - (void)testDisconnect_errors { [[[_authorization expect] andReturn:_authState] authState]; [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; [[[_tokenResponse expect] andReturn:kAccessToken] accessToken]; - [[[_authorization expect] andReturn:_fetcherService] fetcherService]; + OCMStub([_authorization fetcherService]).andReturn(_fetcherService); + NSError *error = [self error]; + [self didFetch:nil withToken:kAccessToken error:error]; + XCTestExpectation *errorExpectation = [self expectationWithDescription:@"Callback called with an error"]; [_signIn disconnectWithCompletion:^(NSError * _Nullable error) { - if (error != nil) { - [errorExpectation fulfill]; - } + XCTAssertNotNil(error); + [errorExpectation fulfill]; }]; - XCTAssertTrue([self isFetcherStarted], @"should start fetching"); - // Emulate result back from server. - NSError *error = [self error]; - [self didFetch:nil error:error]; - [self waitForExpectations:@[errorExpectation] timeout:1]; - [_authorization verify]; - [_authState verify]; - [_tokenResponse verify]; + [self waitForExpectationsWithTimeout:1 handler:nil]; } // Verifies disconnect with errors @@ -953,15 +953,12 @@ - (void)testDisconnectNoCallback_errors { [[[_authorization expect] andReturn:_authState] authState]; [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; [[[_tokenResponse expect] andReturn:kAccessToken] accessToken]; - [[[_authorization expect] andReturn:_fetcherService] fetcherService]; - [_signIn disconnectWithCompletion:nil]; - XCTAssertTrue([self isFetcherStarted], @"should start fetching"); - // Emulate result back from server. + OCMStub([_authorization fetcherService]).andReturn(_fetcherService); NSError *error = [self error]; - [self didFetch:nil error:error]; - [_authorization verify]; - [_authState verify]; - [_tokenResponse verify]; + [self didFetch:nil withToken:kAccessToken error:error]; + + [_signIn disconnectWithCompletion:nil]; + [self waitForExpectationsWithTimeout:1 handler:nil]; } @@ -972,19 +969,16 @@ - (void)testDisconnect_noTokens { [[[_tokenResponse expect] andReturn:nil] accessToken]; [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; [[[_tokenResponse expect] andReturn:nil] refreshToken]; + [self notDoFetch]; XCTestExpectation *noTokensExpectation = [self expectationWithDescription:@"Callback called with nil error"]; [_signIn disconnectWithCompletion:^(NSError * _Nullable error) { - if (error == nil) { - [noTokensExpectation fulfill]; - } + XCTAssertNil(error); + [noTokensExpectation fulfill]; + }]; [self waitForExpectations:@[noTokensExpectation] timeout:1]; - XCTAssertFalse([self isFetcherStarted], @"should not fetch"); XCTAssertTrue(_keychainRemoved, @"keychain should be removed"); - [_authorization verify]; - [_authState verify]; - [_tokenResponse verify]; } // Verifies disconnect clears keychain if no tokens are present. @@ -994,12 +988,10 @@ - (void)testDisconnectNoCallback_noTokens { [[[_tokenResponse expect] andReturn:nil] accessToken]; [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; [[[_tokenResponse expect] andReturn:nil] refreshToken]; + [self notDoFetch]; + [_signIn disconnectWithCompletion:nil]; - XCTAssertFalse([self isFetcherStarted], @"should not fetch"); XCTAssertTrue(_keychainRemoved, @"keychain should be removed"); - [_authorization verify]; - [_authState verify]; - [_tokenResponse verify]; } - (void)testPresentingViewControllerException { @@ -1230,41 +1222,35 @@ - (void)testTokenEndpointEMMError { #pragma mark - Helpers -// Whether or not a fetcher has been started. -- (BOOL)isFetcherStarted { - NSUInteger count = _fetcherService.fetchers.count; - XCTAssertTrue(count <= 1, @"Only one fetcher is supported"); - return !!count; -} - -// Gets the URL being fetched. -- (NSURL *)fetchedURL { - return [_fetcherService.fetchers[0] requestURL]; +- (void)didFetch:(NSData *) data + withToken:(NSString *) token + error:(NSError *) error { + XCTestExpectation *fetcherExpectation = + [self expectationWithDescription:@"testBlock is invoked."]; + GIDHTTPFetcherTestBlock testBlock = + ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) { + [self verifyRevokeRequest:request withToken:token]; + responseProvider(data, error); + [fetcherExpectation fulfill]; + }; + [_httpFetcher setTestBlock:testBlock]; } -// Emulates server returning the data as in JSON. -- (void)didFetch:(id)dataObject error:(NSError *)error { - NSData *data = nil; - if (dataObject) { - NSError *jsonError = nil; - data = [NSJSONSerialization dataWithJSONObject:dataObject - options:0 - error:&jsonError]; - XCTAssertNil(jsonError, @"must provide valid data"); - } - [_fetcherService.fetchers[0] didFinishWithData:data error:error]; +- (void)notDoFetch { + GIDHTTPFetcherTestBlock testBlock = + ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) { + XCTFail(@"_httpFetcher should not be invoked."); + }; + [_httpFetcher setTestBlock:testBlock]; } - (NSError *)error { return [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:nil]; } -// Verifies a fetcher has started for revoking token and emulates a server response. -- (void)verifyAndRevokeToken:(NSString *)token - hasCallback:(BOOL)hasCallback - waitingForExpectations:(NSArray *)expectations { - XCTAssertTrue([self isFetcherStarted], @"should start fetching"); - NSURL *url = [self fetchedURL]; +- (void)verifyRevokeRequest:(NSURLRequest *)request + withToken:(NSString *)token { + NSURL *url = request.URL; XCTAssertEqualObjects([url scheme], @"https", @"scheme must match"); XCTAssertEqualObjects([url host], @"accounts.google.com", @"host must match"); XCTAssertEqualObjects([url path], @"/o/oauth2/revoke", @"path must match"); @@ -1276,12 +1262,6 @@ - (void)verifyAndRevokeToken:(NSString *)token @"SDK version logging parameter should match"); XCTAssertEqualObjects([params valueForKey:kEnvironmentLoggingParameter], GIDEnvironment(), @"Environment logging parameter should match"); - // Emulate result back from server. - [self didFetch:nil error:nil]; - XCTAssertTrue(_keychainRemoved, @"should clear saved keychain name"); - if (hasCallback) { - [self waitForExpectations:expectations timeout:1]; - } } - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow