瀏覽代碼

Add HttpFetcher component

Add class HttpFetcher fetches data from URL endpoint.
Add the FakeHttpFetcher.
Use HttpFetcher on GIDSignIn for revoking the grant scope.
henryhl22321 2 年之前
父節點
當前提交
3a396b6636

+ 41 - 0
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 <Foundation/Foundation.h>
+
+@protocol GTMSessionFetcherServiceProtocol;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol GIDHTTPFetcher <NSObject>
+
+/// 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<GTMSessionFetcherServiceProtocol>)fetcherService
+#pragma clang diagnostic pop
+            withComment:(NSString *)comment
+             completion:(void (^)(NSData *_Nullable, NSError *_Nullable))completion;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 46 - 0
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 <Foundation/Foundation.h>
+
+#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 <GIDHTTPFetcher>
+
+/// Set the test block which provides the response value.
+- (void)setTestBlock:(GIDHTTPFetcherTestBlock)block;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 23 - 0
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<GTMSessionFetcherServiceProtocol>)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

+ 26 - 0
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 <Foundation/Foundation.h>
+
+#import "GoogleSignIn/Sources/GIDHTTPFetcher/API/GIDHTTPFetcher.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface GIDHTTPFetcher : NSObject<GIDHTTPFetcher>
+@end
+
+NS_ASSUME_NONNULL_END

+ 37 - 0
GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.m

@@ -0,0 +1,37 @@
+#import "GoogleSignIn/Sources/GIDHTTPFetcher/Implementations/GIDHTTPFetcher.h"
+
+//#ifdef SWIFT_PACKAGE
+@import GTMAppAuth;
+//#else
+//#import <GTMAppAuth/GTMAppAuth.h>
+//#endif
+#import <GTMSessionFetcher/GTMSessionFetcher.h>
+
+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<GTMSessionFetcherServiceProtocol>)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

+ 42 - 31
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 @@ static NSString *const kUserCanceledError = @"The user canceled the sign-in flow
 // 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 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   OIDServiceConfiguration *_appAuthConfiguration;
   // AppAuth external user-agent session state.
   id<OIDExternalUserAgentSession> _currentAuthorizationFlow;
+  // The class to fetches data from a url end point.
+  id<GIDHTTPFetcher> _httpFetcher;
   // Flag to indicate that the auth flow is restarting.
   BOOL _restarting;
   // Keychain manager for GTMAppAuth
@@ -421,10 +422,13 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
                      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<GTMSessionFetcherServiceProtocol> 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 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 
 #pragma mark - Private methods
 
-- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore {
+- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore
+                          httpFetcher:(id<GIDHTTPFetcher>)httpFetcher {
   self = [super init];
   if (self) {
     // Get the bundle of the current executable.
@@ -477,6 +482,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
         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 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 - (instancetype)initPrivate {
   GTMKeychainStore *keychainStore =
       [[GTMKeychainStore alloc] initWithItemName:kGTMAppAuthKeychainName];
-  return [self initWithKeychainStore:keychainStore];
+  id<GIDHTTPFetcher> httpFetcher = [[GIDHTTPFetcher alloc] init];
+  return [self initWithKeychainStore:keychainStore
+                         httpFetcher:httpFetcher];
 }
 
 // Does sanity check for parameters and then authenticates if necessary.
@@ -823,10 +831,13 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
           [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<GTMSessionFetcherServiceProtocol> 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<NSString *, NSString *> *profileDict =
@@ -876,24 +887,24 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   }];
 }
 
-- (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<GTMSessionFetcherServiceProtocol> 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<GTMSessionFetcherServiceProtocol> 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 {

+ 4 - 1
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<GIDHTTPFetcher>)httpFetcher;
 
 /// Authenticates with extra options.
 - (void)signInWithOptions:(GIDSignInInternalOptions *)options;

+ 106 - 0
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 <XCTest/XCTest.h>
+
+#ifdef SWIFT_PACKAGE
+@import GTMAppAuth;
+#else
+#import <GTMAppAuth/GTMAppAuth.h>
+#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<GTMSessionFetcherServiceProtocol> 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<GTMSessionFetcherServiceProtocol> 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

+ 78 - 98
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 @@ static NSString *const kNewScope = @"newScope";
   // 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 @@ static NSString *const kNewScope = @"newScope";
   // 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 @@ static NSString *const kNewScope = @"newScope";
                  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 @@ static NSString *const kNewScope = @"newScope";
   [[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 @@ static NSString *const kNewScope = @"newScope";
 #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 @@ static NSString *const kNewScope = @"newScope";
   [[[_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 @@ static NSString *const kNewScope = @"newScope";
   [[[_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 @@ static NSString *const kNewScope = @"newScope";
   [[[_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 @@ static NSString *const kNewScope = @"newScope";
   [[[_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 @@ static NSString *const kNewScope = @"newScope";
 
 #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<XCTestExpectation *> *)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 @@ static NSString *const kNewScope = @"newScope";
                         @"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