FIRAppCheckAPIServiceTests.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. /*
  2. * Copyright 2020 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 <XCTest/XCTest.h>
  17. #import <OCMock/OCMock.h>
  18. #import "FBLPromise+Testing.h"
  19. @import HeartbeatLoggingTestUtils;
  20. #import <GoogleUtilities/GULURLSessionDataResponse.h>
  21. #import <GoogleUtilities/NSURLSession+GULPromises.h>
  22. #import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h"
  23. #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h"
  24. #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckErrors.h"
  25. #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h"
  26. #import "FirebaseAppCheck/Tests/Unit/Utils/FIRFixtureLoader.h"
  27. #import "SharedTestUtilities/Date/FIRDateTestUtils.h"
  28. #import "SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.h"
  29. #import "FirebaseCore/Extension/FirebaseCoreInternal.h"
  30. #pragma mark - Fakes
  31. /// A fake heartbeat logger used for dependency injection during testing.
  32. @interface FIRHeartbeatLoggerFake : NSObject <FIRHeartbeatLoggerProtocol>
  33. @property(nonatomic, copy, nullable) FIRHeartbeatsPayload * (^onFlushHeartbeatsIntoPayloadHandler)
  34. (void);
  35. @property(nonatomic, copy, nullable) FIRDailyHeartbeatCode (^onHeartbeatCodeForTodayHandler)(void);
  36. @end
  37. @implementation FIRHeartbeatLoggerFake
  38. - (nonnull FIRHeartbeatsPayload *)flushHeartbeatsIntoPayload {
  39. if (self.onFlushHeartbeatsIntoPayloadHandler) {
  40. return self.onFlushHeartbeatsIntoPayloadHandler();
  41. } else {
  42. return nil;
  43. }
  44. }
  45. - (FIRDailyHeartbeatCode)heartbeatCodeForToday {
  46. // This API should not be used by the below tests because the AppCheck SDK
  47. // uses only the V2 heartbeat API (`flushHeartbeatsIntoPayload`) for getting
  48. // heartbeats.
  49. [self doesNotRecognizeSelector:_cmd];
  50. return FIRDailyHeartbeatCodeNone;
  51. }
  52. - (void)log {
  53. // This API should not be used by the below tests because the AppCheck SDK
  54. // does not log heartbeats in it's networking context.
  55. [self doesNotRecognizeSelector:_cmd];
  56. }
  57. @end
  58. #pragma mark - FIRAppCheckAPIServiceTests
  59. @interface FIRAppCheckAPIServiceTests : XCTestCase
  60. @property(nonatomic) FIRAppCheckAPIService *APIService;
  61. @property(nonatomic) id mockURLSession;
  62. @property(nonatomic) NSString *APIKey;
  63. @property(nonatomic) NSString *appID;
  64. @property(nonatomic) FIRHeartbeatLoggerFake *heartbeatLoggerFake;
  65. @end
  66. @implementation FIRAppCheckAPIServiceTests
  67. - (void)setUp {
  68. [super setUp];
  69. self.APIKey = @"api_key";
  70. self.appID = @"app_id";
  71. self.mockURLSession = OCMStrictClassMock([NSURLSession class]);
  72. self.heartbeatLoggerFake = [[FIRHeartbeatLoggerFake alloc] init];
  73. self.APIService = [[FIRAppCheckAPIService alloc] initWithURLSession:self.mockURLSession
  74. APIKey:self.APIKey
  75. appID:self.appID
  76. heartbeatLogger:self.heartbeatLoggerFake];
  77. }
  78. - (void)tearDown {
  79. [super tearDown];
  80. self.APIService = nil;
  81. [self.mockURLSession stopMocking];
  82. self.mockURLSession = nil;
  83. }
  84. - (void)testDataRequestSuccessWhenNoHeartbeatsNeedSending {
  85. // Given
  86. FIRHeartbeatsPayload *emptyHeartbeatsPayload =
  87. [FIRHeartbeatLoggingTestUtils emptyHeartbeatsPayload];
  88. // When
  89. self.heartbeatLoggerFake.onFlushHeartbeatsIntoPayloadHandler = ^FIRHeartbeatsPayload * {
  90. return emptyHeartbeatsPayload;
  91. };
  92. // Then
  93. [self assertDataRequestSuccessWhenSendingHeartbeatsPayload:emptyHeartbeatsPayload];
  94. }
  95. - (void)testDataRequestSuccessWhenHeartbeatsNeedSending {
  96. // Given
  97. FIRHeartbeatsPayload *nonEmptyHeartbeatsPayload =
  98. [FIRHeartbeatLoggingTestUtils nonEmptyHeartbeatsPayload];
  99. // When
  100. self.heartbeatLoggerFake.onFlushHeartbeatsIntoPayloadHandler = ^FIRHeartbeatsPayload * {
  101. return nonEmptyHeartbeatsPayload;
  102. };
  103. // Then
  104. [self assertDataRequestSuccessWhenSendingHeartbeatsPayload:nonEmptyHeartbeatsPayload];
  105. }
  106. - (void)testDataRequestNetworkError {
  107. NSURL *URL = [NSURL URLWithString:@"https://some.url.com"];
  108. NSDictionary *additionalHeaders = @{@"header1" : @"value1"};
  109. NSData *requestBody = [@"Request body" dataUsingEncoding:NSUTF8StringEncoding];
  110. // 1. Stub URL session.
  111. NSError *networkError = [NSError errorWithDomain:self.name code:-1 userInfo:nil];
  112. [self stubURLSessionDataTaskPromiseWithResponse:nil
  113. body:nil
  114. error:networkError
  115. URLSessionMock:self.mockURLSession
  116. requestValidationBlock:nil];
  117. // 2. Send request.
  118. __auto_type requestPromise = [self.APIService sendRequestWithURL:URL
  119. HTTPMethod:@"POST"
  120. body:requestBody
  121. additionalHeaders:additionalHeaders];
  122. // 3. Verify.
  123. XCTAssert(FBLWaitForPromisesWithTimeout(1));
  124. XCTAssertTrue(requestPromise.isRejected);
  125. XCTAssertNotNil(requestPromise.error);
  126. XCTAssertEqualObjects(requestPromise.error.domain, FIRAppCheckErrorDomain);
  127. XCTAssertEqual(requestPromise.error.code, FIRAppCheckErrorCodeServerUnreachable);
  128. XCTAssertEqualObjects(requestPromise.error.userInfo[NSUnderlyingErrorKey], networkError);
  129. OCMVerifyAll(self.mockURLSession);
  130. }
  131. - (void)testDataRequestNot2xxHTTPStatusCode {
  132. NSURL *URL = [NSURL URLWithString:@"https://some.url.com"];
  133. NSData *requestBody = [@"Request body" dataUsingEncoding:NSUTF8StringEncoding];
  134. NSString *responseBodyString = @"Token verification failed.";
  135. NSData *HTTPResponseBody = [responseBodyString dataUsingEncoding:NSUTF8StringEncoding];
  136. NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:300];
  137. [self stubURLSessionDataTaskPromiseWithResponse:HTTPResponse
  138. body:HTTPResponseBody
  139. error:nil
  140. URLSessionMock:self.mockURLSession
  141. requestValidationBlock:nil];
  142. // 2. Send request.
  143. __auto_type requestPromise = [self.APIService sendRequestWithURL:URL
  144. HTTPMethod:@"POST"
  145. body:requestBody
  146. additionalHeaders:nil];
  147. // 3. Verify.
  148. XCTAssert(FBLWaitForPromisesWithTimeout(1));
  149. XCTAssertTrue(requestPromise.isRejected);
  150. XCTAssertNil(requestPromise.value);
  151. XCTAssertNotNil(requestPromise.error);
  152. XCTAssertEqualObjects(requestPromise.error.domain, FIRAppCheckErrorDomain);
  153. XCTAssertEqual(requestPromise.error.code, FIRAppCheckErrorCodeUnknown);
  154. // Expect response body and HTTP status code to be included in the error.
  155. NSString *failureReason = requestPromise.error.userInfo[NSLocalizedFailureReasonErrorKey];
  156. XCTAssertNotNil(failureReason);
  157. XCTAssertTrue([failureReason containsString:@"300"]);
  158. XCTAssertTrue([failureReason containsString:responseBodyString]);
  159. OCMVerifyAll(self.mockURLSession);
  160. }
  161. #pragma mark - Token Exchange API response
  162. - (void)testAppCheckTokenWithAPIResponseValidResponse {
  163. // 1. Prepare input parameters.
  164. NSData *responseBody =
  165. [FIRFixtureLoader loadFixtureNamed:@"FACTokenExchangeResponseSuccess.json"];
  166. XCTAssertNotNil(responseBody);
  167. NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:200];
  168. GULURLSessionDataResponse *APIResponse =
  169. [[GULURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody];
  170. // 2. Expected result.
  171. NSString *expectedFACToken = @"valid_app_check_token";
  172. // 3. Parse API response.
  173. __auto_type tokenPromise = [self.APIService appCheckTokenWithAPIResponse:APIResponse];
  174. // 4. Verify.
  175. XCTAssert(FBLWaitForPromisesWithTimeout(1));
  176. XCTAssertTrue(tokenPromise.isFulfilled);
  177. XCTAssertNil(tokenPromise.error);
  178. XCTAssertEqualObjects(tokenPromise.value.token, expectedFACToken);
  179. XCTAssertTrue([FIRDateTestUtils isDate:tokenPromise.value.expirationDate
  180. approximatelyEqualCurrentPlusTimeInterval:1800
  181. precision:10]);
  182. }
  183. - (void)testAppCheckTokenWithAPIResponseInvalidFormat {
  184. // 1. Prepare input parameters.
  185. NSString *responseBodyString = @"Token verification failed.";
  186. NSData *responseBody = [responseBodyString dataUsingEncoding:NSUTF8StringEncoding];
  187. NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:200];
  188. GULURLSessionDataResponse *APIResponse =
  189. [[GULURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody];
  190. // 2. Parse API response.
  191. __auto_type tokenPromise = [self.APIService appCheckTokenWithAPIResponse:APIResponse];
  192. // 3. Verify.
  193. XCTAssert(FBLWaitForPromisesWithTimeout(1));
  194. XCTAssertTrue(tokenPromise.isRejected);
  195. XCTAssertNil(tokenPromise.value);
  196. XCTAssertNotNil(tokenPromise.error);
  197. XCTAssertEqualObjects(tokenPromise.error.domain, FIRAppCheckErrorDomain);
  198. XCTAssertEqual(tokenPromise.error.code, FIRAppCheckErrorCodeUnknown);
  199. // Expect response body and HTTP status code to be included in the error.
  200. NSString *failureReason = tokenPromise.error.userInfo[NSLocalizedFailureReasonErrorKey];
  201. XCTAssertEqualObjects(failureReason, @"JSON serialization error.");
  202. }
  203. - (void)testAppCheckTokenResponseMissingFields {
  204. [self assertMissingFieldErrorWithFixture:@"DeviceCheckResponseMissingToken.json"
  205. missingField:@"token"];
  206. [self assertMissingFieldErrorWithFixture:@"DeviceCheckResponseMissingTimeToLive.json"
  207. missingField:@"ttl"];
  208. }
  209. - (void)assertMissingFieldErrorWithFixture:(NSString *)fixtureName
  210. missingField:(NSString *)fieldName {
  211. // 1. Parse API response.
  212. NSData *missingFiledBody = [FIRFixtureLoader loadFixtureNamed:fixtureName];
  213. XCTAssertNotNil(missingFiledBody);
  214. NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:200];
  215. GULURLSessionDataResponse *APIResponse =
  216. [[GULURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:missingFiledBody];
  217. // 2. Parse API response.
  218. __auto_type tokenPromise = [self.APIService appCheckTokenWithAPIResponse:APIResponse];
  219. // 3. Verify.
  220. XCTAssert(FBLWaitForPromisesWithTimeout(1));
  221. XCTAssertTrue(tokenPromise.isRejected);
  222. XCTAssertNil(tokenPromise.value);
  223. XCTAssertNotNil(tokenPromise.error);
  224. XCTAssertEqualObjects(tokenPromise.error.domain, FIRAppCheckErrorDomain);
  225. XCTAssertEqual(tokenPromise.error.code, FIRAppCheckErrorCodeUnknown);
  226. // Expect missing field name to be included in the error.
  227. NSString *failureReason = tokenPromise.error.userInfo[NSLocalizedFailureReasonErrorKey];
  228. NSString *fieldNameString = [NSString stringWithFormat:@"`%@`", fieldName];
  229. XCTAssertTrue([failureReason containsString:fieldNameString],
  230. @"Fixture `%@`: expected missing field %@ error not found", fixtureName,
  231. fieldNameString);
  232. }
  233. #pragma mark - Helpers
  234. - (void)assertDataRequestSuccessWhenSendingHeartbeatsPayload:
  235. (nullable FIRHeartbeatsPayload *)heartbeatsPayload {
  236. NSURL *URL = [NSURL URLWithString:@"https://some.url.com"];
  237. NSDictionary *additionalHeaders = @{@"header1" : @"value1"};
  238. NSData *requestBody = [@"Request body" dataUsingEncoding:NSUTF8StringEncoding];
  239. // 1. Stub URL session.
  240. FIRRequestValidationBlock requestValidation = ^BOOL(NSURLRequest *request) {
  241. XCTAssertEqualObjects(request.URL, URL);
  242. NSMutableDictionary<NSString *, NSString *> *expectedHTTPHeaderFields = @{
  243. @"X-Goog-Api-Key" : self.APIKey,
  244. @"X-Ios-Bundle-Identifier" : [[NSBundle mainBundle] bundleIdentifier],
  245. @"header1" : @"value1",
  246. }
  247. .mutableCopy;
  248. NSString *_Nullable heartbeatHeaderValue =
  249. FIRHeaderValueFromHeartbeatsPayload(heartbeatsPayload);
  250. if (heartbeatHeaderValue) {
  251. expectedHTTPHeaderFields[@"X-firebase-client"] = heartbeatHeaderValue;
  252. }
  253. XCTAssertEqualObjects(request.allHTTPHeaderFields, expectedHTTPHeaderFields);
  254. XCTAssertEqualObjects(request.HTTPMethod, @"POST");
  255. XCTAssertEqualObjects(request.HTTPBody, requestBody);
  256. return YES;
  257. };
  258. NSData *HTTPResponseBody = [@"A response" dataUsingEncoding:NSUTF8StringEncoding];
  259. NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:200];
  260. [self stubURLSessionDataTaskPromiseWithResponse:HTTPResponse
  261. body:HTTPResponseBody
  262. error:nil
  263. URLSessionMock:self.mockURLSession
  264. requestValidationBlock:requestValidation];
  265. // 2. Send request.
  266. __auto_type requestPromise = [self.APIService sendRequestWithURL:URL
  267. HTTPMethod:@"POST"
  268. body:requestBody
  269. additionalHeaders:additionalHeaders];
  270. // 3. Verify.
  271. XCTAssert(FBLWaitForPromisesWithTimeout(1));
  272. XCTAssertTrue(requestPromise.isFulfilled);
  273. XCTAssertNil(requestPromise.error);
  274. XCTAssertEqualObjects(requestPromise.value.HTTPResponse, HTTPResponse);
  275. XCTAssertEqualObjects(requestPromise.value.HTTPBody, HTTPResponseBody);
  276. OCMVerifyAll(self.mockURLSession);
  277. }
  278. - (void)stubURLSessionDataTaskPromiseWithResponse:(NSHTTPURLResponse *)HTTPResponse
  279. body:(NSData *)body
  280. error:(NSError *)error
  281. URLSessionMock:(id)URLSessionMock
  282. requestValidationBlock:
  283. (FIRRequestValidationBlock)requestValidationBlock {
  284. // Validate request content.
  285. FIRRequestValidationBlock nonOptionalRequestValidationBlock =
  286. requestValidationBlock ?: ^BOOL(id request) {
  287. return YES;
  288. };
  289. id URLRequestValidationArg = [OCMArg checkWithBlock:nonOptionalRequestValidationBlock];
  290. // Result promise.
  291. FBLPromise<GULURLSessionDataResponse *> *result = [FBLPromise pendingPromise];
  292. if (error == nil) {
  293. GULURLSessionDataResponse *response =
  294. [[GULURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:body];
  295. [result fulfill:response];
  296. } else {
  297. [result reject:error];
  298. }
  299. // Stub the method.
  300. OCMExpect([URLSessionMock gul_dataTaskPromiseWithRequest:URLRequestValidationArg])
  301. .andReturn(result);
  302. }
  303. @end