GIDEMMSupportTest.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. /*
  2. * Copyright 2022 Google LLC
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <TargetConditionals.h>
  17. #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
  18. #import <UIKit/UIKit.h>
  19. #import <XCTest/XCTest.h>
  20. #import "GoogleSignIn/Sources/GIDEMMSupport.h"
  21. #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
  22. #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
  23. #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
  24. #import "GoogleSignIn/Sources/GIDMDMPasscodeState.h"
  25. #import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h"
  26. #import "GoogleSignIn/Tests/Unit/GIDFailingOIDAuthState.h"
  27. #import "GoogleSignIn/Tests/Unit/GIDFakeFetcher.h"
  28. #import "GoogleSignIn/Tests/Unit/GIDFakeFetcherService.h"
  29. #import "GoogleSignIn/Tests/Unit/UIAlertAction+Testing.h"
  30. #ifdef SWIFT_PACKAGE
  31. @import AppAuth;
  32. @import GoogleUtilities_MethodSwizzler;
  33. @import GoogleUtilities_SwizzlerTestHelpers;
  34. @import OCMock;
  35. #else
  36. #import <GoogleUtilities/GULSwizzler.h>
  37. #import <GoogleUtilities/GULSwizzler+Unswizzle.h>
  38. #import <AppAuth/OIDError.h>
  39. #import <OCMock/OCMock.h>
  40. #endif
  41. NS_ASSUME_NONNULL_BEGIN
  42. // The system name in old iOS versions.
  43. static NSString *const kOldIOSName = @"iPhone OS";
  44. // The system name in new iOS versions.
  45. static NSString *const kNewIOSName = @"iOS";
  46. // They keys in EMM dictionary.
  47. static NSString *const kEMMKey = @"emm_support";
  48. static NSString *const kDeviceOSKey = @"device_os";
  49. static NSString *const kEMMPasscodeInfoKey = @"emm_passcode_info";
  50. @interface GIDEMMSupportTest : XCTestCase
  51. // The view controller that has been presented, if any.
  52. @property(nonatomic, strong, nullable) UIViewController *presentedViewController;
  53. @end
  54. @implementation GIDEMMSupportTest
  55. - (void)testEMMSupportDelegate {
  56. [self setupSwizzlers];
  57. XCTestExpectation *emmErrorExpectation = [self expectationWithDescription:@"EMM AppAuth error"];
  58. GIDFailingOIDAuthState *failingAuthState = [GIDFailingOIDAuthState testInstance];
  59. GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:failingAuthState profileData:nil];
  60. GIDFakeFetcherService *fakeFetcherService = [[GIDFakeFetcherService alloc]
  61. initWithAuthorizer:user.fetcherAuthorizer];
  62. NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@""]];
  63. GTMSessionFetcher *fetcher = [fakeFetcherService fetcherWithRequest:request];
  64. [fetcher beginFetchWithCompletionHandler:^(NSData * _Nullable data, NSError * _Nullable error) {
  65. XCTAssertNotNil(error);
  66. NSDictionary<NSString *, id> *userInfo = @{
  67. @"OIDOAuthErrorResponseErrorKey": @{@"error": @"emm_passcode_required"},
  68. NSUnderlyingErrorKey: [NSError errorWithDomain:@"SomeUnderlyingError" code:0 userInfo:nil]
  69. };
  70. NSError *expectedError = [NSError errorWithDomain:kGIDSignInErrorDomain
  71. code:kGIDSignInErrorCodeEMM
  72. userInfo:userInfo];
  73. XCTAssertEqualObjects(expectedError, error);
  74. [emmErrorExpectation fulfill];
  75. }];
  76. // Wait for the code under test to be executed on the main thread.
  77. XCTestExpectation *mainThreadExpectation =
  78. [self expectationWithDescription:@"wait for main thread"];
  79. dispatch_async(dispatch_get_main_queue(), ^() {
  80. [mainThreadExpectation fulfill];
  81. });
  82. [self waitForExpectations:@[mainThreadExpectation] timeout:1];
  83. XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
  84. UIAlertController *alert = (UIAlertController *)_presentedViewController;
  85. XCTAssertNotNil(alert.title);
  86. XCTAssertNotNil(alert.message);
  87. XCTAssertEqual(alert.actions.count, 2);
  88. // Pretend to touch the "Cancel" button.
  89. UIAlertAction *action = alert.actions[0];
  90. XCTAssertEqualObjects(action.title, @"Cancel");
  91. action.actionHandler(action);
  92. [self waitForExpectations:@[emmErrorExpectation] timeout:1];
  93. [self unswizzle];
  94. }
  95. - (void)testUpdatedEMMParametersWithParameters_NoEMMKey {
  96. NSDictionary *originalParameters = @{
  97. @"not_emm_support_key" : @"xyz",
  98. };
  99. NSDictionary *updatedEMMParameters =
  100. [GIDEMMSupport updatedEMMParametersWithParameters:originalParameters];
  101. XCTAssertEqualObjects(updatedEMMParameters, originalParameters);
  102. }
  103. - (void)testUpdateEMMParametersWithParameters_systemName {
  104. [GULSwizzler swizzleClass:[UIDevice class]
  105. selector:@selector(systemName)
  106. isClassSelector:NO
  107. withBlock:^(id sender) { return kNewIOSName; }];
  108. NSDictionary *originalParameters = @{
  109. kEMMKey : @"xyz",
  110. };
  111. NSDictionary *updatedEMMParameters =
  112. [GIDEMMSupport updatedEMMParametersWithParameters:originalParameters];
  113. NSDictionary *expectedParameters = @{
  114. kEMMKey : @"xyz",
  115. kDeviceOSKey : [NSString stringWithFormat:@"%@ %@", kNewIOSName, [self systemVersion]]
  116. };
  117. XCTAssertEqualObjects(updatedEMMParameters, expectedParameters);
  118. [self addTeardownBlock:^{
  119. [GULSwizzler unswizzleClass:[UIDevice class]
  120. selector:@selector(systemName)
  121. isClassSelector:NO];
  122. }];
  123. }
  124. // When the systemName is @"iPhone OS" we still get "iOS".
  125. - (void)testUpdateEMMParametersWithParameters_systemNameNormalization {
  126. [GULSwizzler swizzleClass:[UIDevice class]
  127. selector:@selector(systemName)
  128. isClassSelector:NO
  129. withBlock:^(id sender) { return kOldIOSName; }];
  130. NSDictionary *originalParameters = @{
  131. kEMMKey : @"xyz",
  132. };
  133. NSDictionary *updatedEMMParameters =
  134. [GIDEMMSupport updatedEMMParametersWithParameters:originalParameters];
  135. NSDictionary *expectedParameters = @{
  136. kEMMKey : @"xyz",
  137. kDeviceOSKey : [NSString stringWithFormat:@"%@ %@", kNewIOSName, [self systemVersion]]
  138. };
  139. XCTAssertEqualObjects(updatedEMMParameters, expectedParameters);
  140. [self addTeardownBlock:^{
  141. [GULSwizzler unswizzleClass:[UIDevice class]
  142. selector:@selector(systemName)
  143. isClassSelector:NO];
  144. }];
  145. }
  146. - (void)testUpdateEMMParametersWithParameters_passcodInfo {
  147. [GULSwizzler swizzleClass:[UIDevice class]
  148. selector:@selector(systemName)
  149. isClassSelector:NO
  150. withBlock:^(id sender) { return kOldIOSName; }];
  151. NSDictionary *originalParameters = @{
  152. kEMMKey : @"xyz",
  153. kDeviceOSKey : @"old one",
  154. kEMMPasscodeInfoKey : @"something",
  155. };
  156. NSDictionary *updatedEMMParameters =
  157. [GIDEMMSupport updatedEMMParametersWithParameters:originalParameters];
  158. NSDictionary *expectedParameters = @{
  159. kEMMKey : @"xyz",
  160. kDeviceOSKey : [NSString stringWithFormat:@"%@ %@", kNewIOSName, [self systemVersion]],
  161. kEMMPasscodeInfoKey : [GIDMDMPasscodeState passcodeState].info,
  162. };
  163. XCTAssertEqualObjects(updatedEMMParameters, expectedParameters);
  164. [self addTeardownBlock:^{
  165. [GULSwizzler unswizzleClass:[UIDevice class]
  166. selector:@selector(systemName)
  167. isClassSelector:NO];
  168. }];
  169. }
  170. - (void)testHandleTokenFetchEMMError_errorIsEMM {
  171. // Set expectations.
  172. NSDictionary *errorJSON = @{ @"error" : @"EMM Specific Error" };
  173. NSError *emmError = [NSError errorWithDomain:@"anydomain"
  174. code:12345
  175. userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }];
  176. id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]);
  177. [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance];
  178. __block void (^savedCompletion)(void);
  179. [[[mockEMMErrorHandler stub] andReturnValue:@YES]
  180. handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) {
  181. savedCompletion = arg;
  182. return YES;
  183. }]];
  184. XCTestExpectation *notCalled = [self expectationWithDescription:@"Callback is not called"];
  185. notCalled.inverted = YES;
  186. XCTestExpectation *called = [self expectationWithDescription:@"Callback is called"];
  187. [GIDEMMSupport handleTokenFetchEMMError:emmError completion:^(NSError *error) {
  188. [notCalled fulfill];
  189. [called fulfill];
  190. XCTAssertEqualObjects(error.domain, kGIDSignInErrorDomain);
  191. XCTAssertEqual(error.code, kGIDSignInErrorCodeEMM);
  192. }];
  193. [self waitForExpectations:@[ notCalled ] timeout:1];
  194. savedCompletion();
  195. [self waitForExpectations:@[ called ] timeout:1];
  196. }
  197. - (void)testHandleTokenFetchEMMError_errorIsNotEMM {
  198. // Set expectations.
  199. NSDictionary *errorJSON = @{ @"error" : @"Not EMM Specific Error" };
  200. NSError *emmError = [NSError errorWithDomain:@"anydomain"
  201. code:12345
  202. userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }];
  203. id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]);
  204. [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance];
  205. __block void (^savedCompletion)(void);
  206. [[[mockEMMErrorHandler stub] andReturnValue:@NO]
  207. handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) {
  208. savedCompletion = arg;
  209. return YES;
  210. }]];
  211. XCTestExpectation *notCalled = [self expectationWithDescription:@"Callback is not called"];
  212. notCalled.inverted = YES;
  213. XCTestExpectation *called = [self expectationWithDescription:@"Callback is called"];
  214. [GIDEMMSupport handleTokenFetchEMMError:emmError completion:^(NSError *error) {
  215. [notCalled fulfill];
  216. [called fulfill];
  217. XCTAssertEqualObjects(error.domain, @"anydomain");
  218. XCTAssertEqual(error.code, 12345);
  219. }];
  220. [self waitForExpectations:@[ notCalled ] timeout:1];
  221. savedCompletion();
  222. [self waitForExpectations:@[ called ] timeout:1];
  223. }
  224. # pragma mark - String Conversion Tests
  225. - (void)testParametersWithParameters_withAnyNumber_isConvertedToString {
  226. NSDictionary *inputParameters = @{ @"number_key": @12345 };
  227. NSDictionary *stringifiedParameters = [GIDEMMSupport parametersWithParameters:inputParameters
  228. emmSupport:@"1"
  229. isPasscodeInfoRequired:NO];
  230. XCTAssertEqualObjects(stringifiedParameters[@"number_key"], @"12345",
  231. @"The NSNumber should be converted to a string.");
  232. XCTAssertTrue([stringifiedParameters[@"number_key"] isKindOfClass:[NSString class]],
  233. @"The final value should be of a NSString type.");
  234. }
  235. - (void)testParametersWithParameters_withNumberOne_isConvertedToString {
  236. NSDictionary *inputParameters = @{ @"number_key": @1 };
  237. NSDictionary *stringifiedParameters = [GIDEMMSupport parametersWithParameters:inputParameters
  238. emmSupport:@"1"
  239. isPasscodeInfoRequired:NO];
  240. XCTAssertEqualObjects(stringifiedParameters[@"number_key"], @"1",
  241. @"The NSNumber should be converted to a string.");
  242. XCTAssertTrue([stringifiedParameters[@"number_key"] isKindOfClass:[NSString class]],
  243. @"The final value should be of a NSString type.");
  244. }
  245. - (void)testParametersWithParameters_withNumberZero_isConvertedToString {
  246. NSDictionary *inputParameters = @{ @"number_key": @0};
  247. NSDictionary *stringifiedParameters = [GIDEMMSupport parametersWithParameters:inputParameters
  248. emmSupport:@"1"
  249. isPasscodeInfoRequired:NO];
  250. XCTAssertEqualObjects(stringifiedParameters[@"number_key"], @"0",
  251. @"The NSNumber should be converted to a string.");
  252. XCTAssertTrue([stringifiedParameters[@"number_key"] isKindOfClass:[NSString class]],
  253. @"The final value should be of a NSString type.");
  254. }
  255. - (void)testParametersWithParameters_withBooleanYes_isConvertedToTrueString {
  256. NSDictionary *inputParameters = @{ @"bool_key": @YES };
  257. NSDictionary *stringifiedParameters = [GIDEMMSupport parametersWithParameters:inputParameters
  258. emmSupport:@"1"
  259. isPasscodeInfoRequired:NO];
  260. XCTAssertEqualObjects(stringifiedParameters[@"bool_key"], @"true",
  261. @"The boolean YES should be converted to the string 'true'.");
  262. XCTAssertTrue([stringifiedParameters[@"bool_key"] isKindOfClass:[NSString class]],
  263. @"The final value should be of a NSString type.");
  264. }
  265. - (void)testParametersWithParameters_withBooleanNo_isConvertedToFalseString {
  266. NSDictionary *inputParameters = @{ @"bool_key": @NO };
  267. NSDictionary *stringifiedParameters = [GIDEMMSupport parametersWithParameters:inputParameters
  268. emmSupport:@"1"
  269. isPasscodeInfoRequired:NO];
  270. XCTAssertEqualObjects(stringifiedParameters[@"bool_key"], @"false",
  271. @"The boolean NO should be converted to the string 'false'.");
  272. XCTAssertTrue([stringifiedParameters[@"bool_key"] isKindOfClass:[NSString class]],
  273. @"The final value should be of a NSString type.");
  274. }
  275. - (void)testParametersWithParameters_withString_remainsUnchanged {
  276. NSDictionary *inputParameters = @{ @"string_key": @"hello" };
  277. NSDictionary *stringifiedParameters = [GIDEMMSupport parametersWithParameters:inputParameters
  278. emmSupport:@"1"
  279. isPasscodeInfoRequired:NO];
  280. XCTAssertEqualObjects(stringifiedParameters[@"string_key"], @"hello",
  281. @"The original string value should be preserved.");
  282. XCTAssertTrue([stringifiedParameters[@"string_key"] isKindOfClass:[NSString class]],
  283. @"The final value should be of a NSString type.");
  284. }
  285. # pragma mark - Helpers
  286. - (NSString *)systemVersion {
  287. return [UIDevice currentDevice].systemVersion;
  288. }
  289. - (void)setupSwizzlers {
  290. UIWindow *fakeKeyWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  291. [GULSwizzler swizzleClass:[GIDEMMErrorHandler class]
  292. selector:@selector(keyWindow)
  293. isClassSelector:NO
  294. withBlock:^() { return fakeKeyWindow; }];
  295. [GULSwizzler swizzleClass:[UIViewController class]
  296. selector:@selector(presentViewController:animated:completion:)
  297. isClassSelector:NO
  298. withBlock:^(id obj, id arg1) { self->_presentedViewController = arg1; }];
  299. }
  300. - (void)unswizzle {
  301. [GULSwizzler unswizzleClass:[GIDEMMErrorHandler class]
  302. selector:@selector(keyWindow)
  303. isClassSelector:NO];
  304. [GULSwizzler unswizzleClass:[UIViewController class]
  305. selector:@selector(presentViewController:animated:completion:)
  306. isClassSelector:NO];
  307. self.presentedViewController = nil;
  308. }
  309. @end
  310. NS_ASSUME_NONNULL_END
  311. #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST