| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531 |
- // Copyright 2021 Google LLC
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- #import <TargetConditionals.h>
- #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
- #import <UIKit/UIKit.h>
- #import <XCTest/XCTest.h>
- #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
- #import "GoogleSignIn/Sources/GIDSignInStrings.h"
- #ifdef SWIFT_PACKAGE
- @import GoogleUtilities_MethodSwizzler;
- @import GoogleUtilities_SwizzlerTestHelpers;
- @import OCMock;
- #else
- #import <GoogleUtilities/GULSwizzler.h>
- #import <GoogleUtilities/GULSwizzler+Unswizzle.h>
- #import <OCMock/OCMock.h>
- #endif
- NS_ASSUME_NONNULL_BEGIN
- // Addtional methods added to UIAlertAction for testing.
- @interface UIAlertAction (Testing)
- // Returns the handler block for this alert action.
- - (void (^)(UIAlertAction *))actionHandler;
- @end
- @implementation UIAlertAction (Testing)
- - (void (^)(UIAlertAction *))actionHandler {
- return [self valueForKey:@"handler"];
- }
- @end
- // Unit test for GIDEMMErrorHandler.
- @interface GIDEMMErrorHandlerTest : XCTestCase
- @end
- @implementation GIDEMMErrorHandlerTest {
- // Whether or not the current device runs on iOS 10.
- BOOL _isIOS10;
- // Whether key window has been set.
- BOOL _keyWindowSet;
- // The view controller that has been presented, if any.
- UIViewController *_presentedViewController;
- }
- - (void)setUp {
- [super setUp];
- _isIOS10 = [UIDevice currentDevice].systemVersion.integerValue == 10;
- _keyWindowSet = NO;
- _presentedViewController = nil;
- UIWindow *fakeKeyWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
- [GULSwizzler swizzleClass:[GIDEMMErrorHandler class]
- selector:@selector(keyWindow)
- isClassSelector:NO
- withBlock:^() { return fakeKeyWindow; }];
- [GULSwizzler swizzleClass:[UIWindow class]
- selector:@selector(makeKeyAndVisible)
- isClassSelector:NO
- withBlock:^() { self->_keyWindowSet = YES; }];
- [GULSwizzler swizzleClass:[UIViewController class]
- selector:@selector(presentViewController:animated:completion:)
- isClassSelector:NO
- withBlock:^(id obj, id arg1) { self->_presentedViewController = arg1; }];
- [GULSwizzler swizzleClass:[GIDSignInStrings class]
- selector:@selector(localizedStringForKey:text:)
- isClassSelector:YES
- withBlock:^(id obj, NSString *key, NSString *text) { return text; }];
- }
- - (void)tearDown {
- [GULSwizzler unswizzleClass:[GIDEMMErrorHandler class]
- selector:@selector(keyWindow)
- isClassSelector:NO];
- [GULSwizzler unswizzleClass:[UIWindow class]
- selector:@selector(makeKeyAndVisible)
- isClassSelector:NO];
- [GULSwizzler unswizzleClass:[UIViewController class]
- selector:@selector(presentViewController:animated:completion:)
- isClassSelector:NO];
- [GULSwizzler unswizzleClass:[GIDSignInStrings class]
- selector:@selector(localizedStringForKey:text:)
- isClassSelector:YES];
- _presentedViewController = nil;
- [super tearDown];
- }
- // Expects opening a particular URL string in performing an action.
- - (void)expectOpenURLString:(NSString *)urlString inAction:(void (^)(void))action {
- // Swizzle and mock [UIApplication sharedApplication] since it is unavailable in unit tests.
- id mockApplication = OCMStrictClassMock([UIApplication class]);
- [GULSwizzler swizzleClass:[UIApplication class]
- selector:@selector(sharedApplication)
- isClassSelector:YES
- withBlock:^() { return mockApplication; }];
- if (@available(iOS 10, *)) {
- [[mockApplication expect] openURL:[NSURL URLWithString:urlString] options:@{} completionHandler:nil];
- } else {
- [[mockApplication expect] openURL:[NSURL URLWithString:urlString]];
- }
- action();
- [mockApplication verify];
- [GULSwizzler unswizzleClass:[UIApplication class]
- selector:@selector(sharedApplication)
- isClassSelector:YES];
- }
- // Verifies that the handler doesn't handle non-exist error.
- - (void)testNoError {
- __block BOOL completionCalled = NO;
- BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:@{ @"abc" : @123 }
- completion:^() {
- completionCalled = YES;
- }];
- XCTAssertFalse(result);
- XCTAssertTrue(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- }
- // Verifies that the handler doesn't handle non-EMM error.
- - (void)testNoEMMError {
- __block BOOL completionCalled = NO;
- NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"invalid_token" };
- BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
- completion:^() {
- completionCalled = YES;
- }];
- XCTAssertFalse(result);
- XCTAssertTrue(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- }
- // Verifies that the handler handles general EMM error with user tapping 'OK'.
- - (void)testGeneralEMMErrorOK {
- __block BOOL completionCalled = NO;
- NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"emm_something_wrong" };
- BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
- completion:^() {
- completionCalled = YES;
- }];
- if (![UIAlertController class]) {
- XCTAssertFalse(result);
- XCTAssertTrue(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- return;
- }
- XCTAssertTrue(result);
- XCTAssertFalse(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- // Should handle no more error while the previous one is being handled.
- __block BOOL secondCompletionCalled = NO;
- BOOL secondResult = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
- completion:^() {
- secondCompletionCalled = YES;
- }];
- XCTAssertFalse(secondResult);
- XCTAssertTrue(secondCompletionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- // Wait for the code under test to be executed on the main thread.
- XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
- dispatch_async(dispatch_get_main_queue(), ^() {
- [expectation fulfill];
- });
- [self waitForExpectationsWithTimeout:1 handler:nil];
- XCTAssertFalse(completionCalled);
- XCTAssertTrue(_keyWindowSet);
- XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
- UIAlertController *alert = (UIAlertController *)_presentedViewController;
- XCTAssertNotNil(alert.title);
- XCTAssertNotNil(alert.message);
- XCTAssertEqual(alert.actions.count, 1);
- // Pretend to touch the "OK" button.
- UIAlertAction *action = alert.actions[0];
- XCTAssertEqualObjects(action.title, @"OK");
- action.actionHandler(action);
- XCTAssertTrue(completionCalled);
- }
- // Verifies that the handler handles EMM screenlock required error with user tapping 'Cancel'.
- - (void)testScreenlockRequiredCancel {
- if (_isIOS10) {
- // The dialog is different on iOS 10.
- return;
- }
- __block BOOL completionCalled = NO;
- NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"emm_passcode_required" };
- BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
- completion:^() {
- completionCalled = YES;
- }];
- if (![UIAlertController class]) {
- XCTAssertFalse(result);
- XCTAssertTrue(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- return;
- }
- XCTAssertTrue(result);
- XCTAssertFalse(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- // Wait for the code under test to be executed on the main thread.
- XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
- dispatch_async(dispatch_get_main_queue(), ^() {
- [expectation fulfill];
- });
- [self waitForExpectationsWithTimeout:1 handler:nil];
- XCTAssertFalse(completionCalled);
- XCTAssertTrue(_keyWindowSet);
- XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
- UIAlertController *alert = (UIAlertController *)_presentedViewController;
- XCTAssertNotNil(alert.title);
- XCTAssertNotNil(alert.message);
- XCTAssertEqual(alert.actions.count, 2);
- // Pretend to touch the "Cancel" button.
- UIAlertAction *action = alert.actions[0];
- XCTAssertEqualObjects(action.title, @"Cancel");
- action.actionHandler(action);
- XCTAssertTrue(completionCalled);
- }
- // Verifies that the handler handles EMM screenlock required error with user tapping 'Settings'.
- - (void)testScreenlockRequiredSettings {
- if (_isIOS10) {
- // The dialog is different on iOS 10.
- return;
- }
- __block BOOL completionCalled = NO;
- NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"emm_passcode_required" };
- BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
- completion:^() {
- completionCalled = YES;
- }];
- if (![UIAlertController class]) {
- XCTAssertFalse(result);
- XCTAssertTrue(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- return;
- }
- XCTAssertTrue(result);
- XCTAssertFalse(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- // Wait for the code under test to be executed on the main thread.
- XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
- dispatch_async(dispatch_get_main_queue(), ^() {
- [expectation fulfill];
- });
- [self waitForExpectationsWithTimeout:1 handler:nil];
- XCTAssertFalse(completionCalled);
- XCTAssertTrue(_keyWindowSet);
- XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
- UIAlertController *alert = (UIAlertController *)_presentedViewController;
- XCTAssertNotNil(alert.title);
- XCTAssertNotNil(alert.message);
- XCTAssertEqual(alert.actions.count, 2);
- // Pretend to touch the "Settings" button.
- UIAlertAction *action = alert.actions[1];
- XCTAssertEqualObjects(action.title, @"Settings");
- [self expectOpenURLString:UIApplicationOpenSettingsURLString inAction:^() {
- action.actionHandler(action);
- }];
- XCTAssertTrue(completionCalled);
- }
- - (void)testScreenlockRequiredOkOnIOS10 {
- if (!_isIOS10) {
- // A more useful dialog is used for other iOS versions.
- return;
- }
- __block BOOL completionCalled = NO;
- NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"emm_passcode_required" };
- BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
- completion:^() {
- completionCalled = YES;
- }];
- if (![UIAlertController class]) {
- XCTAssertFalse(result);
- XCTAssertTrue(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- return;
- }
- XCTAssertTrue(result);
- XCTAssertFalse(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- // Wait for the code under test to be executed on the main thread.
- XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
- dispatch_async(dispatch_get_main_queue(), ^() {
- [expectation fulfill];
- });
- [self waitForExpectationsWithTimeout:1 handler:nil];
- XCTAssertFalse(completionCalled);
- XCTAssertTrue(_keyWindowSet);
- XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
- UIAlertController *alert = (UIAlertController *)_presentedViewController;
- XCTAssertNotNil(alert.title);
- XCTAssertNotNil(alert.message);
- XCTAssertEqual(alert.actions.count, 1);
- // Pretend to touch the "OK" button.
- UIAlertAction *action = alert.actions[0];
- XCTAssertEqualObjects(action.title, @"OK");
- action.actionHandler(action);
- XCTAssertTrue(completionCalled);
- }
- // Verifies that the handler handles EMM app verification required error without a URL.
- - (void)testAppVerificationNoURL {
- __block BOOL completionCalled = NO;
- NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"emm_app_verification_required" };
- BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
- completion:^() {
- completionCalled = YES;
- }];
- if (![UIAlertController class]) {
- XCTAssertFalse(result);
- XCTAssertTrue(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- return;
- }
- XCTAssertTrue(result);
- XCTAssertFalse(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- // Wait for the code under test to be executed on the main thread.
- XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
- dispatch_async(dispatch_get_main_queue(), ^() {
- [expectation fulfill];
- });
- [self waitForExpectationsWithTimeout:1 handler:nil];
- XCTAssertFalse(completionCalled);
- XCTAssertTrue(_keyWindowSet);
- XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
- UIAlertController *alert = (UIAlertController *)_presentedViewController;
- XCTAssertNotNil(alert.title);
- XCTAssertNotNil(alert.message);
- XCTAssertEqual(alert.actions.count, 1);
- // Pretend to touch the "OK" button.
- UIAlertAction *action = alert.actions[0];
- XCTAssertEqualObjects(action.title, @"OK");
- action.actionHandler(action);
- XCTAssertTrue(completionCalled);
- }
- // Verifies that the handler handles EMM app verification required error user tapping 'Cancel'.
- - (void)testAppVerificationCancel {
- __block BOOL completionCalled = NO;
- NSDictionary<NSString *, NSString *> *response =
- @{ @"error" : @"emm_app_verification_required: https://host.domain/path" };
- BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
- completion:^() {
- completionCalled = YES;
- }];
- if (![UIAlertController class]) {
- XCTAssertFalse(result);
- XCTAssertTrue(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- return;
- }
- XCTAssertTrue(result);
- XCTAssertFalse(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- // Wait for the code under test to be executed on the main thread.
- XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
- dispatch_async(dispatch_get_main_queue(), ^() {
- [expectation fulfill];
- });
- [self waitForExpectationsWithTimeout:1 handler:nil];
- XCTAssertFalse(completionCalled);
- XCTAssertTrue(_keyWindowSet);
- XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
- UIAlertController *alert = (UIAlertController *)_presentedViewController;
- XCTAssertNotNil(alert.title);
- XCTAssertNotNil(alert.message);
- XCTAssertEqual(alert.actions.count, 2);
- // Pretend to touch the "Cancel" button.
- UIAlertAction *action = alert.actions[0];
- XCTAssertEqualObjects(action.title, @"Cancel");
- action.actionHandler(action);
- XCTAssertTrue(completionCalled);
- }
- // Verifies that the handler handles EMM app verification required error user tapping 'Connect'.
- - (void)testAppVerificationConnect {
- __block BOOL completionCalled = NO;
- NSDictionary<NSString *, NSString *> *response =
- @{ @"error" : @"emm_app_verification_required: https://host.domain/path" };
- BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
- completion:^() {
- completionCalled = YES;
- }];
- if (![UIAlertController class]) {
- XCTAssertFalse(result);
- XCTAssertTrue(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- return;
- }
- XCTAssertTrue(result);
- XCTAssertFalse(completionCalled);
- XCTAssertFalse(_keyWindowSet);
- XCTAssertNil(_presentedViewController);
- // Wait for the code under test to be executed on the main thread.
- XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
- dispatch_async(dispatch_get_main_queue(), ^() {
- [expectation fulfill];
- });
- [self waitForExpectationsWithTimeout:1 handler:nil];
- XCTAssertFalse(completionCalled);
- XCTAssertTrue(_keyWindowSet);
- XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
- UIAlertController *alert = (UIAlertController *)_presentedViewController;
- XCTAssertNotNil(alert.title);
- XCTAssertNotNil(alert.message);
- XCTAssertEqual(alert.actions.count, 2);
- // Pretend to touch the "Connect" button.
- UIAlertAction *action = alert.actions[1];
- XCTAssertEqualObjects(action.title, @"Connect");
- [self expectOpenURLString:@"https://host.domain/path" inAction:^() {
- action.actionHandler(action);
- }];
- XCTAssertTrue(completionCalled);
- }
- // Verifies that the handler can handle sequential errors independently.
- - (void)testSequentialErrors {
- [self testGeneralEMMErrorOK];
- _keyWindowSet = NO;
- _presentedViewController = nil;
- [self testScreenlockRequiredCancel];
- }
- // Temporarily disable testKeyWindow for Xcode 12 and under due to unexplained failure.
- #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000
- // Verifies that the `keyWindow` internal method works on all OS versions as expected.
- - (void)testKeyWindow {
- // The original method has been swizzled in `setUp` so get its original implementation to test.
- typedef id (*KeyWindowSignature)(id, SEL);
- KeyWindowSignature keyWindowFunction = (KeyWindowSignature)
- [GULSwizzler originalImplementationForClass:[GIDEMMErrorHandler class]
- selector:@selector(keyWindow)
- isClassSelector:NO];
- UIWindow *mockKeyWindow = OCMClassMock([UIWindow class]);
- OCMStub(mockKeyWindow.isKeyWindow).andReturn(YES);
- UIApplication *mockApplication = OCMClassMock([UIApplication class]);
- [GULSwizzler swizzleClass:[UIApplication class]
- selector:@selector(sharedApplication)
- isClassSelector:YES
- withBlock:^{ return mockApplication; }];
- #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000
- if (@available(iOS 15, *)) {
- UIWindowScene *mockWindowScene = OCMClassMock([UIWindowScene class]);
- OCMStub(mockApplication.connectedScenes).andReturn(@[mockWindowScene]);
- OCMStub(mockWindowScene.activationState).andReturn(UISceneActivationStateForegroundActive);
- OCMStub(mockWindowScene.keyWindow).andReturn(mockKeyWindow);
- } else
- #endif // __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000
- {
- #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_15_0
- if (@available(iOS 13, *)) {
- OCMStub(mockApplication.windows).andReturn(@[mockKeyWindow]);
- } else {
- #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_13_0
- OCMStub(mockApplication.keyWindow).andReturn(mockKeyWindow);
- #endif // __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_13_0
- }
- #endif // __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_15_0
- }
- UIWindow *keyWindow =
- keyWindowFunction([GIDEMMErrorHandler sharedInstance], @selector(keyWindow));
- XCTAssertEqual(keyWindow, mockKeyWindow);
- [GULSwizzler unswizzleClass:[UIApplication class]
- selector:@selector(sharedApplication)
- isClassSelector:YES];
- }
- #endif // __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000
- @end
- NS_ASSUME_NONNULL_END
- #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
|