// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #import #if TARGET_OS_IOS || TARGET_OS_MACCATALYST #import #elif TARGET_OS_OSX #import #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST #import #import // Test module imports @import GoogleSignIn; #import "GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/API/GIDAuthorizationFlowProcessor.h" #import "GoogleSignIn/Sources/GIDAuthorizationFlowProcessor/Implementations/Fakes/GIDFakeAuthorizationFlowProcessor.h" #import "GoogleSignIn/Sources/GIDEMMSupport.h" #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h" #import "GoogleSignIn/Sources/GIDSignIn_Private.h" #import "GoogleSignIn/Sources/GIDSignInPreferences.h" #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/API/GIDProfileDataFetcher.h" #import "GoogleSignIn/Sources/GIDProfileDataFetcher/Implementations/Fakes/GIDFakeProfileDataFetcher.h" #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST #import "GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h" #import "GoogleSignIn/Tests/Unit/GIDProfileData+Testing.h" #import "GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.h" #import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h" #ifdef SWIFT_PACKAGE @import AppAuth; @import GTMAppAuth; @import GTMSessionFetcherCore; @import OCMock; #else #import #import #import #import #import #import #import #import #import #if TARGET_OS_IOS || TARGET_OS_MACCATALYST #import #elif TARGET_OS_OSX #import #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST #import #import #import #import #endif // Create a BLOCK to store the actual address for arg in param. #define SAVE_TO_ARG_BLOCK(param) [OCMArg checkWithBlock:^(id arg) {\ param = arg;\ return YES;\ }] #define COPY_TO_ARG_BLOCK(param) [OCMArg checkWithBlock:^(id arg) {\ param = [arg copy];\ return YES;\ }] static NSString * const kFakeGaiaID = @"123456789"; static NSString * const kFakeIDToken = @"FakeIDToken"; static NSString * const kClientId = @"FakeClientID"; static NSString * const kDotReversedClientId = @"FakeClientID"; static NSString * const kClientId2 = @"FakeClientID2"; static NSString * const kServerClientId = @"FakeServerClientID"; static NSString * const kLanguage = @"FakeLanguage"; static NSString * const kAuthCode = @"FakeAuthCode"; static NSString * const kKeychainName = @"auth"; static NSString * const kUserEmail = @"FakeUserEmail"; static NSString * const kVerifier = @"FakeVerifier"; static NSString * const kOpenIDRealm = @"FakeRealm"; static NSString * const kFakeHostedDomain = @"fakehosteddomain.com"; static NSString * const kFakeUserName = @"fake username"; static NSString * const kFakeUserGivenName = @"fake"; static NSString * const kFakeUserFamilyName = @"username"; static NSString * const kFakeUserPictureURL = @"fake_user_picture_url"; static NSString * const kContinueURL = @"com.google.UnitTests:/oauth2callback"; static NSString * const kContinueURLWithClientID = @"FakeClientID:/oauth2callback"; static NSString * const kWrongSchemeURL = @"wrong.app:/oauth2callback"; static NSString * const kWrongPathURL = @"com.google.UnitTests:/wrong_path"; static NSString * const kEMMRestartAuthURL = @"com.google.UnitTests:///emmcallback?action=restart_auth"; static NSString * const kEMMWrongPathURL = @"com.google.UnitTests:///unknowcallback?action=restart_auth"; static NSString * const kEMMWrongActionURL = @"com.google.UnitTests:///emmcallback?action=unrecognized"; static NSString * const kDevicePolicyAppBundleID = @"com.google.DevicePolicy"; static NSString * const kAppHasRunBeforeKey = @"GPP_AppHasRunBefore"; static NSString * const kFingerprintKeychainName = @"fingerprint"; static NSString * const kVerifierKeychainName = @"verifier"; static NSString * const kVerifierKey = @"verifier"; //static NSString * const kOpenIDRealmKey = @"openid.realm"; static NSString * const kSavedKeychainServiceName = @"saved-keychain"; static NSString * const kKeychainAccountName = @"GooglePlus"; static NSString * const kUserNameKey = @"name"; static NSString * const kUserGivenNameKey = @"givenName"; static NSString * const kUserFamilyNameKey = @"familyName"; static NSString * const kUserImageKey = @"picture"; static NSString * const kAppName = @"UnitTests"; static NSString * const kUserIDKey = @"userID"; static NSString * const kHostedDomainKey = @"hostedDomain"; static NSString * const kIDTokenExpirationKey = @"idTokenExp"; static NSString * const kCustomKeychainName = @"CUSTOM_KEYCHAIN_NAME"; static NSString * const kAddActivity = @"http://schemas.google.com/AddActivity"; static NSString * const kErrorDomain = @"ERROR_DOMAIN"; static NSInteger const kErrorCode = 212; static NSString *const kDriveScope = @"https://www.googleapis.com/auth/drive"; static NSString *const kTokenURL = @"https://oauth2.googleapis.com/token"; static NSString *const kFakeURL = @"http://foo.com"; static NSString *const kNewScope = @"newScope"; #if TARGET_OS_IOS || TARGET_OS_MACCATALYST // This category is used to allow the test to swizzle a private method. @interface UIViewController (Testing) // This private method provides access to the window. It's declared here to avoid a warning about // an unrecognized selector in the test. - (UIWindow *)_window; @end #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST // This class extension exposes GIDSignIn methods to our tests. @interface GIDSignIn () // Exposing private method so we can call it to disambiguate between interactive and non-interactive // sign-in attempts for the purposes of testing the GIDSignInUIDelegate (which should not be // called in the case of a non-interactive sign in). - (void)authenticateMaybeInteractively:(BOOL)interactive withParams:(NSDictionary *)params; - (BOOL)assertValidPresentingViewContoller; @end @interface GIDSignInTest : XCTestCase { @private // Whether or not the OS version is eligible for EMM. BOOL _isEligibleForEMM; // Mock `OIDAuthState`. id _authState; // Mock `OIDTokenResponse`. id _tokenResponse; // Mock `OIDTokenRequest`. id _tokenRequest; // Mock `GTMAppAuthFetcherAuthorization`. id _authorization; // Fake for `GIDKeychainHandler`. GIDFakeKeychainHandler *_keychainHandler; // Fake for `GIDHTTPFetcher`. GIDFakeHTTPFetcher *_httpFetcher; // Fake for `GIDAuthorizationFlowProcessor`. GIDFakeAuthorizationFlowProcessor *_authorizationFlowProcessor; // Fake for `GIDProfileDataFetcher`. GIDFakeProfileDataFetcher *_profileDataFetcher; #if TARGET_OS_IOS || TARGET_OS_MACCATALYST // Mock `UIViewController`. id _presentingViewController; #elif TARGET_OS_OSX // Mock `NSWindow`. id _presentingWindow; #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST // Mock for `GIDGoogleUser`. id _user; // Mock for `OIDAuthorizationService`. id _oidAuthorizationService; // Parameter saved from delegate call. NSError *_authError; // Whether callback block has been called. BOOL _completionCalled; // Fake [NSBundle mainBundle]; GIDFakeMainBundle *_fakeMainBundle; // The `GIDSignIn` object being tested. GIDSignIn *_signIn; // The configuration to be used when testing `GIDSignIn`. GIDConfiguration *_configuration; // The completion to be used when testing `GIDSignIn`. GIDSignInCompletion _completion; // The saved token request callback. OIDTokenCallback _savedTokenCallback; } @end @implementation GIDSignInTest #pragma mark - Lifecycle - (void)setUp { [super setUp]; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST _isEligibleForEMM = [UIDevice currentDevice].systemVersion.integerValue >= 9; #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST // States _completionCalled = NO; // Mocks #if TARGET_OS_IOS || TARGET_OS_MACCATALYST _presentingViewController = OCMStrictClassMock([UIViewController class]); #elif TARGET_OS_OSX _presentingWindow = OCMStrictClassMock([NSWindow class]); #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST _authState = OCMStrictClassMock([OIDAuthState class]); OCMStub([_authState alloc]).andReturn(_authState); OCMStub([_authState initWithAuthorizationResponse:OCMOCK_ANY]).andReturn(_authState); _tokenResponse = OCMStrictClassMock([OIDTokenResponse class]); _tokenRequest = OCMStrictClassMock([OIDTokenRequest class]); _authorization = OCMStrictClassMock([GTMAppAuthFetcherAuthorization class]); OCMStub([_authorization alloc]).andReturn(_authorization); OCMStub([_authorization initWithAuthState:OCMOCK_ANY]).andReturn(_authorization); _user = OCMStrictClassMock([GIDGoogleUser class]); _oidAuthorizationService = OCMStrictClassMock([OIDAuthorizationService class]); OCMStub([self->_oidAuthorizationService performTokenRequest:OCMOCK_ANY callback:COPY_TO_ARG_BLOCK(self->_savedTokenCallback)]); // Fakes _fakeMainBundle = [[GIDFakeMainBundle alloc] init]; [_fakeMainBundle startFakingWithClientID:kClientId]; [_fakeMainBundle fakeAllSchemesSupported]; // Object under test [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kAppHasRunBeforeKey]; _keychainHandler = [[GIDFakeKeychainHandler alloc] init]; _httpFetcher = [[GIDFakeHTTPFetcher alloc] init]; _authorizationFlowProcessor = [[GIDFakeAuthorizationFlowProcessor alloc] init]; _profileDataFetcher = [[GIDFakeProfileDataFetcher alloc] init]; _signIn = [[GIDSignIn alloc] initWithKeychainHandler:_keychainHandler httpFetcher:_httpFetcher profileDataFetcher:_profileDataFetcher authorizationFlowProcessor:_authorizationFlowProcessor]; __weak GIDSignInTest *weakSelf = self; _completion = ^(GIDSignInResult *_Nullable signInResult, NSError * _Nullable error) { GIDSignInTest *strongSelf = weakSelf; if (!signInResult) { XCTAssertNotNil(error, @"should have an error if the signInResult is nil"); } XCTAssertFalse(strongSelf->_completionCalled, @"callback already called"); strongSelf->_completionCalled = YES; strongSelf->_authError = error; }; } - (void)tearDown { OCMVerifyAll(_authState); OCMVerifyAll(_tokenResponse); OCMVerifyAll(_tokenRequest); OCMVerifyAll(_user); // OCMVerifyAll(_oidAuthorizationService); #if TARGET_OS_IOS || TARGET_OS_MACCATALYST OCMVerifyAll(_presentingViewController); #elif TARGET_OS_OSX OCMVerifyAll(_presentingWindow); #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST [_fakeMainBundle stopFaking]; [super tearDown]; } #pragma mark - Tests - (void)testShareInstance { GIDSignIn *signIn1 = GIDSignIn.sharedInstance; GIDSignIn *signIn2 = GIDSignIn.sharedInstance; XCTAssertTrue(signIn1 == signIn2, @"shared instance must be singleton"); } - (void)testInitPrivate { GIDSignIn *signIn = [[GIDSignIn alloc] initPrivate]; XCTAssertNotNil(signIn.configuration); XCTAssertEqual(signIn.configuration.clientID, kClientId); XCTAssertNil(signIn.configuration.serverClientID); XCTAssertNil(signIn.configuration.hostedDomain); XCTAssertNil(signIn.configuration.openIDRealm); } - (void)testInitPrivate_noConfig { [_fakeMainBundle fakeWithClientID:nil serverClientID:nil hostedDomain:nil openIDRealm:nil]; GIDSignIn *signIn = [[GIDSignIn alloc] initPrivate]; XCTAssertNil(signIn.configuration); } - (void)testInitPrivate_fullConfig { [_fakeMainBundle fakeWithClientID:kClientId serverClientID:kServerClientId hostedDomain:kFakeHostedDomain openIDRealm:kOpenIDRealm]; GIDSignIn *signIn = [[GIDSignIn alloc] initPrivate]; XCTAssertNotNil(signIn.configuration); XCTAssertEqual(signIn.configuration.clientID, kClientId); XCTAssertEqual(signIn.configuration.serverClientID, kServerClientId); XCTAssertEqual(signIn.configuration.hostedDomain, kFakeHostedDomain); XCTAssertEqual(signIn.configuration.openIDRealm, kOpenIDRealm); } - (void)testInitPrivate_invalidConfig { [_fakeMainBundle fakeWithClientID:@[ @"bad", @"config", @"values" ] serverClientID:nil hostedDomain:nil openIDRealm:nil]; GIDSignIn *signIn = [[GIDSignIn alloc] initPrivate]; XCTAssertNil(signIn.configuration); } - (void)testRestorePreviousSignInNoRefresh_hasPreviousUser { [[[_authorization stub] andReturn:_authState] authState]; [[_authorization expect] setTokenRefreshDelegate:OCMOCK_ANY]; OCMStub([_authState lastTokenResponse]).andReturn(_tokenResponse); OCMStub([_authState refreshToken]).andReturn(kRefreshToken); [[_authState expect] setStateChangeDelegate:OCMOCK_ANY]; [_keychainHandler saveAuthState:_authState]; id idTokenDecoded = OCMClassMock([OIDIDToken class]); OCMStub([idTokenDecoded alloc]).andReturn(idTokenDecoded); OCMStub([idTokenDecoded initWithIDTokenString:OCMOCK_ANY]).andReturn(idTokenDecoded); OCMStub([idTokenDecoded subject]).andReturn(kFakeGaiaID); // Mock generating a GIDConfiguration when initializing GIDGoogleUser. OIDAuthorizationResponse *authResponse = [OIDAuthorizationResponse testInstanceWithAdditionalParameters:nil errorString:nil]; OCMStub([_authState lastAuthorizationResponse]).andReturn(authResponse); OCMStub([_tokenResponse idToken]).andReturn(kFakeIDToken); OCMStub([_tokenResponse accessToken]).andReturn(kAccessToken); OCMStub([_tokenResponse accessTokenExpirationDate]).andReturn(nil); GIDProfileData *fakeProfileData = [GIDProfileData testInstance]; GIDProfileDataFetcherTestBlock testBlock = ^(GIDProfileDataFetcherFakeResponseProvider responseProvider) { responseProvider(fakeProfileData, nil); }; _profileDataFetcher.testBlock = testBlock; [_signIn restorePreviousSignInNoRefresh]; XCTAssertEqual(_signIn.currentUser.userID, kFakeGaiaID); XCTAssertEqualObjects(_signIn.currentUser.profile, fakeProfileData); [idTokenDecoded stopMocking]; } - (void)testRestoredPreviousSignInNoRefresh_hasNoPreviousUser { XCTAssertNil([_keychainHandler loadAuthState]); [_signIn restorePreviousSignInNoRefresh]; XCTAssertNil(_signIn.currentUser); } - (void)testHasPreviousSignIn_HasBeenAuthenticated { [_keychainHandler saveAuthState:_authState]; [[[_authState expect] andReturnValue:[NSNumber numberWithBool:YES]] isAuthorized]; XCTAssertTrue([_signIn hasPreviousSignIn], @"should return |YES|"); [_authState verify]; XCTAssertFalse(_completionCalled, @"should not call delegate"); XCTAssertNil(_authError, @"should have no error"); } - (void)testHasPreviousSignIn_HasNotBeenAuthenticated { [_keychainHandler saveAuthState:_authState]; [[[_authState expect] andReturnValue:[NSNumber numberWithBool:NO]] isAuthorized]; XCTAssertFalse([_signIn hasPreviousSignIn], @"should return |NO|"); [_authState verify]; XCTAssertFalse(_completionCalled, @"should not call delegate"); } - (void)testRestorePreviousSignInWhenSignedOut { [_keychainHandler saveAuthState:_authState]; [[[_authState expect] andReturnValue:[NSNumber numberWithBool:NO]] isAuthorized]; _completionCalled = NO; _authError = nil; XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called."]; [_signIn restorePreviousSignInWithCompletion:^(GIDGoogleUser *_Nullable user, NSError * _Nullable error) { [expectation fulfill]; XCTAssertNotNil(error, @"error should not have been nil"); XCTAssertEqual(error.domain, kGIDSignInErrorDomain, @"error domain should have been the sign-in error domain."); XCTAssertEqual(error.code, kGIDSignInErrorCodeHasNoAuthInKeychain, @"error code should have been the 'NoAuthInKeychain' error code."); }]; [self waitForExpectationsWithTimeout:1 handler:nil]; [_authState verify]; } - (void)testOAuthLogin { [self OAuthLoginWithAddScopesFlow:NO authError:nil tokenError:nil emmPasscodeInfoRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO]; } - (void)testOAuthLogin_RestoredSignIn { [self OAuthLoginWithAddScopesFlow:NO authError:nil tokenError:nil emmPasscodeInfoRequired:NO keychainError:NO restoredSignIn:YES oldAccessToken:NO modalCancel:NO]; } - (void)testOAuthLogin_RestoredSignInOldAccessToken { [self OAuthLoginWithAddScopesFlow:NO authError:nil tokenError:nil emmPasscodeInfoRequired:NO keychainError:NO restoredSignIn:YES oldAccessToken:YES modalCancel:NO]; } - (void)testOAuthLogin_ConsentCanceled { [self OAuthLoginWithAddScopesFlow:NO authError:@"access_denied" tokenError:nil emmPasscodeInfoRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO]; [self waitForExpectationsWithTimeout:1 handler:nil]; XCTAssertTrue(_completionCalled, @"should call delegate"); XCTAssertEqual(_authError.code, kGIDSignInErrorCodeCanceled); } - (void)testOAuthLogin_ModalCanceled { [self OAuthLoginWithAddScopesFlow:NO authError:nil tokenError:nil emmPasscodeInfoRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:YES]; [self waitForExpectationsWithTimeout:1 handler:nil]; XCTAssertTrue(_completionCalled, @"should call delegate"); XCTAssertEqual(_authError.code, kGIDSignInErrorCodeCanceled); } - (void)testOAuthLogin_KeychainError { [self OAuthLoginWithAddScopesFlow:NO authError:nil tokenError:nil emmPasscodeInfoRequired:NO keychainError:YES restoredSignIn:NO oldAccessToken:NO modalCancel:NO]; [self waitForExpectationsWithTimeout:1 handler:nil]; XCTAssertTrue(_completionCalled, @"should call delegate"); XCTAssertEqualObjects(_authError.domain, kGIDSignInErrorDomain); XCTAssertEqual(_authError.code, kGIDSignInErrorCodeKeychain); } - (void)testSignOut { XCTAssert([_keychainHandler saveAuthState:_authState]); // Sign in a user so that we can then sign them out. [self OAuthLoginWithAddScopesFlow:NO authError:nil tokenError:nil emmPasscodeInfoRequired:NO keychainError:NO restoredSignIn:YES oldAccessToken:NO modalCancel:NO]; XCTAssertNotNil(_signIn.currentUser); XCTAssertNotNil([_keychainHandler loadAuthState]); [_signIn signOut]; XCTAssertNil(_signIn.currentUser, @"should not have a current user"); XCTAssertNil([_keychainHandler loadAuthState]); } - (void)testNotHandleWrongPath { XCTAssertFalse([_signIn handleURL:[NSURL URLWithString:kWrongPathURL]], @"should not handle URL"); XCTAssertFalse(_completionCalled, @"should not call delegate"); } #pragma mark - Tests - disconnectWithCallback: // Verifies disconnect calls callback with no errors if access token is present. - (void)testDisconnect_accessTokenIsPresent { [_keychainHandler saveAuthState:_authState]; OCMStub([_authState lastTokenResponse]).andReturn(_tokenResponse); OCMStub([_tokenResponse accessToken]).andReturn(kAccessToken); XCTestExpectation *fetcherExpectation = [self expectationWithDescription:@"testBlock is invoked."]; GIDHTTPFetcherTestBlock testBlock = ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) { [self verifyRevokeRequest:request withToken:kAccessToken]; NSData *data = [[NSData alloc] init]; responseProvider(data, nil); [fetcherExpectation fulfill]; }; [_httpFetcher setTestBlock:testBlock]; XCTestExpectation *completionExpectation = [self expectationWithDescription:@"Callback called with nil error"]; [_signIn disconnectWithCompletion:^(NSError * _Nullable error) { XCTAssertNil(error); [completionExpectation fulfill]; }]; [self waitForExpectationsWithTimeout:1 handler:nil]; XCTAssertNil([_keychainHandler loadAuthState]); } // Verifies disconnect if access token is present. - (void)testDisconnectNoCallback_accessTokenIsPresent { [_keychainHandler saveAuthState:_authState]; OCMStub([_authState lastTokenResponse]).andReturn(_tokenResponse); OCMStub([_tokenResponse accessToken]).andReturn(kAccessToken); XCTestExpectation *fetcherExpectation = [self expectationWithDescription:@"testBlock is invoked."]; GIDHTTPFetcherTestBlock testBlock = ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) { [self verifyRevokeRequest:request withToken:kAccessToken]; NSData *data = [[NSData alloc] init]; responseProvider(data, nil); [fetcherExpectation fulfill]; }; [_httpFetcher setTestBlock:testBlock]; [_signIn disconnectWithCompletion:nil]; [self waitForExpectationsWithTimeout:1 handler:nil]; XCTAssertNil([_keychainHandler loadAuthState]); } // Verifies disconnect calls callback with no errors if refresh token is present. - (void)testDisconnect_refreshTokenIsPresent { [_keychainHandler saveAuthState:_authState]; OCMStub([_authState lastTokenResponse]).andReturn(_tokenResponse); OCMStub([_tokenResponse accessToken]).andReturn(nil); OCMStub([_tokenResponse refreshToken]).andReturn(kRefreshToken); XCTestExpectation *fetcherExpectation = [self expectationWithDescription:@"testBlock is invoked."]; GIDHTTPFetcherTestBlock testBlock = ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) { [self verifyRevokeRequest:request withToken:kRefreshToken]; NSData *data = [[NSData alloc] init]; responseProvider(data, nil); [fetcherExpectation fulfill]; }; [_httpFetcher setTestBlock:testBlock]; XCTestExpectation *completionExpectation = [self expectationWithDescription:@"Callback called with nil error"]; [_signIn disconnectWithCompletion:^(NSError * _Nullable error) { XCTAssertNil(error); [completionExpectation fulfill]; }]; [self waitForExpectationsWithTimeout:1 handler:nil]; XCTAssertNil([_keychainHandler loadAuthState]); } // Verifies disconnect errors are passed along to the callback. - (void)testDisconnect_errors { [_keychainHandler saveAuthState:_authState]; OCMStub([_authState lastTokenResponse]).andReturn(_tokenResponse); OCMStub([_tokenResponse accessToken]).andReturn(kAccessToken); XCTestExpectation *fetcherExpectation = [self expectationWithDescription:@"testBlock is invoked."]; GIDHTTPFetcherTestBlock testBlock = ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) { [self verifyRevokeRequest:request withToken:kAccessToken]; NSError *error = [self error]; responseProvider(nil, error); [fetcherExpectation fulfill]; }; [_httpFetcher setTestBlock:testBlock]; XCTestExpectation *completionExpectation = [self expectationWithDescription:@"Callback called with an error"]; [_signIn disconnectWithCompletion:^(NSError * _Nullable error) { XCTAssertNotNil(error); [completionExpectation fulfill]; }]; [self waitForExpectationsWithTimeout:1 handler:nil]; XCTAssertNotNil([_keychainHandler loadAuthState]); } // Verifies disconnect with errors - (void)testDisconnectNoCallback_errors { [_keychainHandler saveAuthState:_authState]; OCMStub([_authState lastTokenResponse]).andReturn(_tokenResponse); OCMStub([_tokenResponse accessToken]).andReturn(kAccessToken); XCTestExpectation *fetcherExpectation = [self expectationWithDescription:@"testBlock is invoked."]; GIDHTTPFetcherTestBlock testBlock = ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) { [self verifyRevokeRequest:request withToken:kAccessToken]; NSError *error = [self error]; responseProvider(nil, error); [fetcherExpectation fulfill]; }; [_httpFetcher setTestBlock:testBlock]; [_signIn disconnectWithCompletion:nil]; [self waitForExpectationsWithTimeout:1 handler:nil]; XCTAssertNotNil([_keychainHandler loadAuthState]); } // Verifies disconnect calls callback with no errors and clears keychain if no tokens are present. - (void)testDisconnect_noTokens { [_keychainHandler saveAuthState:_authState]; OCMStub([_authState lastTokenResponse]).andReturn(_tokenResponse); OCMStub([_tokenResponse accessToken]).andReturn(nil); OCMStub([_tokenResponse refreshToken]).andReturn(nil); GIDHTTPFetcherTestBlock testBlock = ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) { XCTFail(@"_httpFetcher should not be invoked."); }; [_httpFetcher setTestBlock:testBlock]; XCTestExpectation *expectation = [self expectationWithDescription:@"Callback called with nil error"]; [_signIn disconnectWithCompletion:^(NSError * _Nullable error) { XCTAssertNil(error); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:1 handler:nil]; XCTAssertNil([_keychainHandler loadAuthState]); } // Verifies disconnect clears keychain if no tokens are present. - (void)testDisconnectNoCallback_noTokens { [_keychainHandler saveAuthState:_authState]; OCMStub([_authState lastTokenResponse]).andReturn(_tokenResponse); OCMStub([_tokenResponse accessToken]).andReturn(nil); OCMStub([_tokenResponse refreshToken]).andReturn(nil); GIDHTTPFetcherTestBlock testBlock = ^(NSURLRequest *request, GIDHTTPFetcherFakeResponseProviderBlock responseProvider) { XCTFail(@"_httpFetcher should not be invoked."); }; [_httpFetcher setTestBlock:testBlock]; [_signIn disconnectWithCompletion:nil]; XCTAssertNil([_keychainHandler loadAuthState]); } - (void)testPresentingViewControllerException { #if TARGET_OS_IOS || TARGET_OS_MACCATALYST _presentingViewController = nil; #elif TARGET_OS_OSX _presentingWindow = nil; #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST #if TARGET_OS_IOS || TARGET_OS_MACCATALYST XCTAssertThrows([_signIn signInWithPresentingViewController:_presentingViewController #elif TARGET_OS_OSX XCTAssertThrows([_signIn signInWithPresentingWindow:_presentingWindow #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST hint:nil completion:_completion]); } - (void)testClientIDMissingException { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wnonnull" _signIn.configuration = [[GIDConfiguration alloc] initWithClientID:nil]; #pragma GCC diagnostic pop BOOL threw = NO; @try { #if TARGET_OS_IOS || TARGET_OS_MACCATALYST [_signIn signInWithPresentingViewController:_presentingViewController #elif TARGET_OS_OSX [_signIn signInWithPresentingWindow:_presentingWindow #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST completion:nil]; } @catch (NSException *exception) { threw = YES; XCTAssertEqualObjects(exception.description, @"You must specify |clientID| in |GIDConfiguration|"); } @finally { } XCTAssert(threw); } - (void)testSchemesNotSupportedException { [_fakeMainBundle fakeMissingAllSchemes]; BOOL threw = NO; @try { #if TARGET_OS_IOS || TARGET_OS_MACCATALYST [_signIn signInWithPresentingViewController:_presentingViewController #elif TARGET_OS_OSX [_signIn signInWithPresentingWindow:_presentingWindow #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST hint:nil completion:_completion]; } @catch (NSException *exception) { threw = YES; XCTAssertEqualObjects(exception.description, @"Your app is missing support for the following URL schemes: " "fakeclientid"); } @finally { } XCTAssert(threw); } #pragma mark - Restarting Authentication Tests // Verifies that URL is not handled if there is no pending sign-in - (void)testRequiringPendingSignIn { BOOL result = [_signIn handleURL:[NSURL URLWithString:kEMMRestartAuthURL]]; XCTAssertFalse(result); } #pragma mark - EMM tests #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - (void)testAuthEndpointEMMError { if (!_isEligibleForEMM) { return; } id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]); [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance]; __block void (^completion)(void); NSDictionary *callbackParams = @{ @"error" : @"EMM Specific Error" }; [[[mockEMMErrorHandler expect] andReturnValue:@YES] handleErrorFromResponse:callbackParams completion:SAVE_TO_ARG_BLOCK(completion)]; [self OAuthLoginWithAddScopesFlow:NO authError:callbackParams[@"error"] tokenError:nil emmPasscodeInfoRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO]; [mockEMMErrorHandler verify]; [mockEMMErrorHandler stopMocking]; completion(); [self waitForExpectationsWithTimeout:1 handler:nil]; XCTAssertTrue(_completionCalled, @"should call delegate"); XCTAssertNotNil(_authError, @"should have error"); XCTAssertEqualObjects(_authError.domain, kGIDSignInErrorDomain); XCTAssertEqual(_authError.code, kGIDSignInErrorCodeEMM); XCTAssertNil(_signIn.currentUser, @"should not have current user"); } - (void)testTokenEndpointEMMError { if (!_isEligibleForEMM) { return; } __block void (^completion)(NSError *); NSDictionary *errorJSON = @{ @"error" : @"EMM Specific Error" }; NSError *emmError = [NSError errorWithDomain:@"anydomain" code:12345 userInfo:@{ OIDOAuthErrorFieldError : errorJSON }]; id emmSupport = OCMStrictClassMock([GIDEMMSupport class]); [[emmSupport expect] handleTokenFetchEMMError:emmError completion:SAVE_TO_ARG_BLOCK(completion)]; [self OAuthLoginWithAddScopesFlow:NO authError:nil tokenError:emmError emmPasscodeInfoRequired:NO keychainError:NO restoredSignIn:NO oldAccessToken:NO modalCancel:NO]; NSError *handledError = [NSError errorWithDomain:kGIDSignInErrorDomain code:kGIDSignInErrorCodeEMM userInfo:emmError.userInfo]; completion(handledError); [self waitForExpectationsWithTimeout:1 handler:nil]; [emmSupport verify]; XCTAssertTrue(_completionCalled, @"should call delegate"); XCTAssertNotNil(_authError, @"should have error"); XCTAssertEqualObjects(_authError.domain, kGIDSignInErrorDomain); XCTAssertEqual(_authError.code, kGIDSignInErrorCodeEMM); XCTAssertNil(_signIn.currentUser, @"should not have current user"); } #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST #pragma mark - Helpers - (NSError *)error { return [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:nil]; } - (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"); OIDURLQueryComponent *queryComponent = [[OIDURLQueryComponent alloc] initWithURL:url]; NSDictionary *> *params = queryComponent.dictionaryValue; XCTAssertEqualObjects([params valueForKey:kSDKVersionLoggingParameter], GIDVersion(), @"SDK version logging parameter should match"); XCTAssertEqualObjects([params valueForKey:kEnvironmentLoggingParameter], GIDEnvironment(), @"Environment logging parameter should match"); NSData *body = request.HTTPBody; NSString* bodyString = [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]; NSArray *strings = [bodyString componentsSeparatedByString:@"="]; XCTAssertEqualObjects(strings[1], token); } // The authorization flow with parameters to control which branches to take. - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow authError:(NSString *)authError tokenError:(NSError *)tokenError emmPasscodeInfoRequired:(BOOL)emmPasscodeInfoRequired keychainError:(BOOL)keychainError restoredSignIn:(BOOL)restoredSignIn oldAccessToken:(BOOL)oldAccessToken modalCancel:(BOOL)modalCancel { if (restoredSignIn) { // clearAndAuthenticateWithOptions [_keychainHandler saveAuthState:_authState]; BOOL isAuthorized = restoredSignIn ? YES : NO; [[[_authState expect] andReturnValue:[NSNumber numberWithBool:isAuthorized]] isAuthorized]; } NSDictionary *additionalParameters = emmPasscodeInfoRequired ? @{ @"emm_passcode_info_required" : @"1" } : nil; OIDAuthorizationResponse *authResponse = [OIDAuthorizationResponse testInstanceWithAdditionalParameters:additionalParameters errorString:authError]; OIDTokenResponse *tokenResponse = [OIDTokenResponse testInstanceWithIDToken:[OIDTokenResponse fatIDToken] accessToken:restoredSignIn ? kAccessToken : nil expiresIn:oldAccessToken ? @(300) : nil refreshToken:kRefreshToken tokenRequest:nil]; OIDTokenRequest *tokenRequest = [[OIDTokenRequest alloc] initWithConfiguration:authResponse.request.configuration grantType:OIDGrantTypeRefreshToken authorizationCode:nil redirectURL:nil clientID:authResponse.request.clientID clientSecret:authResponse.request.clientSecret scope:nil refreshToken:kRefreshToken codeVerifier:nil additionalParameters:tokenResponse.request.additionalParameters]; // Set the response for the auth endpoint. GIDAuthorizationFlowProcessorTestBlock authorizationFlowTestBlock; if (modalCancel) { NSError *error = [NSError errorWithDomain:OIDGeneralErrorDomain code:OIDErrorCodeUserCanceledAuthorizationFlow userInfo:nil]; authorizationFlowTestBlock = ^(GIDAuthorizationFlowProcessorFakeResponseProviderBlock responseProvider) { responseProvider(nil, error); }; } else { authorizationFlowTestBlock = ^(GIDAuthorizationFlowProcessorFakeResponseProviderBlock responseProvider) { responseProvider(authResponse, nil); }; } _authorizationFlowProcessor.testBlock = authorizationFlowTestBlock; // Set the response for `GIDProfileDataFetcher`. GIDProfileDataFetcherTestBlock profileDataFetcherTestBlock = ^(GIDProfileDataFetcherFakeResponseProvider responseProvider) { GIDProfileData *profileData = [GIDProfileData testInstance]; responseProvider(profileData, nil); }; _profileDataFetcher.testBlock = profileDataFetcherTestBlock; if (restoredSignIn) { // Mock `maybeFetchToken:` method in `restorePreviousSignIn:` flow. [[[_authState expect] andReturn:tokenResponse] lastTokenResponse]; [[[_authState expect] andReturn:tokenResponse] lastTokenResponse]; if (oldAccessToken) { #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST // Corresponds to EMM support [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse]; #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST [[[_authState expect] andReturn:tokenResponse] lastTokenResponse]; [[[_authState expect] andReturn:tokenResponse] lastTokenResponse]; [[[_authState expect] andReturn:tokenRequest] tokenRefreshRequestWithAdditionalParameters:[OCMArg any]]; } } else { XCTestExpectation *expectation = [self expectationWithDescription:@"Callback called"]; GIDSignInCompletion completion = ^(GIDSignInResult *_Nullable signInResult, NSError * _Nullable error) { [expectation fulfill]; if (signInResult) { XCTAssertEqualObjects(signInResult.serverAuthCode, kServerAuthCode); } else { XCTAssertNotNil(error, @"Should have an error if the signInResult is nil"); } XCTAssertFalse(self->_completionCalled, @"callback already called"); self->_completionCalled = YES; self->_authError = error; }; if (addScopesFlow) { [_signIn addScopes:@[kNewScope] #if TARGET_OS_IOS || TARGET_OS_MACCATALYST presentingViewController:_presentingViewController #elif TARGET_OS_OSX presentingWindow:_presentingWindow #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST completion:completion]; } else { // Mock `maybeFetchToken:` method in Sign in flow. if (!(authError || modalCancel)) { [[[_authState expect] andReturn:nil] lastTokenResponse]; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST // Corresponds to EMM support [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse]; #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST [[[_authState expect] andReturn:nil] lastTokenResponse]; [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse]; [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse]; } #if TARGET_OS_IOS || TARGET_OS_MACCATALYST [_signIn signInWithPresentingViewController:_presentingViewController #elif TARGET_OS_OSX [_signIn signInWithPresentingWindow:_presentingWindow #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST hint:nil completion:completion]; } if (authError || modalCancel) { return; } [_authState verify]; } if (restoredSignIn && oldAccessToken) { XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called"]; [_signIn restorePreviousSignInWithCompletion:^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) { [expectation fulfill]; XCTAssertNil(error, @"should have no error"); }]; } if (!restoredSignIn || (restoredSignIn && oldAccessToken)) { XCTAssertNotNil(_savedTokenCallback); // OIDTokenCallback if (tokenError) { [[_authState expect] updateWithTokenResponse:nil error:tokenError]; } else { [[_authState expect] updateWithTokenResponse:[OCMArg any] error:nil]; } } if (tokenError) { _savedTokenCallback(nil, tokenError); return; } // SaveAuthCallback __block OIDAuthState *authState; __block OIDTokenResponse *updatedTokenResponse; __block OIDAuthorizationResponse *updatedAuthorizationResponse; __block GIDProfileData *profileData; if (keychainError) { _keychainHandler.failToSave = YES; } else { if (addScopesFlow) { [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse]; [[[_authState expect] andReturn:tokenResponse] lastTokenResponse]; [[_user expect] updateWithTokenResponse:SAVE_TO_ARG_BLOCK(updatedTokenResponse) authorizationResponse:SAVE_TO_ARG_BLOCK(updatedAuthorizationResponse) profileData:SAVE_TO_ARG_BLOCK(profileData)]; } else { [[[_user stub] andReturn:_user] alloc]; (void)[[[_user expect] andReturn:_user] initWithAuthState:SAVE_TO_ARG_BLOCK(authState) profileData:SAVE_TO_ARG_BLOCK(profileData)]; } } // CompletionCallback - mock server auth code parsing if (!keychainError) { [[[_authState expect] andReturn:tokenResponse] lastTokenResponse]; } if (restoredSignIn && !oldAccessToken) { XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called"]; [_signIn restorePreviousSignInWithCompletion:^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) { [expectation fulfill]; XCTAssertNil(error, @"should have no error"); }]; } else { // Simulate token endpoint response. _savedTokenCallback(tokenResponse, nil); } if (keychainError) { return; } [self waitForExpectationsWithTimeout:1 handler:nil]; [_authState verify]; if (addScopesFlow) { XCTAssertNotNil(updatedTokenResponse); XCTAssertNotNil(updatedAuthorizationResponse); } else { XCTAssertNotNil(authState); } // Check fat ID token decoding XCTAssertEqualObjects(profileData, [GIDProfileData testInstance]); // If attempt to authenticate again, will reuse existing auth object. _completionCalled = NO; _authError = nil; __block GIDGoogleUserCompletion completion; [[_user expect] refreshTokensIfNeededWithCompletion:SAVE_TO_ARG_BLOCK(completion)]; XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called"]; [_signIn restorePreviousSignInWithCompletion:^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) { [expectation fulfill]; XCTAssertNil(error, @"should have no error"); }]; completion(_user, nil); [self waitForExpectationsWithTimeout:1 handler:nil]; } @end