GIDAuthenticationTest.m 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  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 <XCTest/XCTest.h>
  15. #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h"
  16. #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
  17. #import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
  18. #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
  19. #import "GoogleSignIn/Sources/GIDMDMPasscodeState.h"
  20. #import "GoogleSignIn/Sources/GIDSignInPreferences.h"
  21. #import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h"
  22. #import "GoogleSignIn/Tests/Unit/OIDTokenRequest+Testing.h"
  23. #import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h"
  24. #import <AppAuth/OIDAuthState.h>
  25. #import <AppAuth/OIDAuthorizationRequest.h>
  26. #import <AppAuth/OIDAuthorizationResponse.h>
  27. #import <AppAuth/OIDAuthorizationService.h>
  28. #import <AppAuth/OIDError.h>
  29. #import <AppAuth/OIDIDToken.h>
  30. #import <AppAuth/OIDServiceConfiguration.h>
  31. #import <AppAuth/OIDTokenRequest.h>
  32. #import <AppAuth/OIDTokenResponse.h>
  33. #import <GoogleUtilities/GULSwizzler.h>
  34. #import <GoogleUtilities/GULSwizzler+Unswizzle.h>
  35. #import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
  36. #import <GTMSessionFetcher/GTMSessionFetcher.h>
  37. #import <OCMock/OCMock.h>
  38. static NSString *const kClientID = @"87654321.googleusercontent.com";
  39. static NSString *const kNewAccessToken = @"new_access_token";
  40. static NSString *const kUserEmail = @"foo@gmail.com";
  41. static NSTimeInterval const kExpireTime = 442886117;
  42. static NSTimeInterval const kNewExpireTime = 442886123;
  43. static NSTimeInterval const kNewExpireTime2 = 442886124;
  44. static NSTimeInterval const kTimeAccuracy = 10;
  45. // The system name in old iOS versions.
  46. static NSString *const kOldIOSName = @"iPhone OS";
  47. // The system name in new iOS versions.
  48. static NSString *const kNewIOSName = @"iOS";
  49. // List of observed properties of the class being tested.
  50. static NSString *const kObservedProperties[] = {
  51. @"accessToken",
  52. @"accessTokenExpirationDate",
  53. @"idToken",
  54. @"idTokenExpirationDate"
  55. };
  56. static const NSUInteger kNumberOfObservedProperties =
  57. sizeof(kObservedProperties) / sizeof(*kObservedProperties);
  58. // Bit position for notification change type bitmask flags.
  59. // Must match the list of observed properties above.
  60. typedef NS_ENUM(NSUInteger, ChangeType) {
  61. kChangeTypeAccessTokenPrior,
  62. kChangeTypeAccessToken,
  63. kChangeTypeAccessTokenExpirationDatePrior,
  64. kChangeTypeAccessTokenExpirationDate,
  65. kChangeTypeIDTokenPrior,
  66. kChangeTypeIDToken,
  67. kChangeTypeIDTokenExpirationDatePrior,
  68. kChangeTypeIDTokenExpirationDate,
  69. kChangeTypeEnd // not a real change type but an end mark for calculating |kChangeAll|
  70. };
  71. static const NSUInteger kChangeNone = 0u;
  72. static const NSUInteger kChangeAll = (1u << kChangeTypeEnd) - 1u;
  73. #if __has_feature(c_static_assert) || __has_extension(c_static_assert)
  74. _Static_assert(kChangeTypeEnd == (sizeof(kObservedProperties) / sizeof(*kObservedProperties)) * 2,
  75. "List of observed properties must match list of change notification enums");
  76. #endif
  77. @interface GIDAuthenticationTest : XCTestCase
  78. @end
  79. @implementation GIDAuthenticationTest {
  80. // Whether the auth object has ID token or not.
  81. BOOL _hasIDToken;
  82. // Fake data used to generate the expiration date of the access token.
  83. NSTimeInterval _accessTokenExpireTime;
  84. // Fake data used to generate the expiration date of the ID token.
  85. NSTimeInterval _idTokenExpireTime;
  86. // Fake data used to generate the additional token request parameters.
  87. NSDictionary *_additionalTokenRequestParameters;
  88. // The saved token fetch handler.
  89. OIDTokenCallback _tokenFetchHandler;
  90. // The saved token request.
  91. OIDTokenRequest *_tokenRequest;
  92. // All GIDAuthentication objects that are observed.
  93. NSMutableArray *_observedAuths;
  94. // Bitmask flags for observed changes, as specified in |ChangeType|.
  95. NSUInteger _changesObserved;
  96. // The fake system name used for testing.
  97. NSString *_fakeSystemName;
  98. }
  99. - (void)setUp {
  100. _hasIDToken = YES;
  101. _accessTokenExpireTime = kAccessTokenExpiresIn;
  102. _idTokenExpireTime = kExpireTime;
  103. _additionalTokenRequestParameters = nil;
  104. _tokenFetchHandler = nil;
  105. _tokenRequest = nil;
  106. [GULSwizzler swizzleClass:[OIDAuthorizationService class]
  107. selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:)
  108. isClassSelector:YES
  109. withBlock:^(id sender,
  110. OIDTokenRequest *request,
  111. OIDAuthorizationResponse *authorizationResponse,
  112. OIDTokenCallback callback) {
  113. XCTAssertNotNil(authorizationResponse.request.clientID);
  114. XCTAssertNotNil(authorizationResponse.request.configuration.tokenEndpoint);
  115. XCTAssertNil(_tokenFetchHandler); // only one on-going fetch allowed
  116. _tokenFetchHandler = [callback copy];
  117. _tokenRequest = [request copy];
  118. return nil;
  119. }];
  120. _observedAuths = [[NSMutableArray alloc] init];
  121. _changesObserved = 0;
  122. _fakeSystemName = kNewIOSName;
  123. [GULSwizzler swizzleClass:[UIDevice class]
  124. selector:@selector(systemName)
  125. isClassSelector:NO
  126. withBlock:^(id sender) { return _fakeSystemName; }];
  127. }
  128. - (void)tearDown {
  129. [GULSwizzler unswizzleClass:[OIDAuthorizationService class]
  130. selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:)
  131. isClassSelector:YES];
  132. [GULSwizzler unswizzleClass:[UIDevice class]
  133. selector:@selector(systemName)
  134. isClassSelector:NO];
  135. for (GIDAuthentication *auth in _observedAuths) {
  136. for (unsigned int i = 0; i < kNumberOfObservedProperties; ++i) {
  137. [auth removeObserver:self forKeyPath:kObservedProperties[i]];
  138. }
  139. }
  140. _observedAuths = nil;
  141. }
  142. #pragma mark - Tests
  143. - (void)testInitWithAuthState {
  144. OIDAuthState *authState = [OIDAuthState testInstance];
  145. GIDAuthentication *auth = [[GIDAuthentication alloc] initWithAuthState:authState];
  146. XCTAssertEqualObjects(auth.clientID, authState.lastAuthorizationResponse.request.clientID);
  147. XCTAssertEqualObjects(auth.accessToken, authState.lastTokenResponse.accessToken);
  148. XCTAssertEqualObjects(auth.accessTokenExpirationDate,
  149. authState.lastTokenResponse.accessTokenExpirationDate);
  150. XCTAssertEqualObjects(auth.refreshToken, authState.refreshToken);
  151. XCTAssertEqualObjects(auth.idToken, authState.lastTokenResponse.idToken);
  152. OIDIDToken *idToken = [[OIDIDToken alloc]
  153. initWithIDTokenString:authState.lastTokenResponse.idToken];
  154. XCTAssertEqualObjects(auth.idTokenExpirationDate, [idToken expiresAt]);
  155. }
  156. - (void)testInitWithAuthStateNoIDToken {
  157. OIDAuthState *authState = [OIDAuthState testInstanceWithIDToken:nil];
  158. GIDAuthentication *auth = [[GIDAuthentication alloc] initWithAuthState:authState];
  159. XCTAssertEqualObjects(auth.clientID, authState.lastAuthorizationResponse.request.clientID);
  160. XCTAssertEqualObjects(auth.accessToken, authState.lastTokenResponse.accessToken);
  161. XCTAssertEqualObjects(auth.accessTokenExpirationDate,
  162. authState.lastTokenResponse.accessTokenExpirationDate);
  163. XCTAssertEqualObjects(auth.refreshToken, authState.refreshToken);
  164. XCTAssertNil(auth.idToken);
  165. XCTAssertNil(auth.idTokenExpirationDate);
  166. }
  167. - (void)testAuthState {
  168. OIDAuthState *authState = [OIDAuthState testInstance];
  169. GIDAuthentication *auth = [[GIDAuthentication alloc] initWithAuthState:authState];
  170. OIDAuthState *authStateReturned = auth.authState;
  171. XCTAssertEqual(authState, authStateReturned);
  172. }
  173. - (void)testCoding {
  174. GIDAuthentication *auth = [self auth];
  175. NSData *data = [NSKeyedArchiver archivedDataWithRootObject:auth];
  176. GIDAuthentication *newAuth = [NSKeyedUnarchiver unarchiveObjectWithData:data];
  177. XCTAssertEqualObjects(auth, newAuth);
  178. XCTAssertTrue([GIDAuthentication supportsSecureCoding]);
  179. }
  180. - (void)testFetcherAuthorizer {
  181. // This is really hard to test without assuming how GTMAppAuthFetcherAuthorization works
  182. // internally, so let's just take the shortcut here by asserting we get a
  183. // GTMAppAuthFetcherAuthorization object.
  184. GIDAuthentication *auth = [self auth];
  185. id<GTMFetcherAuthorizationProtocol> fetcherAuthroizer = auth.fetcherAuthorizer;
  186. XCTAssertTrue([fetcherAuthroizer isKindOfClass:[GTMAppAuthFetcherAuthorization class]]);
  187. XCTAssertTrue([fetcherAuthroizer canAuthorize]);
  188. }
  189. - (void)testDoWithFreshTokensWithBothExpired {
  190. // Both tokens expired 10 seconds ago.
  191. [self setExpireTimeForAccessToken:-10 IDToken:-10];
  192. [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
  193. }
  194. - (void)testDoWithFreshTokensWithAccessTokenExpired {
  195. // Access token expired 10 seconds ago while ID token to expire in 10 minutes.
  196. [self setExpireTimeForAccessToken:-10 IDToken:10 * 60];
  197. [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
  198. }
  199. - (void)testDoWithFreshTokensWithIDTokenToExpire {
  200. // Access token to expire in 10 minutes while ID token to expire in 10 seconds.
  201. [self setExpireTimeForAccessToken:10 * 60 IDToken:10];
  202. [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
  203. }
  204. - (void)testDoWithFreshTokensWithBothFresh {
  205. // Both tokens to expire in 10 minutes.
  206. [self setExpireTimeForAccessToken:10 * 60 IDToken:10 * 60];
  207. [self verifyTokensNotRefreshedWithMethod:@selector(doWithFreshTokens:)];
  208. }
  209. - (void)testDoWithFreshTokensWithAccessTokenExpiredAndNoIDToken {
  210. _hasIDToken = NO;
  211. [self setExpireTimeForAccessToken:-10 IDToken:10 * 60]; // access token expired 10 seconds ago
  212. [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
  213. }
  214. - (void)testDoWithFreshTokensWithAccessTokenFreshAndNoIDToken {
  215. _hasIDToken = NO;
  216. [self setExpireTimeForAccessToken:10 * 60 IDToken:-10]; // access token to expire in 10 minutes
  217. [self verifyTokensNotRefreshedWithMethod:@selector(doWithFreshTokens:)];
  218. }
  219. - (void)testDoWithFreshTokensError {
  220. [self setTokensExpireTime:-10]; // expired 10 seconds ago
  221. GIDAuthentication *auth = [self observedAuth];
  222. __block BOOL callbackCalled = NO;
  223. [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
  224. callbackCalled = YES;
  225. XCTAssertNil(authentication);
  226. XCTAssertNotNil(error);
  227. }];
  228. _tokenFetchHandler(nil, [self fakeError]);
  229. XCTAssertTrue(callbackCalled);
  230. [self assertOldTokensInAuth:auth];
  231. }
  232. - (void)testDoWithFreshTokensQueue {
  233. GIDAuthentication *auth = [self observedAuth];
  234. __block BOOL firstCallbackCalled = NO;
  235. [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
  236. firstCallbackCalled = YES;
  237. [self assertNewTokensInAuth:authentication];
  238. XCTAssertNil(error);
  239. }];
  240. __block BOOL secondCallbackCalled = NO;
  241. [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
  242. secondCallbackCalled = YES;
  243. [self assertNewTokensInAuth:authentication];
  244. XCTAssertNil(error);
  245. }];
  246. _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
  247. XCTAssertTrue(firstCallbackCalled);
  248. XCTAssertTrue(secondCallbackCalled);
  249. [self assertNewTokensInAuth:auth];
  250. }
  251. #pragma mark - EMM Support
  252. - (void)testEMMSupport {
  253. _additionalTokenRequestParameters = @{
  254. @"emm_support" : @"xyz",
  255. };
  256. GIDAuthentication *auth = [self auth];
  257. [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
  258. NSError * _Nullable error) {}];
  259. _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
  260. NSDictionary *expectedParameters = @{
  261. @"emm_support" : @"xyz",
  262. @"device_os" : [NSString stringWithFormat:@"%@ %@",
  263. _fakeSystemName, [UIDevice currentDevice].systemVersion],
  264. kSDKVersionLoggingParameter : GIDVersion(),
  265. };
  266. XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
  267. expectedParameters);
  268. }
  269. - (void)testSystemNameNormalization {
  270. _fakeSystemName = kOldIOSName;
  271. _additionalTokenRequestParameters = @{
  272. @"emm_support" : @"xyz",
  273. };
  274. GIDAuthentication *auth = [self auth];
  275. [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
  276. NSError * _Nullable error) {}];
  277. _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
  278. NSDictionary *expectedParameters = @{
  279. @"emm_support" : @"xyz",
  280. @"device_os" : [NSString stringWithFormat:@"%@ %@",
  281. kNewIOSName, [UIDevice currentDevice].systemVersion],
  282. kSDKVersionLoggingParameter : GIDVersion(),
  283. };
  284. XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
  285. expectedParameters);
  286. }
  287. - (void)testEMMPasscodeInfo {
  288. _additionalTokenRequestParameters = @{
  289. @"emm_support" : @"xyz",
  290. @"device_os" : @"old one",
  291. @"emm_passcode_info" : @"something",
  292. };
  293. GIDAuthentication *auth = [self auth];
  294. [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
  295. NSError * _Nullable error) {}];
  296. _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
  297. NSDictionary *expectedParameters = @{
  298. @"emm_support" : @"xyz",
  299. @"device_os" : [NSString stringWithFormat:@"%@ %@",
  300. _fakeSystemName, [UIDevice currentDevice].systemVersion],
  301. @"emm_passcode_info" : [GIDMDMPasscodeState passcodeState].info,
  302. kSDKVersionLoggingParameter : GIDVersion(),
  303. };
  304. XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
  305. expectedParameters);
  306. }
  307. - (void)testEMMError {
  308. // Set expectations.
  309. NSDictionary *errorJSON = @{ @"error" : @"EMM Specific Error" };
  310. NSError *emmError = [NSError errorWithDomain:@"anydomain"
  311. code:12345
  312. userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }];
  313. id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]);
  314. [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance];
  315. __block void (^completion)(void);
  316. [[[mockEMMErrorHandler expect] andReturnValue:@YES]
  317. handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) {
  318. completion = arg;
  319. return YES;
  320. }]];
  321. // Start testing.
  322. _additionalTokenRequestParameters = @{
  323. @"emm_support" : @"xyz",
  324. };
  325. GIDAuthentication *auth = [self auth];
  326. __block BOOL callbackCalled = NO;
  327. [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
  328. callbackCalled = YES;
  329. XCTAssertNil(authentication);
  330. XCTAssertEqualObjects(error.domain, kGIDSignInErrorDomain);
  331. XCTAssertEqual(error.code, kGIDSignInErrorCodeEMM);
  332. }];
  333. _tokenFetchHandler(nil, emmError);
  334. // Verify and clean up.
  335. [mockEMMErrorHandler verify];
  336. [mockEMMErrorHandler stopMocking];
  337. XCTAssertFalse(callbackCalled);
  338. completion();
  339. XCTAssertTrue(callbackCalled);
  340. [self assertOldTokensInAuth:auth];
  341. }
  342. - (void)testNonEMMError {
  343. // Set expectations.
  344. NSDictionary *errorJSON = @{ @"error" : @"Not EMM Specific Error" };
  345. NSError *emmError = [NSError errorWithDomain:@"anydomain"
  346. code:12345
  347. userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }];
  348. id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]);
  349. [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance];
  350. __block void (^completion)(void);
  351. [[[mockEMMErrorHandler expect] andReturnValue:@NO]
  352. handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) {
  353. completion = arg;
  354. return YES;
  355. }]];
  356. // Start testing.
  357. _additionalTokenRequestParameters = @{
  358. @"emm_support" : @"xyz",
  359. };
  360. GIDAuthentication *auth = [self auth];
  361. __block BOOL callbackCalled = NO;
  362. [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
  363. callbackCalled = YES;
  364. XCTAssertNil(authentication);
  365. XCTAssertEqualObjects(error.domain, @"anydomain");
  366. XCTAssertEqual(error.code, 12345);
  367. }];
  368. _tokenFetchHandler(nil, emmError);
  369. // Verify and clean up.
  370. [mockEMMErrorHandler verify];
  371. [mockEMMErrorHandler stopMocking];
  372. XCTAssertFalse(callbackCalled);
  373. completion();
  374. XCTAssertTrue(callbackCalled);
  375. [self assertOldTokensInAuth:auth];
  376. }
  377. - (void)testCodingPreserveEMMParameters {
  378. _additionalTokenRequestParameters = @{
  379. @"emm_support" : @"xyz",
  380. @"device_os" : @"old one",
  381. @"emm_passcode_info" : @"something",
  382. };
  383. NSData *data = [NSKeyedArchiver archivedDataWithRootObject:[self auth]];
  384. GIDAuthentication *auth = [NSKeyedUnarchiver unarchiveObjectWithData:data];
  385. [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
  386. NSError * _Nullable error) {}];
  387. _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
  388. NSDictionary *expectedParameters = @{
  389. @"emm_support" : @"xyz",
  390. @"device_os" : [NSString stringWithFormat:@"%@ %@",
  391. [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion],
  392. @"emm_passcode_info" : [GIDMDMPasscodeState passcodeState].info,
  393. kSDKVersionLoggingParameter : GIDVersion(),
  394. };
  395. XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
  396. expectedParameters);
  397. }
  398. #pragma mark - NSKeyValueObserving
  399. - (void)observeValueForKeyPath:(NSString *)keyPath
  400. ofObject:(id)object
  401. change:(NSDictionary *)change
  402. context:(void *)context {
  403. GIDAuthentication *auth = (GIDAuthentication *)object;
  404. ChangeType changeType;
  405. if ([keyPath isEqualToString:@"accessToken"]) {
  406. if (change[NSKeyValueChangeNotificationIsPriorKey]) {
  407. XCTAssertEqualObjects(auth.accessToken, kAccessToken);
  408. changeType = kChangeTypeAccessTokenPrior;
  409. } else {
  410. XCTAssertEqualObjects(auth.accessToken, kNewAccessToken);
  411. changeType = kChangeTypeAccessToken;
  412. }
  413. } else if ([keyPath isEqualToString:@"accessTokenExpirationDate"]) {
  414. if (change[NSKeyValueChangeNotificationIsPriorKey]) {
  415. [self assertDate:auth.accessTokenExpirationDate equalTime:_accessTokenExpireTime];
  416. changeType = kChangeTypeAccessTokenExpirationDatePrior;
  417. } else {
  418. [self assertDate:auth.accessTokenExpirationDate equalTime:kNewExpireTime];
  419. changeType = kChangeTypeAccessTokenExpirationDate;
  420. }
  421. } else if ([keyPath isEqualToString:@"idToken"]) {
  422. if (change[NSKeyValueChangeNotificationIsPriorKey]) {
  423. XCTAssertEqualObjects(auth.idToken, [self idToken]);
  424. changeType = kChangeTypeIDTokenPrior;
  425. } else {
  426. XCTAssertEqualObjects(auth.idToken, [self idTokenNew]);
  427. changeType = kChangeTypeIDToken;
  428. }
  429. } else if ([keyPath isEqualToString:@"idTokenExpirationDate"]) {
  430. if (change[NSKeyValueChangeNotificationIsPriorKey]) {
  431. if (_hasIDToken) {
  432. [self assertDate:auth.idTokenExpirationDate equalTime:_idTokenExpireTime];
  433. }
  434. changeType = kChangeTypeIDTokenExpirationDatePrior;
  435. } else {
  436. if (_hasIDToken) {
  437. [self assertDate:auth.idTokenExpirationDate equalTime:kNewExpireTime2];
  438. }
  439. changeType = kChangeTypeIDTokenExpirationDate;
  440. }
  441. } else {
  442. XCTFail(@"unexpected keyPath");
  443. return; // so compiler knows |changeType| is always assigned
  444. }
  445. NSUInteger changeMask = 1u << changeType;
  446. XCTAssertFalse(_changesObserved & changeMask); // each change type should only fire once
  447. _changesObserved |= changeMask;
  448. }
  449. #pragma mark - Helpers
  450. - (GIDAuthentication *)auth {
  451. NSString *idToken = [self idToken];
  452. NSNumber *accessTokenExpiresIn =
  453. @(_accessTokenExpireTime - [[NSDate date] timeIntervalSinceReferenceDate]);
  454. OIDTokenRequest *tokenRequest =
  455. [OIDTokenRequest testInstanceWithAdditionalParameters:_additionalTokenRequestParameters];
  456. OIDTokenResponse *tokenResponse =
  457. [OIDTokenResponse testInstanceWithIDToken:idToken
  458. accessToken:kAccessToken
  459. expiresIn:accessTokenExpiresIn
  460. tokenRequest:tokenRequest];
  461. return [[GIDAuthentication alloc]
  462. initWithAuthState:[OIDAuthState testInstanceWithTokenResponse:tokenResponse]];
  463. }
  464. - (NSString *)idTokenWithExpireTime:(NSTimeInterval)expireTime {
  465. if (!_hasIDToken) {
  466. return nil;
  467. }
  468. return [OIDTokenResponse idTokenWithSub:kUserID exp:@(expireTime + NSTimeIntervalSince1970)];
  469. }
  470. - (NSString *)idToken {
  471. return [self idTokenWithExpireTime:_idTokenExpireTime];
  472. }
  473. - (NSString *)idTokenNew {
  474. return [self idTokenWithExpireTime:kNewExpireTime2];
  475. }
  476. // Return the auth object that has certain property changes observed.
  477. - (GIDAuthentication *)observedAuth {
  478. GIDAuthentication *auth = [self auth];
  479. for (unsigned int i = 0; i < kNumberOfObservedProperties; ++i) {
  480. [auth addObserver:self
  481. forKeyPath:kObservedProperties[i]
  482. options:NSKeyValueObservingOptionPrior
  483. context:NULL];
  484. }
  485. [_observedAuths addObject:auth];
  486. return auth;
  487. }
  488. - (OIDTokenResponse *)tokenResponseWithNewTokens {
  489. NSNumber *expiresIn = @(kNewExpireTime - [NSDate timeIntervalSinceReferenceDate]);
  490. return [OIDTokenResponse testInstanceWithIDToken:(_hasIDToken ? [self idTokenNew] : nil)
  491. accessToken:kNewAccessToken
  492. expiresIn:expiresIn
  493. tokenRequest:_tokenRequest ?: nil];
  494. }
  495. - (NSError *)fakeError {
  496. return [NSError errorWithDomain:@"fake.domain" code:-1 userInfo:nil];
  497. }
  498. - (void)assertDate:(NSDate *)date equalTime:(NSTimeInterval)time {
  499. XCTAssertEqualWithAccuracy([date timeIntervalSinceReferenceDate], time, kTimeAccuracy);
  500. }
  501. - (void)assertOldAccessTokenInAuth:(GIDAuthentication *)auth {
  502. XCTAssertEqualObjects(auth.accessToken, kAccessToken);
  503. [self assertDate:auth.accessTokenExpirationDate equalTime:_accessTokenExpireTime];
  504. XCTAssertEqual(_changesObserved, kChangeNone);
  505. }
  506. - (void)assertNewAccessTokenInAuth:(GIDAuthentication *)auth {
  507. XCTAssertEqualObjects(auth.accessToken, kNewAccessToken);
  508. [self assertDate:auth.accessTokenExpirationDate equalTime:kNewExpireTime];
  509. XCTAssertEqual(_changesObserved, kChangeAll);
  510. }
  511. - (void)assertOldTokensInAuth:(GIDAuthentication *)auth {
  512. [self assertOldAccessTokenInAuth:auth];
  513. XCTAssertEqualObjects(auth.idToken, [self idToken]);
  514. if (_hasIDToken) {
  515. [self assertDate:auth.idTokenExpirationDate equalTime:_idTokenExpireTime];
  516. }
  517. }
  518. - (void)assertNewTokensInAuth:(GIDAuthentication *)auth {
  519. [self assertNewAccessTokenInAuth:auth];
  520. XCTAssertEqualObjects(auth.idToken, [self idTokenNew]);
  521. if (_hasIDToken) {
  522. [self assertDate:auth.idTokenExpirationDate equalTime:kNewExpireTime2];
  523. }
  524. }
  525. - (void)setTokensExpireTime:(NSTimeInterval)fromNow {
  526. [self setExpireTimeForAccessToken:fromNow IDToken:fromNow];
  527. }
  528. - (void)setExpireTimeForAccessToken:(NSTimeInterval)accessExpire IDToken:(NSTimeInterval)idExpire {
  529. _accessTokenExpireTime = [[NSDate date] timeIntervalSinceReferenceDate] + accessExpire;
  530. _idTokenExpireTime = [[NSDate date] timeIntervalSinceReferenceDate] + idExpire;
  531. }
  532. - (void)verifyTokensRefreshedWithMethod:(SEL)sel {
  533. GIDAuthentication *auth = [self observedAuth];
  534. __block BOOL callbackCalled = NO;
  535. #pragma clang diagnostic push
  536. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  537. // We know the method doesn't return anything, so there is no risk of leaking.
  538. [auth performSelector:sel withObject:^(GIDAuthentication *authentication, NSError *error) {
  539. #pragma clang diagnostic pop
  540. callbackCalled = YES;
  541. [self assertNewTokensInAuth:authentication];
  542. XCTAssertNil(error);
  543. }];
  544. _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
  545. XCTAssertTrue(callbackCalled);
  546. [self assertNewTokensInAuth:auth];
  547. }
  548. - (void)verifyTokensNotRefreshedWithMethod:(SEL)sel {
  549. GIDAuthentication *auth = [self observedAuth];
  550. __block BOOL callbackCalled = NO;
  551. #pragma clang diagnostic push
  552. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  553. // We know the method doesn't return anything, so there is no risk of leaking.
  554. [auth performSelector:sel withObject:^(GIDAuthentication *authentication, NSError *error) {
  555. #pragma clang diagnostic pop
  556. callbackCalled = YES;
  557. [self assertOldTokensInAuth:authentication];
  558. XCTAssertNil(error);
  559. }];
  560. XCTAssertNil(_tokenFetchHandler);
  561. XCTAssertTrue(callbackCalled);
  562. [self assertOldTokensInAuth:auth];
  563. }
  564. @end