GIDSignInTest.m 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404
  1. // Copyright 2021 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #import <TargetConditionals.h>
  15. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  16. #import <UIKit/UIKit.h>
  17. #elif TARGET_OS_OSX
  18. #import <AppKit/AppKit.h>
  19. #endif
  20. #import <SafariServices/SafariServices.h>
  21. #import <XCTest/XCTest.h>
  22. // Test module imports
  23. @import GoogleSignIn;
  24. #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
  25. #import "GoogleSignIn/Sources/GIDSignIn_Private.h"
  26. #import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
  27. #if TARGET_OS_IOS
  28. #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
  29. #endif
  30. #import "GoogleSignIn/Tests/Unit/GIDFakeFetcher.h"
  31. #import "GoogleSignIn/Tests/Unit/GIDFakeFetcherService.h"
  32. #import "GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h"
  33. #import "GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.h"
  34. #import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h"
  35. #ifdef SWIFT_PACKAGE
  36. @import AppAuth;
  37. @import GTMAppAuth;
  38. @import GTMSessionFetcherCore;
  39. @import OCMock;
  40. #else
  41. #import <AppAuth/OIDAuthState.h>
  42. #import <AppAuth/OIDAuthorizationRequest.h>
  43. #import <AppAuth/OIDAuthorizationResponse.h>
  44. #import <AppAuth/OIDAuthorizationService.h>
  45. #import <AppAuth/OIDError.h>
  46. #import <AppAuth/OIDGrantTypes.h>
  47. #import <AppAuth/OIDTokenRequest.h>
  48. #import <AppAuth/OIDTokenResponse.h>
  49. #import <AppAuth/OIDURLQueryComponent.h>
  50. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  51. #import <AppAuth/OIDAuthorizationService+IOS.h>
  52. #elif TARGET_OS_OSX
  53. #import <AppAuth/OIDAuthorizationService+Mac.h>
  54. #endif
  55. #import <GTMAppAuth/GTMAppAuthFetcherAuthorization+Keychain.h>
  56. #import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
  57. #import <GTMSessionFetcher/GTMSessionFetcher.h>
  58. #import <OCMock/OCMock.h>
  59. #endif
  60. // Create a BLOCK to store the actual address for arg in param.
  61. #define SAVE_TO_ARG_BLOCK(param) [OCMArg checkWithBlock:^(id arg) {\
  62. param = arg;\
  63. return YES;\
  64. }]
  65. #define COPY_TO_ARG_BLOCK(param) [OCMArg checkWithBlock:^(id arg) {\
  66. param = [arg copy];\
  67. return YES;\
  68. }]
  69. static NSString * const kFakeGaiaID = @"123456789";
  70. static NSString * const kFakeIDToken = @"FakeIDToken";
  71. static NSString * const kClientId = @"FakeClientID";
  72. static NSString * const kDotReversedClientId = @"FakeClientID";
  73. static NSString * const kClientId2 = @"FakeClientID2";
  74. static NSString * const kAppBundleId = @"FakeBundleID";
  75. static NSString * const kLanguage = @"FakeLanguage";
  76. static NSString * const kScope = @"FakeScope";
  77. static NSString * const kScope2 = @"FakeScope2";
  78. static NSString * const kAuthCode = @"FakeAuthCode";
  79. static NSString * const kFakeKeychainName = @"FakeKeychainName";
  80. static NSString * const kUserEmail = @"FakeUserEmail";
  81. static NSString * const kVerifier = @"FakeVerifier";
  82. static NSString * const kOpenIDRealm = @"FakeRealm";
  83. static NSString * const kFakeHostedDomain = @"fakehosteddomain.com";
  84. static NSString * const kFakeUserName = @"fake username";
  85. static NSString * const kFakeUserGivenName = @"fake";
  86. static NSString * const kFakeUserFamilyName = @"username";
  87. static NSString * const kFakeUserPictureURL = @"fake_user_picture_url";
  88. static NSString * const kContinueURL = @"com.google.UnitTests:/oauth2callback";
  89. static NSString * const kContinueURLWithClientID = @"FakeClientID:/oauth2callback";
  90. static NSString * const kWrongSchemeURL = @"wrong.app:/oauth2callback";
  91. static NSString * const kWrongPathURL = @"com.google.UnitTests:/wrong_path";
  92. static NSString * const kEMMRestartAuthURL =
  93. @"com.google.UnitTests:///emmcallback?action=restart_auth";
  94. static NSString * const kEMMWrongPathURL =
  95. @"com.google.UnitTests:///unknowcallback?action=restart_auth";
  96. static NSString * const kEMMWrongActionURL =
  97. @"com.google.UnitTests:///emmcallback?action=unrecognized";
  98. static NSString * const kDevicePolicyAppBundleID = @"com.google.DevicePolicy";
  99. static NSString * const kAppHasRunBeforeKey = @"GPP_AppHasRunBefore";
  100. static NSString * const kFingerprintKeychainName = @"fingerprint";
  101. static NSString * const kVerifierKeychainName = @"verifier";
  102. static NSString * const kVerifierKey = @"verifier";
  103. static NSString * const kOpenIDRealmKey = @"openid.realm";
  104. static NSString * const kSavedKeychainServiceName = @"saved-keychain";
  105. static NSString * const kKeychainAccountName = @"GooglePlus";
  106. static NSString * const kUserNameKey = @"name";
  107. static NSString * const kUserGivenNameKey = @"givenName";
  108. static NSString * const kUserFamilyNameKey = @"familyName";
  109. static NSString * const kUserImageKey = @"picture";
  110. static NSString * const kAppName = @"UnitTests";
  111. static NSString * const kUserIDKey = @"userID";
  112. static NSString * const kHostedDomainKey = @"hostedDomain";
  113. static NSString * const kIDTokenExpirationKey = @"idTokenExp";
  114. static NSString * const kScopeKey = @"scope";
  115. // Basic profile (Fat ID Token / userinfo endpoint) keys
  116. static NSString *const kBasicProfilePictureKey = @"picture";
  117. static NSString *const kBasicProfileNameKey = @"name";
  118. static NSString *const kBasicProfileGivenNameKey = @"given_name";
  119. static NSString *const kBasicProfileFamilyNameKey = @"family_name";
  120. static NSString * const kCustomKeychainName = @"CUSTOM_KEYCHAIN_NAME";
  121. static NSString * const kAddActivity = @"http://schemas.google.com/AddActivity";
  122. static NSString * const kErrorDomain = @"ERROR_DOMAIN";
  123. static NSInteger const kErrorCode = 212;
  124. static NSString *const kDriveScope = @"https://www.googleapis.com/auth/drive";
  125. static NSString *const kTokenURL = @"https://oauth2.googleapis.com/token";
  126. static NSString *const kFakeURL = @"http://foo.com";
  127. static NSString *const kEMMSupport = @"1";
  128. static NSString *const kGrantedScope = @"grantedScope";
  129. static NSString *const kNewScope = @"newScope";
  130. /// Unique pointer value for KVO tests.
  131. static void *kTestObserverContext = &kTestObserverContext;
  132. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  133. // This category is used to allow the test to swizzle a private method.
  134. @interface UIViewController (Testing)
  135. // This private method provides access to the window. It's declared here to avoid a warning about
  136. // an unrecognized selector in the test.
  137. - (UIWindow *)_window;
  138. @end
  139. #endif
  140. // This class extension exposes GIDSignIn methods to our tests.
  141. @interface GIDSignIn ()
  142. // Exposing private method so we can call it to disambiguate between interactive and non-interactive
  143. // sign-in attempts for the purposes of testing the GIDSignInUIDelegate (which should not be
  144. // called in the case of a non-interactive sign in).
  145. - (void)authenticateMaybeInteractively:(BOOL)interactive withParams:(NSDictionary *)params;
  146. - (BOOL)assertValidPresentingViewContoller;
  147. @end
  148. @interface GIDSignInTest : XCTestCase {
  149. @private
  150. // Whether or not the OS version is eligible for EMM.
  151. BOOL _isEligibleForEMM;
  152. // Mock |OIDAuthState|.
  153. id _authState;
  154. // Mock |OIDTokenResponse|.
  155. id _tokenResponse;
  156. // Mock |OIDTokenRequest|.
  157. id _tokenRequest;
  158. // Mock |GTMAppAuthFetcherAuthorization|.
  159. id _authorization;
  160. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  161. // Mock |UIViewController|.
  162. id _presentingViewController;
  163. #elif TARGET_OS_OSX
  164. // Mock |NSWindow|.
  165. id _presentingWindow;
  166. #endif
  167. // Mock for |GIDGoogleUser|.
  168. id _user;
  169. // Mock for |GIDAuthentication|.
  170. id _authentication;
  171. // Mock for |OIDAuthorizationService|
  172. id _oidAuthorizationService;
  173. // Parameter saved from delegate call.
  174. NSError *_authError;
  175. // Whether callback block has been called.
  176. BOOL _callbackCalled;
  177. // Fake fetcher service to emulate network requests.
  178. GIDFakeFetcherService *_fetcherService;
  179. // Fake [NSBundle mainBundle];
  180. GIDFakeMainBundle *_fakeMainBundle;
  181. // Whether |saveParamsToKeychainForName:authentication:| has been called.
  182. BOOL _keychainSaved;
  183. // Whether |removeAuthFromKeychainForName:| has been called.
  184. BOOL _keychainRemoved;
  185. // The |GIDSignIn| object being tested.
  186. GIDSignIn *_signIn;
  187. // The configuration to be used when testing |GIDSignIn|.
  188. GIDConfiguration *_configuration;
  189. // The login hint to be used when testing |GIDSignIn|.
  190. NSString *_hint;
  191. // The callback to be used when testing |GIDSignIn|.
  192. GIDSignInCallback _callback;
  193. // The saved authorization request.
  194. OIDAuthorizationRequest *_savedAuthorizationRequest;
  195. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  196. // The saved presentingViewController from the authorization request.
  197. UIViewController *_savedPresentingViewController;
  198. #elif TARGET_OS_OSX
  199. // The saved presentingWindow from the authorization request.
  200. NSWindow *_savedPresentingWindow;
  201. #endif
  202. // The saved authorization callback.
  203. OIDAuthorizationCallback _savedAuthorizationCallback;
  204. // The saved token request.
  205. OIDTokenRequest *_savedTokenRequest;
  206. // The saved token request callback.
  207. OIDTokenCallback _savedTokenCallback;
  208. // Set of all |GIDSignIn| key paths which were observed to change.
  209. NSMutableSet *_changedKeyPaths;
  210. // Status returned by saveAuthorization:toKeychainForName:
  211. BOOL _saveAuthorizationReturnValue;
  212. }
  213. @end
  214. @implementation GIDSignInTest
  215. #pragma mark - Lifecycle
  216. - (void)setUp {
  217. [super setUp];
  218. #if TARGET_OS_IOS
  219. _isEligibleForEMM = [UIDevice currentDevice].systemVersion.integerValue >= 9;
  220. #endif
  221. _saveAuthorizationReturnValue = YES;
  222. // States
  223. _callbackCalled = NO;
  224. _keychainSaved = NO;
  225. _keychainRemoved = NO;
  226. _changedKeyPaths = [[NSMutableSet alloc] init];
  227. // Mocks
  228. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  229. _presentingViewController = OCMStrictClassMock([UIViewController class]);
  230. #elif TARGET_OS_OSX
  231. _presentingWindow = OCMStrictClassMock([NSWindow class]);
  232. #endif
  233. _authState = OCMStrictClassMock([OIDAuthState class]);
  234. OCMStub([_authState alloc]).andReturn(_authState);
  235. OCMStub([_authState initWithAuthorizationResponse:OCMOCK_ANY]).andReturn(_authState);
  236. _tokenResponse = OCMStrictClassMock([OIDTokenResponse class]);
  237. _tokenRequest = OCMStrictClassMock([OIDTokenRequest class]);
  238. _authorization = OCMStrictClassMock([GTMAppAuthFetcherAuthorization class]);
  239. OCMStub([_authorization authorizationFromKeychainForName:OCMOCK_ANY]).andReturn(_authorization);
  240. OCMStub([_authorization alloc]).andReturn(_authorization);
  241. OCMStub([_authorization initWithAuthState:OCMOCK_ANY]).andReturn(_authorization);
  242. OCMStub([_authorization saveAuthorization:OCMOCK_ANY toKeychainForName:OCMOCK_ANY])
  243. .andDo(^(NSInvocation *invocation) {
  244. self->_keychainSaved = self->_saveAuthorizationReturnValue;
  245. [invocation setReturnValue:&self->_saveAuthorizationReturnValue];
  246. });
  247. OCMStub([_authorization removeAuthorizationFromKeychainForName:OCMOCK_ANY])
  248. .andDo(^(NSInvocation *invocation) {
  249. self->_keychainRemoved = YES;
  250. });
  251. _user = OCMStrictClassMock([GIDGoogleUser class]);
  252. _authentication = OCMStrictClassMock([GIDAuthentication class]);
  253. _oidAuthorizationService = OCMStrictClassMock([OIDAuthorizationService class]);
  254. OCMStub([_oidAuthorizationService
  255. presentAuthorizationRequest:SAVE_TO_ARG_BLOCK(self->_savedAuthorizationRequest)
  256. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  257. presentingViewController:SAVE_TO_ARG_BLOCK(self->_savedPresentingViewController)
  258. #elif TARGET_OS_OSX
  259. presentingWindow:SAVE_TO_ARG_BLOCK(self->_savedPresentingWindow)
  260. #endif
  261. callback:COPY_TO_ARG_BLOCK(self->_savedAuthorizationCallback)]);
  262. OCMStub([self->_oidAuthorizationService
  263. performTokenRequest:SAVE_TO_ARG_BLOCK(self->_savedTokenRequest)
  264. callback:COPY_TO_ARG_BLOCK(self->_savedTokenCallback)]);
  265. // Fakes
  266. _fetcherService = [[GIDFakeFetcherService alloc] init];
  267. _fakeMainBundle = [[GIDFakeMainBundle alloc] init];
  268. [_fakeMainBundle startFakingWithBundleId:kAppBundleId clientId:kClientId];
  269. [_fakeMainBundle fakeAllSchemesSupported];
  270. // Object under test
  271. [[NSUserDefaults standardUserDefaults] setBool:YES
  272. forKey:kAppHasRunBeforeKey];
  273. _signIn = [[GIDSignIn alloc] initPrivate];
  274. _configuration = [[GIDConfiguration alloc] initWithClientID:kClientId];
  275. _hint = nil;
  276. __weak GIDSignInTest *weakSelf = self;
  277. _callback = ^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) {
  278. GIDSignInTest *strongSelf = weakSelf;
  279. if (!user) {
  280. XCTAssertNotNil(error, @"should have an error if user is nil");
  281. }
  282. XCTAssertFalse(strongSelf->_callbackCalled, @"callback already called");
  283. strongSelf->_callbackCalled = YES;
  284. strongSelf->_authError = error;
  285. };
  286. [_signIn addObserver:self
  287. forKeyPath:NSStringFromSelector(@selector(currentUser))
  288. options:0
  289. context:kTestObserverContext];
  290. }
  291. - (void)tearDown {
  292. OCMVerifyAll(_authState);
  293. OCMVerifyAll(_tokenResponse);
  294. OCMVerifyAll(_tokenRequest);
  295. OCMVerifyAll(_authorization);
  296. OCMVerifyAll(_user);
  297. OCMVerifyAll(_authentication);
  298. OCMVerifyAll(_oidAuthorizationService);
  299. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  300. OCMVerifyAll(_presentingViewController);
  301. #elif TARGET_OS_OSX
  302. OCMVerifyAll(_presentingWindow);
  303. #endif
  304. [_fakeMainBundle stopFaking];
  305. [super tearDown];
  306. [_signIn removeObserver:self
  307. forKeyPath:NSStringFromSelector(@selector(currentUser))
  308. context:kTestObserverContext];
  309. }
  310. #pragma mark - Tests
  311. - (void)testShareInstance {
  312. GIDSignIn *signIn1 = GIDSignIn.sharedInstance;
  313. GIDSignIn *signIn2 = GIDSignIn.sharedInstance;
  314. XCTAssertTrue(signIn1 == signIn2, @"shared instance must be singleton");
  315. }
  316. - (void)testRestorePreviousSignInNoRefresh_hasPreviousUser {
  317. [[[_authorization expect] andReturn:_authState] authState];
  318. OCMStub([_authState lastTokenResponse]).andReturn(_tokenResponse);
  319. OCMStub([_tokenResponse scope]).andReturn(nil);
  320. OCMStub([_tokenResponse additionalParameters]).andReturn(nil);
  321. OCMStub([_tokenResponse idToken]).andReturn(kFakeIDToken);
  322. OCMStub([_tokenResponse request]).andReturn(_tokenRequest);
  323. OCMStub([_tokenRequest additionalParameters]).andReturn(nil);
  324. id idTokenDecoded = OCMClassMock([OIDIDToken class]);
  325. OCMStub([idTokenDecoded alloc]).andReturn(idTokenDecoded);
  326. OCMStub([idTokenDecoded initWithIDTokenString:OCMOCK_ANY]).andReturn(idTokenDecoded);
  327. OCMStub([idTokenDecoded subject]).andReturn(kFakeGaiaID);
  328. [_signIn restorePreviousSignInNoRefresh];
  329. [_authorization verify];
  330. [_authState verify];
  331. [_tokenResponse verify];
  332. XCTAssertEqual(_signIn.currentUser.userID, kFakeGaiaID);
  333. [idTokenDecoded stopMocking];
  334. }
  335. - (void)testRestoredPreviousSignInNoRefresh_hasNoPreviousUser {
  336. [[[_authorization expect] andReturn:nil] authState];
  337. [_signIn restorePreviousSignInNoRefresh];
  338. [_authorization verify];
  339. XCTAssertNil(_signIn.currentUser);
  340. }
  341. - (void)testHasPreviousSignIn_HasBeenAuthenticated {
  342. [[[_authorization expect] andReturn:_authState] authState];
  343. [[[_authState expect] andReturnValue:[NSNumber numberWithBool:YES]] isAuthorized];
  344. XCTAssertTrue([_signIn hasPreviousSignIn], @"should return |YES|");
  345. [_authorization verify];
  346. [_authState verify];
  347. XCTAssertFalse(_keychainRemoved, @"should not remove keychain");
  348. XCTAssertFalse(_callbackCalled, @"should not call delegate");
  349. XCTAssertNil(_authError, @"should have no error");
  350. }
  351. - (void)testHasPreviousSignIn_HasNotBeenAuthenticated {
  352. [[[_authorization expect] andReturn:_authState] authState];
  353. [[[_authState expect] andReturnValue:[NSNumber numberWithBool:NO]] isAuthorized];
  354. XCTAssertFalse([_signIn hasPreviousSignIn], @"should return |NO|");
  355. [_authorization verify];
  356. [_authState verify];
  357. XCTAssertFalse(_keychainRemoved, @"should not remove keychain");
  358. XCTAssertFalse(_callbackCalled, @"should not call delegate");
  359. }
  360. - (void)testRestorePreviousSignInWhenSignedOut {
  361. [[[_authorization expect] andReturn:_authState] authState];
  362. [[[_authState expect] andReturnValue:[NSNumber numberWithBool:NO]] isAuthorized];
  363. _callbackCalled = NO;
  364. _authError = nil;
  365. XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called."];
  366. [_signIn restorePreviousSignInWithCallback:^(GIDGoogleUser * _Nullable user,
  367. NSError * _Nullable error) {
  368. [expectation fulfill];
  369. XCTAssertNotNil(error, @"error should not have been nil");
  370. XCTAssertEqual(error.domain,
  371. kGIDSignInErrorDomain,
  372. @"error domain should have been the sign-in error domain.");
  373. XCTAssertEqual(error.code,
  374. kGIDSignInErrorCodeHasNoAuthInKeychain,
  375. @"error code should have been the 'NoAuthInKeychain' error code.");
  376. }];
  377. [self waitForExpectationsWithTimeout:1 handler:nil];
  378. [_authorization verify];
  379. [_authState verify];
  380. }
  381. - (void)testOAuthLogin {
  382. [self OAuthLoginWithAddScopesFlow:NO
  383. authError:nil
  384. tokenError:nil
  385. emmPasscodeInfoRequired:NO
  386. keychainError:NO
  387. restoredSignIn:NO
  388. oldAccessToken:NO
  389. modalCancel:NO];
  390. }
  391. - (void)testOAuthLogin_RestoredSignIn {
  392. [self OAuthLoginWithAddScopesFlow:NO
  393. authError:nil
  394. tokenError:nil
  395. emmPasscodeInfoRequired:NO
  396. keychainError:NO
  397. restoredSignIn:YES
  398. oldAccessToken:NO
  399. modalCancel:NO];
  400. }
  401. - (void)testOAuthLogin_RestoredSignInOldAccessToken {
  402. [self OAuthLoginWithAddScopesFlow:NO
  403. authError:nil
  404. tokenError:nil
  405. emmPasscodeInfoRequired:NO
  406. keychainError:NO
  407. restoredSignIn:YES
  408. oldAccessToken:YES
  409. modalCancel:NO];
  410. }
  411. - (void)testOAuthLogin_AdditionalScopes {
  412. NSString *expectedScopeString;
  413. [self OAuthLoginWithAddScopesFlow:NO
  414. authError:nil
  415. tokenError:nil
  416. emmPasscodeInfoRequired:NO
  417. keychainError:NO
  418. restoredSignIn:NO
  419. oldAccessToken:NO
  420. modalCancel:NO
  421. useAdditionalScopes:YES
  422. additionalScopes:nil];
  423. expectedScopeString = [@[ @"email", @"profile" ] componentsJoinedByString:@" "];
  424. XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString);
  425. [self OAuthLoginWithAddScopesFlow:NO
  426. authError:nil
  427. tokenError:nil
  428. emmPasscodeInfoRequired:NO
  429. keychainError:NO
  430. restoredSignIn:NO
  431. oldAccessToken:NO
  432. modalCancel:NO
  433. useAdditionalScopes:YES
  434. additionalScopes:@[ kScope ]];
  435. expectedScopeString = [@[ kScope, @"email", @"profile" ] componentsJoinedByString:@" "];
  436. XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString);
  437. [self OAuthLoginWithAddScopesFlow:NO
  438. authError:nil
  439. tokenError:nil
  440. emmPasscodeInfoRequired:NO
  441. keychainError:NO
  442. restoredSignIn:NO
  443. oldAccessToken:NO
  444. modalCancel:NO
  445. useAdditionalScopes:YES
  446. additionalScopes:@[ kScope, kScope2 ]];
  447. expectedScopeString = [@[ kScope, kScope2, @"email", @"profile" ] componentsJoinedByString:@" "];
  448. XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString);
  449. }
  450. - (void)testAddScopes {
  451. // Restore the previous sign-in account. This is the preparation for adding scopes.
  452. [self OAuthLoginWithAddScopesFlow:NO
  453. authError:nil
  454. tokenError:nil
  455. emmPasscodeInfoRequired:NO
  456. keychainError:NO
  457. restoredSignIn:YES
  458. oldAccessToken:NO
  459. modalCancel:NO];
  460. XCTAssertNotNil(_signIn.currentUser);
  461. id profile = OCMStrictClassMock([GIDProfileData class]);
  462. OCMStub([profile email]).andReturn(kUserEmail);
  463. OCMStub([_user authentication]).andReturn(_authentication);
  464. OCMStub([_authentication clientID]).andReturn(kClientId);
  465. OCMStub([_user serverClientID]).andReturn(nil);
  466. OCMStub([_user hostedDomain]).andReturn(nil);
  467. OCMStub([_user openIDRealm]).andReturn(kOpenIDRealm);
  468. OCMStub([_user profile]).andReturn(profile);
  469. OCMStub([_user grantedScopes]).andReturn(@[kGrantedScope]);
  470. [self OAuthLoginWithAddScopesFlow:YES
  471. authError:nil
  472. tokenError:nil
  473. emmPasscodeInfoRequired:NO
  474. keychainError:NO
  475. restoredSignIn:NO
  476. oldAccessToken:NO
  477. modalCancel:NO];
  478. NSArray<NSString *> *grantedScopes;
  479. NSString *grantedScopeString = _savedAuthorizationRequest.scope;
  480. if (grantedScopeString) {
  481. grantedScopeString = [grantedScopeString stringByTrimmingCharactersInSet:
  482. [NSCharacterSet whitespaceCharacterSet]];
  483. // Tokenize with space as a delimiter.
  484. NSMutableArray<NSString *> *parsedScopes =
  485. [[grantedScopeString componentsSeparatedByString:@" "] mutableCopy];
  486. // Remove empty strings.
  487. [parsedScopes removeObject:@""];
  488. grantedScopes = [parsedScopes copy];
  489. }
  490. NSArray<NSString *> *expectedScopes = @[kNewScope, kGrantedScope];
  491. XCTAssertEqualObjects(grantedScopes, expectedScopes);
  492. [profile stopMocking];
  493. }
  494. - (void)testOpenIDRealm {
  495. _configuration = [[GIDConfiguration alloc] initWithClientID:kClientId
  496. serverClientID:nil
  497. hostedDomain:nil
  498. openIDRealm:kOpenIDRealm];
  499. [self OAuthLoginWithAddScopesFlow:NO
  500. authError:nil
  501. tokenError:nil
  502. emmPasscodeInfoRequired:NO
  503. keychainError:NO
  504. restoredSignIn:NO
  505. oldAccessToken:NO
  506. modalCancel:NO];
  507. NSDictionary<NSString *, NSString *> *params = _savedTokenRequest.additionalParameters;
  508. XCTAssertEqual(params[kOpenIDRealmKey], kOpenIDRealm, @"OpenID Realm should match.");
  509. }
  510. - (void)testOAuthLogin_LoginHint {
  511. _hint = kUserEmail;
  512. [self OAuthLoginWithAddScopesFlow:NO
  513. authError:nil
  514. tokenError:nil
  515. emmPasscodeInfoRequired:NO
  516. keychainError:NO
  517. restoredSignIn:NO
  518. oldAccessToken:NO
  519. modalCancel:NO];
  520. NSDictionary<NSString *, NSObject *> *params = _savedAuthorizationRequest.additionalParameters;
  521. XCTAssertEqualObjects(params[@"login_hint"], kUserEmail, @"login hint should match");
  522. }
  523. - (void)testOAuthLogin_HostedDomain {
  524. _configuration = [[GIDConfiguration alloc] initWithClientID:kClientId
  525. serverClientID:nil
  526. hostedDomain:kHostedDomain
  527. openIDRealm:nil];
  528. [self OAuthLoginWithAddScopesFlow:NO
  529. authError:nil
  530. tokenError:nil
  531. emmPasscodeInfoRequired:NO
  532. keychainError:NO
  533. restoredSignIn:NO
  534. oldAccessToken:NO
  535. modalCancel:NO];
  536. NSDictionary<NSString *, NSObject *> *params = _savedAuthorizationRequest.additionalParameters;
  537. XCTAssertEqualObjects(params[@"hd"], kHostedDomain, @"hosted domain should match");
  538. }
  539. - (void)testOAuthLogin_ConsentCanceled {
  540. [self OAuthLoginWithAddScopesFlow:NO
  541. authError:@"access_denied"
  542. tokenError:nil
  543. emmPasscodeInfoRequired:NO
  544. keychainError:NO
  545. restoredSignIn:NO
  546. oldAccessToken:NO
  547. modalCancel:NO];
  548. [self waitForExpectationsWithTimeout:1 handler:nil];
  549. XCTAssertTrue(_callbackCalled, @"should call delegate");
  550. XCTAssertEqual(_authError.code, kGIDSignInErrorCodeCanceled);
  551. }
  552. - (void)testOAuthLogin_ModalCanceled {
  553. [self OAuthLoginWithAddScopesFlow:NO
  554. authError:nil
  555. tokenError:nil
  556. emmPasscodeInfoRequired:NO
  557. keychainError:NO
  558. restoredSignIn:NO
  559. oldAccessToken:NO
  560. modalCancel:YES];
  561. [self waitForExpectationsWithTimeout:1 handler:nil];
  562. XCTAssertTrue(_callbackCalled, @"should call delegate");
  563. XCTAssertEqual(_authError.code, kGIDSignInErrorCodeCanceled);
  564. }
  565. - (void)testOAuthLogin_KeychainError {
  566. [self OAuthLoginWithAddScopesFlow:NO
  567. authError:nil
  568. tokenError:nil
  569. emmPasscodeInfoRequired:NO
  570. keychainError:YES
  571. restoredSignIn:NO
  572. oldAccessToken:NO
  573. modalCancel:NO];
  574. [self waitForExpectationsWithTimeout:1 handler:nil];
  575. XCTAssertFalse(_keychainSaved, @"should save to keychain");
  576. XCTAssertTrue(_callbackCalled, @"should call delegate");
  577. XCTAssertEqualObjects(_authError.domain, kGIDSignInErrorDomain);
  578. XCTAssertEqual(_authError.code, kGIDSignInErrorCodeKeychain);
  579. }
  580. - (void)testSignOut {
  581. // Sign in a user so that we can then sign them out.
  582. [self OAuthLoginWithAddScopesFlow:NO
  583. authError:nil
  584. tokenError:nil
  585. emmPasscodeInfoRequired:NO
  586. keychainError:NO
  587. restoredSignIn:YES
  588. oldAccessToken:NO
  589. modalCancel:NO];
  590. XCTAssertNotNil(_signIn.currentUser);
  591. [_signIn signOut];
  592. XCTAssertNil(_signIn.currentUser, @"should not have a current user");
  593. XCTAssertTrue(_keychainRemoved, @"should remove keychain");
  594. XCTAssertTrue([_changedKeyPaths containsObject:NSStringFromSelector(@selector(currentUser))],
  595. @"should notify observers that signed in user changed");
  596. }
  597. - (void)testNotHandleWrongScheme {
  598. XCTAssertFalse([_signIn handleURL:[NSURL URLWithString:kWrongSchemeURL]],
  599. @"should not handle URL");
  600. XCTAssertFalse(_keychainSaved, @"should not save to keychain");
  601. XCTAssertFalse(_callbackCalled, @"should not call delegate");
  602. }
  603. - (void)testNotHandleWrongPath {
  604. XCTAssertFalse([_signIn handleURL:[NSURL URLWithString:kWrongPathURL]], @"should not handle URL");
  605. XCTAssertFalse(_keychainSaved, @"should not save to keychain");
  606. XCTAssertFalse(_callbackCalled, @"should not call delegate");
  607. }
  608. #pragma mark - Tests - disconnectWithCallback:
  609. // Verifies disconnect calls callback with no errors if access token is present.
  610. - (void)testDisconnect_accessToken {
  611. [[[_authorization expect] andReturn:_authState] authState];
  612. [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse];
  613. [[[_tokenResponse expect] andReturn:kAccessToken] accessToken];
  614. [[[_authorization expect] andReturn:_fetcherService] fetcherService];
  615. XCTestExpectation *expectation =
  616. [self expectationWithDescription:@"Callback called with nil error"];
  617. [_signIn disconnectWithCallback:^(NSError * _Nullable error) {
  618. if (error == nil) {
  619. [expectation fulfill];
  620. }
  621. }];
  622. [self verifyAndRevokeToken:kAccessToken hasCallback:YES];
  623. [_authorization verify];
  624. [_authState verify];
  625. [_tokenResponse verify];
  626. }
  627. // Verifies disconnect if access token is present.
  628. - (void)testDisconnectNoCallback_accessToken {
  629. [[[_authorization expect] andReturn:_authState] authState];
  630. [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse];
  631. [[[_tokenResponse expect] andReturn:kAccessToken] accessToken];
  632. [[[_authorization expect] andReturn:_fetcherService] fetcherService];
  633. [_signIn disconnectWithCallback:nil];
  634. [self verifyAndRevokeToken:kAccessToken hasCallback:NO];
  635. [_authorization verify];
  636. [_authState verify];
  637. [_tokenResponse verify];
  638. }
  639. // Verifies disconnect calls callback with no errors if refresh token is present.
  640. - (void)testDisconnect_refreshToken {
  641. [[[_authorization expect] andReturn:_authState] authState];
  642. [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse];
  643. [[[_tokenResponse expect] andReturn:nil] accessToken];
  644. [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse];
  645. [[[_tokenResponse expect] andReturn:kRefreshToken] refreshToken];
  646. [[[_authorization expect] andReturn:_fetcherService] fetcherService];
  647. XCTestExpectation *expectation =
  648. [self expectationWithDescription:@"Callback called with nil error"];
  649. [_signIn disconnectWithCallback:^(NSError * _Nullable error) {
  650. if (error == nil) {
  651. [expectation fulfill];
  652. }
  653. }];
  654. [self verifyAndRevokeToken:kRefreshToken hasCallback:YES];
  655. [_authorization verify];
  656. [_authState verify];
  657. [_tokenResponse verify];
  658. }
  659. // Verifies disconnect errors are passed along to the callback.
  660. - (void)testDisconnect_errors {
  661. [[[_authorization expect] andReturn:_authState] authState];
  662. [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse];
  663. [[[_tokenResponse expect] andReturn:kAccessToken] accessToken];
  664. [[[_authorization expect] andReturn:_fetcherService] fetcherService];
  665. XCTestExpectation *expectation =
  666. [self expectationWithDescription:@"Callback called with an error"];
  667. [_signIn disconnectWithCallback:^(NSError * _Nullable error) {
  668. if (error != nil) {
  669. [expectation fulfill];
  670. }
  671. }];
  672. XCTAssertTrue([self isFetcherStarted], @"should start fetching");
  673. // Emulate result back from server.
  674. NSError *error = [self error];
  675. [self didFetch:nil error:error];
  676. [self waitForExpectationsWithTimeout:1 handler:nil];
  677. [_authorization verify];
  678. [_authState verify];
  679. [_tokenResponse verify];
  680. }
  681. // Verifies disconnect with errors
  682. - (void)testDisconnectNoCallback_errors {
  683. [[[_authorization expect] andReturn:_authState] authState];
  684. [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse];
  685. [[[_tokenResponse expect] andReturn:kAccessToken] accessToken];
  686. [[[_authorization expect] andReturn:_fetcherService] fetcherService];
  687. [_signIn disconnectWithCallback:nil];
  688. XCTAssertTrue([self isFetcherStarted], @"should start fetching");
  689. // Emulate result back from server.
  690. NSError *error = [self error];
  691. [self didFetch:nil error:error];
  692. [_authorization verify];
  693. [_authState verify];
  694. [_tokenResponse verify];
  695. }
  696. // Verifies disconnect calls callback with no errors and clears keychain if no tokens are present.
  697. - (void)testDisconnect_noTokens {
  698. [[[_authorization expect] andReturn:_authState] authState];
  699. [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse];
  700. [[[_tokenResponse expect] andReturn:nil] accessToken];
  701. [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse];
  702. [[[_tokenResponse expect] andReturn:nil] refreshToken];
  703. XCTestExpectation *expectation =
  704. [self expectationWithDescription:@"Callback called with nil error"];
  705. [_signIn disconnectWithCallback:^(NSError * _Nullable error) {
  706. if (error == nil) {
  707. [expectation fulfill];
  708. }
  709. }];
  710. [self waitForExpectationsWithTimeout:1 handler:nil];
  711. XCTAssertFalse([self isFetcherStarted], @"should not fetch");
  712. XCTAssertTrue(_keychainRemoved, @"keychain should be removed");
  713. [_authorization verify];
  714. [_authState verify];
  715. [_tokenResponse verify];
  716. }
  717. // Verifies disconnect clears keychain if no tokens are present.
  718. - (void)testDisconnectNoCallback_noTokens {
  719. [[[_authorization expect] andReturn:_authState] authState];
  720. [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse];
  721. [[[_tokenResponse expect] andReturn:nil] accessToken];
  722. [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse];
  723. [[[_tokenResponse expect] andReturn:nil] refreshToken];
  724. [_signIn disconnectWithCallback:nil];
  725. XCTAssertFalse([self isFetcherStarted], @"should not fetch");
  726. XCTAssertTrue(_keychainRemoved, @"keychain should be removed");
  727. [_authorization verify];
  728. [_authState verify];
  729. [_tokenResponse verify];
  730. }
  731. - (void)testPresentingViewControllerException {
  732. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  733. _presentingViewController = nil;
  734. #elif TARGET_OS_OSX
  735. _presentingWindow = nil;
  736. #endif
  737. XCTAssertThrows([_signIn signInWithConfiguration:_configuration
  738. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  739. presentingViewController:_presentingViewController
  740. #elif TARGET_OS_OSX
  741. presentingWindow:_presentingWindow
  742. #endif
  743. hint:_hint
  744. callback:_callback]);
  745. }
  746. - (void)testClientIDMissingException {
  747. #pragma GCC diagnostic push
  748. #pragma GCC diagnostic ignored "-Wnonnull"
  749. GIDConfiguration *configuration = [[GIDConfiguration alloc] initWithClientID:nil];
  750. #pragma GCC diagnostic pop
  751. BOOL threw = NO;
  752. @try {
  753. [_signIn signInWithConfiguration:configuration
  754. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  755. presentingViewController:_presentingViewController
  756. #elif TARGET_OS_OSX
  757. presentingWindow:_presentingWindow
  758. #endif
  759. callback:nil];
  760. } @catch (NSException *exception) {
  761. threw = YES;
  762. XCTAssertEqualObjects(exception.description,
  763. @"You must specify |clientID| in |GIDConfiguration|");
  764. } @finally {
  765. }
  766. XCTAssert(threw);
  767. }
  768. - (void)testSchemesNotSupportedException {
  769. [_fakeMainBundle fakeMissingAllSchemes];
  770. BOOL threw = NO;
  771. @try {
  772. [_signIn signInWithConfiguration:_configuration
  773. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  774. presentingViewController:_presentingViewController
  775. #elif TARGET_OS_OSX
  776. presentingWindow:_presentingWindow
  777. #endif
  778. hint:_hint
  779. callback:_callback];
  780. } @catch (NSException *exception) {
  781. threw = YES;
  782. XCTAssertEqualObjects(exception.description,
  783. @"Your app is missing support for the following URL schemes: "
  784. "fakeclientid");
  785. } @finally {
  786. }
  787. XCTAssert(threw);
  788. }
  789. #pragma mark - Restarting Authentication Tests
  790. // Verifies that URL is not handled if there is no pending sign-in
  791. - (void)testRequiringPendingSignIn {
  792. BOOL result = [_signIn handleURL:[NSURL URLWithString:kEMMRestartAuthURL]];
  793. XCTAssertFalse(result);
  794. }
  795. #pragma mark - EMM tests
  796. #if TARGET_OS_IOS
  797. - (void)testEmmSupportRequestParameters {
  798. [self OAuthLoginWithAddScopesFlow:NO
  799. authError:nil
  800. tokenError:nil
  801. emmPasscodeInfoRequired:NO
  802. keychainError:NO
  803. restoredSignIn:NO
  804. oldAccessToken:NO
  805. modalCancel:NO];
  806. NSString *systemName = [UIDevice currentDevice].systemName;
  807. if ([systemName isEqualToString:@"iPhone OS"]) {
  808. systemName = @"iOS";
  809. }
  810. NSString *expectedOSVersion = [NSString stringWithFormat:@"%@ %@",
  811. systemName, [UIDevice currentDevice].systemVersion];
  812. NSDictionary<NSString *, NSObject *> *authParams =
  813. _savedAuthorizationRequest.additionalParameters;
  814. NSDictionary<NSString *, NSString *> *tokenParams = _savedTokenRequest.additionalParameters;
  815. if (_isEligibleForEMM) {
  816. XCTAssertEqualObjects(authParams[@"emm_support"], kEMMSupport,
  817. @"EMM support should match in auth request");
  818. XCTAssertEqualObjects(authParams[@"device_os"], expectedOSVersion,
  819. @"OS version should match in auth request");
  820. XCTAssertEqualObjects(tokenParams[@"emm_support"], kEMMSupport,
  821. @"EMM support should match in token request");
  822. XCTAssertEqualObjects(tokenParams[@"device_os"],
  823. expectedOSVersion,
  824. @"OS version should match in token request");
  825. XCTAssertNil(tokenParams[@"emm_passcode_info"],
  826. @"no passcode info should be in token request");
  827. } else {
  828. XCTAssertNil(authParams[@"emm_support"],
  829. @"EMM support should not be in auth request for unsupported OS");
  830. XCTAssertNil(authParams[@"device_os"],
  831. @"OS version should not be in auth request for unsupported OS");
  832. XCTAssertNil(tokenParams[@"emm_support"],
  833. @"EMM support should not be in token request for unsupported OS");
  834. XCTAssertNil(tokenParams[@"device_os"],
  835. @"OS version should not be in token request for unsupported OS");
  836. XCTAssertNil(tokenParams[@"emm_passcode_info"],
  837. @"passcode info should not be in token request for unsupported OS");
  838. }
  839. }
  840. - (void)testEmmPasscodeInfo {
  841. [self OAuthLoginWithAddScopesFlow:NO
  842. authError:nil
  843. tokenError:nil
  844. emmPasscodeInfoRequired:YES
  845. keychainError:NO
  846. restoredSignIn:NO
  847. oldAccessToken:NO
  848. modalCancel:NO];
  849. NSDictionary<NSString *, NSString *> *tokenParams = _savedTokenRequest.additionalParameters;
  850. if (_isEligibleForEMM) {
  851. XCTAssertNotNil(tokenParams[@"emm_passcode_info"],
  852. @"passcode info should be in token request");
  853. } else {
  854. XCTAssertNil(tokenParams[@"emm_passcode_info"],
  855. @"passcode info should not be in token request for unsupported OS");
  856. }
  857. }
  858. - (void)testAuthEndpointEMMError {
  859. if (!_isEligibleForEMM) {
  860. return;
  861. }
  862. id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]);
  863. [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance];
  864. __block void (^completion)(void);
  865. NSDictionary<NSString *, NSString *> *callbackParams = @{ @"error" : @"EMM Specific Error" };
  866. [[[mockEMMErrorHandler expect] andReturnValue:@YES]
  867. handleErrorFromResponse:callbackParams completion:SAVE_TO_ARG_BLOCK(completion)];
  868. [self OAuthLoginWithAddScopesFlow:NO
  869. authError:callbackParams[@"error"]
  870. tokenError:nil
  871. emmPasscodeInfoRequired:NO
  872. keychainError:NO
  873. restoredSignIn:NO
  874. oldAccessToken:NO
  875. modalCancel:NO];
  876. [mockEMMErrorHandler verify];
  877. [mockEMMErrorHandler stopMocking];
  878. completion();
  879. [self waitForExpectationsWithTimeout:1 handler:nil];
  880. XCTAssertFalse(_keychainSaved, @"should not save to keychain");
  881. XCTAssertTrue(_callbackCalled, @"should call delegate");
  882. XCTAssertNotNil(_authError, @"should have error");
  883. XCTAssertEqualObjects(_authError.domain, kGIDSignInErrorDomain);
  884. XCTAssertEqual(_authError.code, kGIDSignInErrorCodeEMM);
  885. XCTAssertNil(_signIn.currentUser, @"should not have current user");
  886. }
  887. - (void)testTokenEndpointEMMError {
  888. if (!_isEligibleForEMM) {
  889. return;
  890. }
  891. __block void (^completion)(NSError *);
  892. NSDictionary *errorJSON = @{ @"error" : @"EMM Specific Error" };
  893. NSError *emmError = [NSError errorWithDomain:@"anydomain"
  894. code:12345
  895. userInfo:@{ OIDOAuthErrorFieldError : errorJSON }];
  896. [[_authentication expect] handleTokenFetchEMMError:emmError
  897. completion:SAVE_TO_ARG_BLOCK(completion)];
  898. [self OAuthLoginWithAddScopesFlow:NO
  899. authError:nil
  900. tokenError:emmError
  901. emmPasscodeInfoRequired:NO
  902. keychainError:NO
  903. restoredSignIn:NO
  904. oldAccessToken:NO
  905. modalCancel:NO];
  906. NSError *handledError = [NSError errorWithDomain:kGIDSignInErrorDomain
  907. code:kGIDSignInErrorCodeEMM
  908. userInfo:emmError.userInfo];
  909. completion(handledError);
  910. [self waitForExpectationsWithTimeout:1 handler:nil];
  911. [_authentication verify];
  912. XCTAssertFalse(_keychainSaved, @"should not save to keychain");
  913. XCTAssertTrue(_callbackCalled, @"should call delegate");
  914. XCTAssertNotNil(_authError, @"should have error");
  915. XCTAssertEqualObjects(_authError.domain, kGIDSignInErrorDomain);
  916. XCTAssertEqual(_authError.code, kGIDSignInErrorCodeEMM);
  917. XCTAssertNil(_signIn.currentUser, @"should not have current user");
  918. }
  919. #endif
  920. #pragma mark - Helpers
  921. // Whether or not a fetcher has been started.
  922. - (BOOL)isFetcherStarted {
  923. NSUInteger count = _fetcherService.fetchers.count;
  924. XCTAssertTrue(count <= 1, @"Only one fetcher is supported");
  925. return !!count;
  926. }
  927. // Gets the URL being fetched.
  928. - (NSURL *)fetchedURL {
  929. return [_fetcherService.fetchers[0] requestURL];
  930. }
  931. // Emulates server returning the data as in JSON.
  932. - (void)didFetch:(id)dataObject error:(NSError *)error {
  933. NSData *data = nil;
  934. if (dataObject) {
  935. NSError *jsonError = nil;
  936. data = [NSJSONSerialization dataWithJSONObject:dataObject
  937. options:0
  938. error:&jsonError];
  939. XCTAssertNil(jsonError, @"must provide valid data");
  940. }
  941. [_fetcherService.fetchers[0] didFinishWithData:data error:error];
  942. }
  943. - (NSError *)error {
  944. return [NSError errorWithDomain:kErrorDomain code:kErrorCode userInfo:nil];
  945. }
  946. // Verifies a fetcher has started for revoking token and emulates a server response.
  947. - (void)verifyAndRevokeToken:(NSString *)token hasCallback:(BOOL)hasCallback {
  948. XCTAssertTrue([self isFetcherStarted], @"should start fetching");
  949. NSURL *url = [self fetchedURL];
  950. XCTAssertEqualObjects([url scheme], @"https", @"scheme must match");
  951. XCTAssertEqualObjects([url host], @"accounts.google.com", @"host must match");
  952. XCTAssertEqualObjects([url path], @"/o/oauth2/revoke", @"path must match");
  953. OIDURLQueryComponent *queryComponent = [[OIDURLQueryComponent alloc] initWithURL:url];
  954. NSDictionary<NSString *, NSObject<NSCopying> *> *params = queryComponent.dictionaryValue;
  955. XCTAssertEqualObjects([params valueForKey:@"token"], token,
  956. @"token parameter should match");
  957. // Emulate result back from server.
  958. [self didFetch:nil error:nil];
  959. if (hasCallback) {
  960. [self waitForExpectationsWithTimeout:1 handler:nil];
  961. }
  962. XCTAssertTrue(_keychainRemoved, @"should clear saved keychain name");
  963. }
  964. - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow
  965. authError:(NSString *)authError
  966. tokenError:(NSError *)tokenError
  967. emmPasscodeInfoRequired:(BOOL)emmPasscodeInfoRequired
  968. keychainError:(BOOL)keychainError
  969. restoredSignIn:(BOOL)restoredSignIn
  970. oldAccessToken:(BOOL)oldAccessToken
  971. modalCancel:(BOOL)modalCancel {
  972. [self OAuthLoginWithAddScopesFlow:addScopesFlow
  973. authError:authError
  974. tokenError:tokenError
  975. emmPasscodeInfoRequired:emmPasscodeInfoRequired
  976. keychainError:keychainError
  977. restoredSignIn:restoredSignIn
  978. oldAccessToken:oldAccessToken
  979. modalCancel:modalCancel
  980. useAdditionalScopes:NO
  981. additionalScopes:nil];
  982. }
  983. // The authorization flow with parameters to control which branches to take.
  984. - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow
  985. authError:(NSString *)authError
  986. tokenError:(NSError *)tokenError
  987. emmPasscodeInfoRequired:(BOOL)emmPasscodeInfoRequired
  988. keychainError:(BOOL)keychainError
  989. restoredSignIn:(BOOL)restoredSignIn
  990. oldAccessToken:(BOOL)oldAccessToken
  991. modalCancel:(BOOL)modalCancel
  992. useAdditionalScopes:(BOOL)useAdditionalScopes
  993. additionalScopes:(NSArray *)additionalScopes {
  994. if (restoredSignIn) {
  995. // clearAndAuthenticateWithOptions
  996. [[[_authorization expect] andReturn:_authState] authState];
  997. BOOL isAuthorized = restoredSignIn ? YES : NO;
  998. [[[_authState expect] andReturnValue:[NSNumber numberWithBool:isAuthorized]] isAuthorized];
  999. }
  1000. NSDictionary<NSString *, NSString *> *additionalParameters = emmPasscodeInfoRequired ?
  1001. @{ @"emm_passcode_info_required" : @"1" } : nil;
  1002. OIDAuthorizationResponse *authResponse =
  1003. [OIDAuthorizationResponse testInstanceWithAdditionalParameters:additionalParameters
  1004. errorString:authError];
  1005. OIDTokenResponse *tokenResponse =
  1006. [OIDTokenResponse testInstanceWithIDToken:[OIDTokenResponse fatIDToken]
  1007. accessToken:restoredSignIn ? kAccessToken : nil
  1008. expiresIn:oldAccessToken ? @(300) : nil
  1009. tokenRequest:nil];
  1010. OIDTokenRequest *tokenRequest = [[OIDTokenRequest alloc]
  1011. initWithConfiguration:authResponse.request.configuration
  1012. grantType:OIDGrantTypeRefreshToken
  1013. authorizationCode:nil
  1014. redirectURL:nil
  1015. clientID:authResponse.request.clientID
  1016. clientSecret:authResponse.request.clientSecret
  1017. scope:nil
  1018. refreshToken:kRefreshToken
  1019. codeVerifier:nil
  1020. additionalParameters:tokenResponse.request.additionalParameters];
  1021. if (restoredSignIn) {
  1022. // maybeFetchToken
  1023. [[[_authState expect] andReturn:tokenResponse] lastTokenResponse];
  1024. [[[_authState expect] andReturn:tokenResponse] lastTokenResponse];
  1025. if (oldAccessToken) {
  1026. #if TARGET_OS_IOS
  1027. // Corresponds to EMM support
  1028. [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse];
  1029. #endif
  1030. [[[_authState expect] andReturn:tokenResponse] lastTokenResponse];
  1031. [[[_authState expect] andReturn:tokenResponse] lastTokenResponse];
  1032. [[[_authState expect] andReturn:tokenRequest]
  1033. tokenRefreshRequestWithAdditionalParameters:[OCMArg any]];
  1034. }
  1035. } else {
  1036. XCTestExpectation *expectation = [self expectationWithDescription:@"Callback called"];
  1037. GIDSignInCallback callback = ^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) {
  1038. [expectation fulfill];
  1039. if (!user) {
  1040. XCTAssertNotNil(error, @"should have an error if user is nil");
  1041. }
  1042. XCTAssertFalse(self->_callbackCalled, @"callback already called");
  1043. self->_callbackCalled = YES;
  1044. self->_authError = error;
  1045. };
  1046. if (addScopesFlow) {
  1047. [_signIn addScopes:@[kNewScope]
  1048. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  1049. presentingViewController:_presentingViewController
  1050. #elif TARGET_OS_OSX
  1051. presentingWindow:_presentingWindow
  1052. #endif
  1053. callback:callback];
  1054. } else {
  1055. if (useAdditionalScopes) {
  1056. [_signIn signInWithConfiguration:_configuration
  1057. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  1058. presentingViewController:_presentingViewController
  1059. #elif TARGET_OS_OSX
  1060. presentingWindow:_presentingWindow
  1061. #endif
  1062. hint:_hint
  1063. additionalScopes:additionalScopes
  1064. callback:callback];
  1065. } else {
  1066. [_signIn signInWithConfiguration:_configuration
  1067. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  1068. presentingViewController:_presentingViewController
  1069. #elif TARGET_OS_OSX
  1070. presentingWindow:_presentingWindow
  1071. #endif
  1072. hint:_hint
  1073. callback:callback];
  1074. }
  1075. }
  1076. [_authorization verify];
  1077. [_authState verify];
  1078. XCTAssertNotNil(_savedAuthorizationRequest);
  1079. NSDictionary<NSString *, NSObject *> *params = _savedAuthorizationRequest.additionalParameters;
  1080. XCTAssertEqualObjects(params[@"include_granted_scopes"], @"true");
  1081. XCTAssertNotNil(_savedAuthorizationCallback);
  1082. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  1083. XCTAssertEqual(_savedPresentingViewController, _presentingViewController);
  1084. #elif TARGET_OS_OSX
  1085. XCTAssertEqual(_savedPresentingWindow, _presentingWindow);
  1086. #endif
  1087. // maybeFetchToken
  1088. if (!(authError || modalCancel)) {
  1089. [[[_authState expect] andReturn:nil] lastTokenResponse];
  1090. #if TARGET_OS_IOS
  1091. // Corresponds to EMM support
  1092. [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse];
  1093. #endif
  1094. [[[_authState expect] andReturn:nil] lastTokenResponse];
  1095. [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse];
  1096. [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse];
  1097. }
  1098. // Simulate auth endpoint response
  1099. if (modalCancel) {
  1100. NSError *error = [NSError errorWithDomain:OIDGeneralErrorDomain
  1101. code:OIDErrorCodeUserCanceledAuthorizationFlow
  1102. userInfo:nil];
  1103. _savedAuthorizationCallback(nil, error);
  1104. } else {
  1105. _savedAuthorizationCallback(authResponse, nil);
  1106. }
  1107. if (authError || modalCancel) {
  1108. return;
  1109. }
  1110. [_authState verify];
  1111. }
  1112. if (restoredSignIn && oldAccessToken) {
  1113. XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called"];
  1114. [_signIn restorePreviousSignInWithCallback:^(GIDGoogleUser * _Nullable user,
  1115. NSError * _Nullable error) {
  1116. [expectation fulfill];
  1117. XCTAssertNil(error, @"should have no error");
  1118. }];
  1119. }
  1120. if (!restoredSignIn || (restoredSignIn && oldAccessToken)) {
  1121. XCTAssertNotNil(_savedTokenRequest);
  1122. XCTAssertNotNil(_savedTokenCallback);
  1123. // OIDTokenCallback
  1124. if (tokenError) {
  1125. [[_authState expect] updateWithTokenResponse:nil error:tokenError];
  1126. } else {
  1127. [[_authState expect] updateWithTokenResponse:[OCMArg any] error:nil];
  1128. }
  1129. }
  1130. if (tokenError) {
  1131. _savedTokenCallback(nil, tokenError);
  1132. return;
  1133. }
  1134. // DecodeIdTokenCallback
  1135. [[[_authState expect] andReturn:tokenResponse] lastTokenResponse];
  1136. // SaveAuthCallback
  1137. __block OIDAuthState *authState;
  1138. __block GIDProfileData *profileData;
  1139. if (keychainError) {
  1140. _saveAuthorizationReturnValue = NO;
  1141. } else {
  1142. if (addScopesFlow) {
  1143. [[_user expect] updateAuthState:SAVE_TO_ARG_BLOCK(authState)
  1144. profileData:SAVE_TO_ARG_BLOCK(profileData)];
  1145. } else {
  1146. [[[_user stub] andReturn:_user] alloc];
  1147. (void)[[[_user expect] andReturn:_user] initWithAuthState:SAVE_TO_ARG_BLOCK(authState)
  1148. profileData:SAVE_TO_ARG_BLOCK(profileData)];
  1149. }
  1150. }
  1151. if (restoredSignIn && !oldAccessToken) {
  1152. XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called"];
  1153. [_signIn restorePreviousSignInWithCallback:^(GIDGoogleUser * _Nullable user,
  1154. NSError * _Nullable error) {
  1155. [expectation fulfill];
  1156. XCTAssertNil(error, @"should have no error");
  1157. }];
  1158. } else {
  1159. // Simulate token endpoint response.
  1160. _savedTokenCallback(tokenResponse, nil);
  1161. }
  1162. [_authState verify];
  1163. if (keychainError) {
  1164. return;
  1165. }
  1166. [self waitForExpectationsWithTimeout:1 handler:nil];
  1167. XCTAssertTrue(_keychainSaved, @"should save to keychain");
  1168. XCTAssertNotNil(authState);
  1169. // Check fat ID token decoding
  1170. XCTAssertEqualObjects(profileData.name, kFatName);
  1171. XCTAssertEqualObjects(profileData.givenName, kFatGivenName);
  1172. XCTAssertEqualObjects(profileData.familyName, kFatFamilyName);
  1173. XCTAssertTrue(profileData.hasImage);
  1174. // If attempt to authenticate again, will reuse existing auth object.
  1175. _callbackCalled = NO;
  1176. _keychainRemoved = NO;
  1177. _keychainSaved = NO;
  1178. _authError = nil;
  1179. if (!addScopesFlow) {
  1180. [[[_user expect] andReturn:_authentication] authentication];
  1181. [[[_user expect] andReturn:_authentication] authentication];
  1182. }
  1183. __block GIDAuthenticationAction action;
  1184. [[_authentication expect] doWithFreshTokens:SAVE_TO_ARG_BLOCK(action)];
  1185. XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called"];
  1186. [_signIn restorePreviousSignInWithCallback:^(GIDGoogleUser * _Nullable user,
  1187. NSError * _Nullable error) {
  1188. [expectation fulfill];
  1189. XCTAssertNil(error, @"should have no error");
  1190. }];
  1191. action(_authentication, nil);
  1192. [self waitForExpectationsWithTimeout:1 handler:nil];
  1193. XCTAssertFalse(_keychainRemoved, @"should not remove keychain");
  1194. XCTAssertFalse(_keychainSaved, @"should not save to keychain again");
  1195. }
  1196. #pragma mark - Key Value Observing
  1197. - (void)observeValueForKeyPath:(NSString *)keyPath
  1198. ofObject:(id)object
  1199. change:(NSDictionary<NSKeyValueChangeKey, id> *)change
  1200. context:(void *)context {
  1201. if (context == kTestObserverContext && object == _signIn) {
  1202. [_changedKeyPaths addObject:keyPath];
  1203. }
  1204. }
  1205. @end