GIDEMMErrorHandlerTest.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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 <UIKit/UIKit.h>
  15. #import <XCTest/XCTest.h>
  16. #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
  17. #import "GoogleSignIn/Sources/GIDSignInStrings.h"
  18. #import <GoogleUtilities/GULSwizzler.h>
  19. #import <GoogleUtilities/GULSwizzler+Unswizzle.h>
  20. #import <OCMock/OCMock.h>
  21. NS_ASSUME_NONNULL_BEGIN
  22. // Addtional methods added to UIAlertAction for testing.
  23. @interface UIAlertAction (Testing)
  24. // Returns the handler block for this alert action.
  25. - (void (^)(UIAlertAction *))actionHandler;
  26. @end
  27. @implementation UIAlertAction (Testing)
  28. - (void (^)(UIAlertAction *))actionHandler {
  29. return [self valueForKey:@"handler"];
  30. }
  31. @end
  32. // Unit test for GIDEMMErrorHandler.
  33. @interface GIDEMMErrorHandlerTest : XCTestCase
  34. @end
  35. @implementation GIDEMMErrorHandlerTest {
  36. // Whether or not the current device runs on iOS 10.
  37. BOOL _isIOS10;
  38. // Whether key window has been set.
  39. BOOL _keyWindowSet;
  40. // The view controller that has been presented, if any.
  41. UIViewController *_presentedViewController;
  42. }
  43. - (void)setUp {
  44. [super setUp];
  45. _isIOS10 = [UIDevice currentDevice].systemVersion.integerValue == 10;
  46. _keyWindowSet = NO;
  47. _presentedViewController = nil;
  48. [GULSwizzler swizzleClass:[UIWindow class]
  49. selector:@selector(makeKeyAndVisible)
  50. isClassSelector:NO
  51. withBlock:^() { _keyWindowSet = YES; }];
  52. [GULSwizzler swizzleClass:[UIViewController class]
  53. selector:@selector(presentViewController:animated:completion:)
  54. isClassSelector:NO
  55. withBlock:^(id obj, id arg1) { _presentedViewController = arg1; }];
  56. [GULSwizzler swizzleClass:[GIDSignInStrings class]
  57. selector:@selector(localizedStringForKey:text:)
  58. isClassSelector:YES
  59. withBlock:^(id obj, NSString *key, NSString *text) { return text; }];
  60. }
  61. - (void)tearDown {
  62. [GULSwizzler unswizzleClass:[UIWindow class]
  63. selector:@selector(makeKeyAndVisible)
  64. isClassSelector:NO];
  65. [GULSwizzler unswizzleClass:[UIViewController class]
  66. selector:@selector(presentViewController:animated:completion:)
  67. isClassSelector:NO];
  68. [GULSwizzler unswizzleClass:[GIDSignInStrings class]
  69. selector:@selector(localizedStringForKey:text:)
  70. isClassSelector:YES];
  71. _presentedViewController = nil;
  72. [super tearDown];
  73. }
  74. // Expects opening a particular URL string in performing an action.
  75. - (void)expectOpenURLString:(NSString *)urlString inAction:(void (^)())action {
  76. // Swizzle and mock [UIApplication sharedApplication] since it is unavailable in unit tests.
  77. id mockApplication = OCMStrictClassMock([UIApplication class]);
  78. [GULSwizzler swizzleClass:[UIApplication class]
  79. selector:@selector(sharedApplication)
  80. isClassSelector:YES
  81. withBlock:^() { return mockApplication; }];
  82. [[mockApplication expect] openURL:[NSURL URLWithString:urlString]];
  83. action();
  84. [mockApplication verify];
  85. [GULSwizzler unswizzleClass:[UIApplication class]
  86. selector:@selector(sharedApplication)
  87. isClassSelector:YES];
  88. }
  89. // Verifies that the handler doesn't handle non-exist error.
  90. - (void)testNoError {
  91. __block BOOL completionCalled = NO;
  92. BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:@{ @"abc" : @123 }
  93. completion:^() {
  94. completionCalled = YES;
  95. }];
  96. XCTAssertFalse(result);
  97. XCTAssertTrue(completionCalled);
  98. XCTAssertFalse(_keyWindowSet);
  99. XCTAssertNil(_presentedViewController);
  100. }
  101. // Verifies that the handler doesn't handle non-EMM error.
  102. - (void)testNoEMMError {
  103. __block BOOL completionCalled = NO;
  104. NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"invalid_token" };
  105. BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
  106. completion:^() {
  107. completionCalled = YES;
  108. }];
  109. XCTAssertFalse(result);
  110. XCTAssertTrue(completionCalled);
  111. XCTAssertFalse(_keyWindowSet);
  112. XCTAssertNil(_presentedViewController);
  113. }
  114. // Verifies that the handler handles general EMM error with user tapping 'OK'.
  115. - (void)testGeneralEMMErrorOK {
  116. __block BOOL completionCalled = NO;
  117. NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"emm_something_wrong" };
  118. BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
  119. completion:^() {
  120. completionCalled = YES;
  121. }];
  122. if (![UIAlertController class]) {
  123. XCTAssertFalse(result);
  124. XCTAssertTrue(completionCalled);
  125. XCTAssertFalse(_keyWindowSet);
  126. XCTAssertNil(_presentedViewController);
  127. return;
  128. }
  129. XCTAssertTrue(result);
  130. XCTAssertFalse(completionCalled);
  131. XCTAssertFalse(_keyWindowSet);
  132. XCTAssertNil(_presentedViewController);
  133. // Should handle no more error while the previous one is being handled.
  134. __block BOOL secondCompletionCalled = NO;
  135. BOOL secondResult = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
  136. completion:^() {
  137. secondCompletionCalled = YES;
  138. }];
  139. XCTAssertFalse(secondResult);
  140. XCTAssertTrue(secondCompletionCalled);
  141. XCTAssertFalse(_keyWindowSet);
  142. XCTAssertNil(_presentedViewController);
  143. // Wait for the code under test to be executed on the main thread.
  144. XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
  145. dispatch_async(dispatch_get_main_queue(), ^() {
  146. [expectation fulfill];
  147. });
  148. [self waitForExpectationsWithTimeout:1 handler:nil];
  149. XCTAssertFalse(completionCalled);
  150. XCTAssertTrue(_keyWindowSet);
  151. XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
  152. UIAlertController *alert = (UIAlertController *)_presentedViewController;
  153. XCTAssertNotNil(alert.title);
  154. XCTAssertNotNil(alert.message);
  155. XCTAssertEqual(alert.actions.count, 1);
  156. // Pretend to touch the "OK" button.
  157. UIAlertAction *action = alert.actions[0];
  158. XCTAssertEqualObjects(action.title, @"OK");
  159. action.actionHandler(action);
  160. XCTAssertTrue(completionCalled);
  161. }
  162. // Verifies that the handler handles EMM screenlock required error with user tapping 'Cancel'.
  163. - (void)testScreenlockRequiredCancel {
  164. if (_isIOS10) {
  165. // The dialog is different on iOS 10.
  166. return;
  167. }
  168. __block BOOL completionCalled = NO;
  169. NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"emm_passcode_required" };
  170. BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
  171. completion:^() {
  172. completionCalled = YES;
  173. }];
  174. if (![UIAlertController class]) {
  175. XCTAssertFalse(result);
  176. XCTAssertTrue(completionCalled);
  177. XCTAssertFalse(_keyWindowSet);
  178. XCTAssertNil(_presentedViewController);
  179. return;
  180. }
  181. XCTAssertTrue(result);
  182. XCTAssertFalse(completionCalled);
  183. XCTAssertFalse(_keyWindowSet);
  184. XCTAssertNil(_presentedViewController);
  185. // Wait for the code under test to be executed on the main thread.
  186. XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
  187. dispatch_async(dispatch_get_main_queue(), ^() {
  188. [expectation fulfill];
  189. });
  190. [self waitForExpectationsWithTimeout:1 handler:nil];
  191. XCTAssertFalse(completionCalled);
  192. XCTAssertTrue(_keyWindowSet);
  193. XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
  194. UIAlertController *alert = (UIAlertController *)_presentedViewController;
  195. XCTAssertNotNil(alert.title);
  196. XCTAssertNotNil(alert.message);
  197. XCTAssertEqual(alert.actions.count, 2);
  198. // Pretend to touch the "Cancel" button.
  199. UIAlertAction *action = alert.actions[0];
  200. XCTAssertEqualObjects(action.title, @"Cancel");
  201. action.actionHandler(action);
  202. XCTAssertTrue(completionCalled);
  203. }
  204. // Verifies that the handler handles EMM screenlock required error with user tapping 'Settings'.
  205. - (void)testScreenlockRequiredSettings {
  206. if (_isIOS10) {
  207. // The dialog is different on iOS 10.
  208. return;
  209. }
  210. __block BOOL completionCalled = NO;
  211. NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"emm_passcode_required" };
  212. BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
  213. completion:^() {
  214. completionCalled = YES;
  215. }];
  216. if (![UIAlertController class]) {
  217. XCTAssertFalse(result);
  218. XCTAssertTrue(completionCalled);
  219. XCTAssertFalse(_keyWindowSet);
  220. XCTAssertNil(_presentedViewController);
  221. return;
  222. }
  223. XCTAssertTrue(result);
  224. XCTAssertFalse(completionCalled);
  225. XCTAssertFalse(_keyWindowSet);
  226. XCTAssertNil(_presentedViewController);
  227. // Wait for the code under test to be executed on the main thread.
  228. XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
  229. dispatch_async(dispatch_get_main_queue(), ^() {
  230. [expectation fulfill];
  231. });
  232. [self waitForExpectationsWithTimeout:1 handler:nil];
  233. XCTAssertFalse(completionCalled);
  234. XCTAssertTrue(_keyWindowSet);
  235. XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
  236. UIAlertController *alert = (UIAlertController *)_presentedViewController;
  237. XCTAssertNotNil(alert.title);
  238. XCTAssertNotNil(alert.message);
  239. XCTAssertEqual(alert.actions.count, 2);
  240. // Pretend to touch the "Settings" button.
  241. UIAlertAction *action = alert.actions[1];
  242. XCTAssertEqualObjects(action.title, @"Settings");
  243. [self expectOpenURLString:UIApplicationOpenSettingsURLString inAction:^() {
  244. action.actionHandler(action);
  245. }];
  246. XCTAssertTrue(completionCalled);
  247. }
  248. - (void)testScreenlockRequiredOkOnIOS10 {
  249. if (!_isIOS10) {
  250. // A more useful dialog is used for other iOS versions.
  251. return;
  252. }
  253. __block BOOL completionCalled = NO;
  254. NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"emm_passcode_required" };
  255. BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
  256. completion:^() {
  257. completionCalled = YES;
  258. }];
  259. if (![UIAlertController class]) {
  260. XCTAssertFalse(result);
  261. XCTAssertTrue(completionCalled);
  262. XCTAssertFalse(_keyWindowSet);
  263. XCTAssertNil(_presentedViewController);
  264. return;
  265. }
  266. XCTAssertTrue(result);
  267. XCTAssertFalse(completionCalled);
  268. XCTAssertFalse(_keyWindowSet);
  269. XCTAssertNil(_presentedViewController);
  270. // Wait for the code under test to be executed on the main thread.
  271. XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
  272. dispatch_async(dispatch_get_main_queue(), ^() {
  273. [expectation fulfill];
  274. });
  275. [self waitForExpectationsWithTimeout:1 handler:nil];
  276. XCTAssertFalse(completionCalled);
  277. XCTAssertTrue(_keyWindowSet);
  278. XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
  279. UIAlertController *alert = (UIAlertController *)_presentedViewController;
  280. XCTAssertNotNil(alert.title);
  281. XCTAssertNotNil(alert.message);
  282. XCTAssertEqual(alert.actions.count, 1);
  283. // Pretend to touch the "OK" button.
  284. UIAlertAction *action = alert.actions[0];
  285. XCTAssertEqualObjects(action.title, @"OK");
  286. action.actionHandler(action);
  287. XCTAssertTrue(completionCalled);
  288. }
  289. // Verifies that the handler handles EMM app verification required error without a URL.
  290. - (void)testAppVerificationNoURL {
  291. __block BOOL completionCalled = NO;
  292. NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"emm_app_verification_required" };
  293. BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
  294. completion:^() {
  295. completionCalled = YES;
  296. }];
  297. if (![UIAlertController class]) {
  298. XCTAssertFalse(result);
  299. XCTAssertTrue(completionCalled);
  300. XCTAssertFalse(_keyWindowSet);
  301. XCTAssertNil(_presentedViewController);
  302. return;
  303. }
  304. XCTAssertTrue(result);
  305. XCTAssertFalse(completionCalled);
  306. XCTAssertFalse(_keyWindowSet);
  307. XCTAssertNil(_presentedViewController);
  308. // Wait for the code under test to be executed on the main thread.
  309. XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
  310. dispatch_async(dispatch_get_main_queue(), ^() {
  311. [expectation fulfill];
  312. });
  313. [self waitForExpectationsWithTimeout:1 handler:nil];
  314. XCTAssertFalse(completionCalled);
  315. XCTAssertTrue(_keyWindowSet);
  316. XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
  317. UIAlertController *alert = (UIAlertController *)_presentedViewController;
  318. XCTAssertNotNil(alert.title);
  319. XCTAssertNotNil(alert.message);
  320. XCTAssertEqual(alert.actions.count, 1);
  321. // Pretend to touch the "OK" button.
  322. UIAlertAction *action = alert.actions[0];
  323. XCTAssertEqualObjects(action.title, @"OK");
  324. action.actionHandler(action);
  325. XCTAssertTrue(completionCalled);
  326. }
  327. // Verifies that the handler handles EMM app verification required error user tapping 'Cancel'.
  328. - (void)testAppVerificationCancel {
  329. __block BOOL completionCalled = NO;
  330. NSDictionary<NSString *, NSString *> *response =
  331. @{ @"error" : @"emm_app_verification_required: https://host.domain/path" };
  332. BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
  333. completion:^() {
  334. completionCalled = YES;
  335. }];
  336. if (![UIAlertController class]) {
  337. XCTAssertFalse(result);
  338. XCTAssertTrue(completionCalled);
  339. XCTAssertFalse(_keyWindowSet);
  340. XCTAssertNil(_presentedViewController);
  341. return;
  342. }
  343. XCTAssertTrue(result);
  344. XCTAssertFalse(completionCalled);
  345. XCTAssertFalse(_keyWindowSet);
  346. XCTAssertNil(_presentedViewController);
  347. // Wait for the code under test to be executed on the main thread.
  348. XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
  349. dispatch_async(dispatch_get_main_queue(), ^() {
  350. [expectation fulfill];
  351. });
  352. [self waitForExpectationsWithTimeout:1 handler:nil];
  353. XCTAssertFalse(completionCalled);
  354. XCTAssertTrue(_keyWindowSet);
  355. XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
  356. UIAlertController *alert = (UIAlertController *)_presentedViewController;
  357. XCTAssertNotNil(alert.title);
  358. XCTAssertNotNil(alert.message);
  359. XCTAssertEqual(alert.actions.count, 2);
  360. // Pretend to touch the "Cancel" button.
  361. UIAlertAction *action = alert.actions[0];
  362. XCTAssertEqualObjects(action.title, @"Cancel");
  363. action.actionHandler(action);
  364. XCTAssertTrue(completionCalled);
  365. }
  366. // Verifies that the handler handles EMM app verification required error user tapping 'Connect'.
  367. - (void)testAppVerificationConnect {
  368. __block BOOL completionCalled = NO;
  369. NSDictionary<NSString *, NSString *> *response =
  370. @{ @"error" : @"emm_app_verification_required: https://host.domain/path" };
  371. BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
  372. completion:^() {
  373. completionCalled = YES;
  374. }];
  375. if (![UIAlertController class]) {
  376. XCTAssertFalse(result);
  377. XCTAssertTrue(completionCalled);
  378. XCTAssertFalse(_keyWindowSet);
  379. XCTAssertNil(_presentedViewController);
  380. return;
  381. }
  382. XCTAssertTrue(result);
  383. XCTAssertFalse(completionCalled);
  384. XCTAssertFalse(_keyWindowSet);
  385. XCTAssertNil(_presentedViewController);
  386. // Wait for the code under test to be executed on the main thread.
  387. XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
  388. dispatch_async(dispatch_get_main_queue(), ^() {
  389. [expectation fulfill];
  390. });
  391. [self waitForExpectationsWithTimeout:1 handler:nil];
  392. XCTAssertFalse(completionCalled);
  393. XCTAssertTrue(_keyWindowSet);
  394. XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
  395. UIAlertController *alert = (UIAlertController *)_presentedViewController;
  396. XCTAssertNotNil(alert.title);
  397. XCTAssertNotNil(alert.message);
  398. XCTAssertEqual(alert.actions.count, 2);
  399. // Pretend to touch the "Connect" button.
  400. UIAlertAction *action = alert.actions[1];
  401. XCTAssertEqualObjects(action.title, @"Connect");
  402. [self expectOpenURLString:@"https://host.domain/path" inAction:^() {
  403. action.actionHandler(action);
  404. }];
  405. XCTAssertTrue(completionCalled);
  406. }
  407. // Verifies that the handler can handle sequential errors independently.
  408. - (void)testSequentialErrors {
  409. [self testGeneralEMMErrorOK];
  410. _keyWindowSet = NO;
  411. _presentedViewController = nil;
  412. [self testScreenlockRequiredCancel];
  413. }
  414. @end
  415. NS_ASSUME_NONNULL_END