FIRAppCheckAPIServiceTests.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  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 <GoogleUtilities/GULURLSessionDataResponse.h>
  20. #import <GoogleUtilities/NSURLSession+GULPromises.h>
  21. #import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h"
  22. #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h"
  23. #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h"
  24. #import "FirebaseAppCheck/Tests/Unit/Utils/FIRFixtureLoader.h"
  25. #import "SharedTestUtilities/Date/FIRDateTestUtils.h"
  26. #import "SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.h"
  27. #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
  28. @interface FIRAppCheckAPIServiceTests : XCTestCase
  29. @property(nonatomic) FIRAppCheckAPIService *APIService;
  30. @property(nonatomic) id mockURLSession;
  31. @property(nonatomic) id mockHeartbeatInfo;
  32. @property(nonatomic) NSString *APIKey;
  33. @property(nonatomic) NSString *projectID;
  34. @property(nonatomic) NSString *appID;
  35. @end
  36. @implementation FIRAppCheckAPIServiceTests
  37. - (void)setUp {
  38. [super setUp];
  39. self.APIKey = @"api_key";
  40. self.projectID = @"project_id";
  41. self.appID = @"app_id";
  42. // Stub FIRHeartbeatInfo.
  43. self.mockHeartbeatInfo = OCMClassMock([FIRHeartbeatInfo class]);
  44. OCMStub([self.mockHeartbeatInfo heartbeatCodeForTag:@"fire-app-check"])
  45. .andDo(^(NSInvocation *invocation) {
  46. XCTAssertFalse([NSThread isMainThread]);
  47. })
  48. .andReturn(FIRHeartbeatInfoCodeCombined);
  49. self.mockURLSession = OCMStrictClassMock([NSURLSession class]);
  50. self.APIService = [[FIRAppCheckAPIService alloc] initWithURLSession:self.mockURLSession
  51. APIKey:self.APIKey
  52. projectID:self.projectID
  53. appID:self.appID];
  54. }
  55. - (void)tearDown {
  56. [super tearDown];
  57. self.APIService = nil;
  58. [self.mockURLSession stopMocking];
  59. self.mockURLSession = nil;
  60. [self.mockHeartbeatInfo stopMocking];
  61. self.mockHeartbeatInfo = nil;
  62. }
  63. - (void)testDataRequestSuccess {
  64. NSURL *URL = [NSURL URLWithString:@"https://some.url.com"];
  65. NSDictionary *additionalHeaders = @{@"header1" : @"value1"};
  66. NSData *requestBody = [@"Request body" dataUsingEncoding:NSUTF8StringEncoding];
  67. // 1. Stub URL session.
  68. FIRRequestValidationBlock requestValidation = ^BOOL(NSURLRequest *request) {
  69. XCTAssertEqualObjects(request.URL, URL);
  70. XCTAssertEqualObjects(request.allHTTPHeaderFields[@"x-firebase-client"],
  71. [FIRApp firebaseUserAgent]);
  72. XCTAssertEqualObjects(request.allHTTPHeaderFields[@"X-firebase-client-log-type"], @"3");
  73. XCTAssertEqualObjects(request.allHTTPHeaderFields[@"X-Goog-Api-Key"], self.APIKey);
  74. XCTAssertEqualObjects(request.allHTTPHeaderFields[@"header1"], @"value1");
  75. XCTAssertEqualObjects(request.HTTPMethod, @"POST");
  76. XCTAssertEqualObjects(request.HTTPBody, requestBody);
  77. return YES;
  78. };
  79. NSData *HTTPResponseBody = [@"A response" dataUsingEncoding:NSUTF8StringEncoding];
  80. NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:200];
  81. [self stubURLSessionDataTaskPromiseWithResponse:HTTPResponse
  82. body:HTTPResponseBody
  83. error:nil
  84. URLSessionMock:self.mockURLSession
  85. requestValidationBlock:requestValidation];
  86. // 2. Send request.
  87. __auto_type requestPromise = [self.APIService sendRequestWithURL:URL
  88. HTTPMethod:@"POST"
  89. body:requestBody
  90. additionalHeaders:additionalHeaders];
  91. // 3. Verify.
  92. XCTAssert(FBLWaitForPromisesWithTimeout(1));
  93. XCTAssertTrue(requestPromise.isFulfilled);
  94. XCTAssertNil(requestPromise.error);
  95. XCTAssertEqualObjects(requestPromise.value.HTTPResponse, HTTPResponse);
  96. XCTAssertEqualObjects(requestPromise.value.HTTPBody, HTTPResponseBody);
  97. OCMVerifyAll(self.mockURLSession);
  98. }
  99. - (void)testDataRequestNetworkError {
  100. NSURL *URL = [NSURL URLWithString:@"https://some.url.com"];
  101. NSDictionary *additionalHeaders = @{@"header1" : @"value1"};
  102. NSData *requestBody = [@"Request body" dataUsingEncoding:NSUTF8StringEncoding];
  103. // 1. Stub URL session.
  104. NSError *networkError = [NSError errorWithDomain:self.name code:-1 userInfo:nil];
  105. [self stubURLSessionDataTaskPromiseWithResponse:nil
  106. body:nil
  107. error:networkError
  108. URLSessionMock:self.mockURLSession
  109. requestValidationBlock:nil];
  110. // 2. Send request.
  111. __auto_type requestPromise = [self.APIService sendRequestWithURL:URL
  112. HTTPMethod:@"POST"
  113. body:requestBody
  114. additionalHeaders:additionalHeaders];
  115. // 3. Verify.
  116. XCTAssert(FBLWaitForPromisesWithTimeout(1));
  117. XCTAssertTrue(requestPromise.isRejected);
  118. XCTAssertNotNil(requestPromise.error);
  119. XCTAssertEqualObjects(requestPromise.error.domain, kFIRAppCheckErrorDomain);
  120. XCTAssertEqual(requestPromise.error.code, FIRAppCheckErrorCodeUnknown);
  121. XCTAssertEqualObjects(requestPromise.error.userInfo[NSUnderlyingErrorKey], networkError);
  122. OCMVerifyAll(self.mockURLSession);
  123. }
  124. - (void)testDataRequestNot2xxHTTPStatusCode {
  125. NSURL *URL = [NSURL URLWithString:@"https://some.url.com"];
  126. NSData *requestBody = [@"Request body" dataUsingEncoding:NSUTF8StringEncoding];
  127. NSString *responseBodyString = @"Token verification failed.";
  128. NSData *HTTPResponseBody = [responseBodyString dataUsingEncoding:NSUTF8StringEncoding];
  129. NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:300];
  130. [self stubURLSessionDataTaskPromiseWithResponse:HTTPResponse
  131. body:HTTPResponseBody
  132. error:nil
  133. URLSessionMock:self.mockURLSession
  134. requestValidationBlock:nil];
  135. // 2. Send request.
  136. __auto_type requestPromise = [self.APIService sendRequestWithURL:URL
  137. HTTPMethod:@"POST"
  138. body:requestBody
  139. additionalHeaders:nil];
  140. // 3. Verify.
  141. XCTAssert(FBLWaitForPromisesWithTimeout(1));
  142. XCTAssertTrue(requestPromise.isRejected);
  143. XCTAssertNil(requestPromise.value);
  144. XCTAssertNotNil(requestPromise.error);
  145. XCTAssertEqualObjects(requestPromise.error.domain, kFIRAppCheckErrorDomain);
  146. XCTAssertEqual(requestPromise.error.code, FIRAppCheckErrorCodeUnknown);
  147. // Expect response body and HTTP status code to be included in the error.
  148. NSString *failureReason = requestPromise.error.userInfo[NSLocalizedFailureReasonErrorKey];
  149. XCTAssertNotNil(failureReason);
  150. XCTAssertTrue([failureReason containsString:@"300"]);
  151. XCTAssertTrue([failureReason containsString:responseBodyString]);
  152. OCMVerifyAll(self.mockURLSession);
  153. }
  154. #pragma mark - Token Exchange API response
  155. - (void)testAppCheckTokenWithAPIResponseValidResponse {
  156. // 1. Prepare input parameters.
  157. NSData *responseBody =
  158. [FIRFixtureLoader loadFixtureNamed:@"FACTokenExchangeResponseSuccess.json"];
  159. XCTAssertNotNil(responseBody);
  160. NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:200];
  161. GULURLSessionDataResponse *APIResponse =
  162. [[GULURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody];
  163. // 2. Expected result.
  164. NSString *expectedFACToken = @"valid_app_check_token";
  165. // 3. Parse API response.
  166. __auto_type tokenPromise = [self.APIService appCheckTokenWithAPIResponse:APIResponse];
  167. // 4. Verify.
  168. XCTAssert(FBLWaitForPromisesWithTimeout(1));
  169. XCTAssertTrue(tokenPromise.isFulfilled);
  170. XCTAssertNil(tokenPromise.error);
  171. XCTAssertEqualObjects(tokenPromise.value.token, expectedFACToken);
  172. XCTAssertTrue([FIRDateTestUtils isDate:tokenPromise.value.expirationDate
  173. approximatelyEqualCurrentPlusTimeInterval:1800
  174. precision:10]);
  175. }
  176. - (void)testAppCheckTokenWithAPIResponseInvalidFormat {
  177. // 1. Prepare input parameters.
  178. NSString *responseBodyString = @"Token verification failed.";
  179. NSData *responseBody = [responseBodyString dataUsingEncoding:NSUTF8StringEncoding];
  180. NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:200];
  181. GULURLSessionDataResponse *APIResponse =
  182. [[GULURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody];
  183. // 2. Parse API response.
  184. __auto_type tokenPromise = [self.APIService appCheckTokenWithAPIResponse:APIResponse];
  185. // 3. Verify.
  186. XCTAssert(FBLWaitForPromisesWithTimeout(1));
  187. XCTAssertTrue(tokenPromise.isRejected);
  188. XCTAssertNil(tokenPromise.value);
  189. XCTAssertNotNil(tokenPromise.error);
  190. XCTAssertEqualObjects(tokenPromise.error.domain, kFIRAppCheckErrorDomain);
  191. XCTAssertEqual(tokenPromise.error.code, FIRAppCheckErrorCodeUnknown);
  192. // Expect response body and HTTP status code to be included in the error.
  193. NSString *failureReason = tokenPromise.error.userInfo[NSLocalizedFailureReasonErrorKey];
  194. XCTAssertEqualObjects(failureReason, @"JSON serialization error.");
  195. }
  196. - (void)testAppCheckTokenResponseMissingFields {
  197. [self assertMissingFieldErrorWithFixture:@"DeviceCheckResponseMissingToken.json"
  198. missingField:@"attestationToken"];
  199. [self assertMissingFieldErrorWithFixture:@"DeviceCheckResponseMissingTimeToLive.json"
  200. missingField:@"ttl"];
  201. }
  202. - (void)assertMissingFieldErrorWithFixture:(NSString *)fixtureName
  203. missingField:(NSString *)fieldName {
  204. // 1. Parse API response.
  205. NSData *missingFiledBody = [FIRFixtureLoader loadFixtureNamed:fixtureName];
  206. XCTAssertNotNil(missingFiledBody);
  207. NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:200];
  208. GULURLSessionDataResponse *APIResponse =
  209. [[GULURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:missingFiledBody];
  210. // 2. Parse API response.
  211. __auto_type tokenPromise = [self.APIService appCheckTokenWithAPIResponse:APIResponse];
  212. // 3. Verify.
  213. XCTAssert(FBLWaitForPromisesWithTimeout(1));
  214. XCTAssertTrue(tokenPromise.isRejected);
  215. XCTAssertNil(tokenPromise.value);
  216. XCTAssertNotNil(tokenPromise.error);
  217. XCTAssertEqualObjects(tokenPromise.error.domain, kFIRAppCheckErrorDomain);
  218. XCTAssertEqual(tokenPromise.error.code, FIRAppCheckErrorCodeUnknown);
  219. // Expect missing field name to be included in the error.
  220. NSString *failureReason = tokenPromise.error.userInfo[NSLocalizedFailureReasonErrorKey];
  221. NSString *fieldNameString = [NSString stringWithFormat:@"`%@`", fieldName];
  222. XCTAssertTrue([failureReason containsString:fieldNameString],
  223. @"Fixture `%@`: expected missing field %@ error not found", fixtureName,
  224. fieldNameString);
  225. }
  226. #pragma mark - Helpers
  227. - (void)stubURLSessionDataTaskPromiseWithResponse:(NSHTTPURLResponse *)HTTPResponse
  228. body:(NSData *)body
  229. error:(NSError *)error
  230. URLSessionMock:(id)URLSessionMock
  231. requestValidationBlock:
  232. (FIRRequestValidationBlock)requestValidationBlock {
  233. // Validate request content.
  234. FIRRequestValidationBlock nonOptionalRequestValidationBlock =
  235. requestValidationBlock ?: ^BOOL(id request) {
  236. return YES;
  237. };
  238. id URLRequestValidationArg = [OCMArg checkWithBlock:nonOptionalRequestValidationBlock];
  239. // Result promise.
  240. FBLPromise<GULURLSessionDataResponse *> *result = [FBLPromise pendingPromise];
  241. if (error == nil) {
  242. GULURLSessionDataResponse *response =
  243. [[GULURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:body];
  244. [result fulfill:response];
  245. } else {
  246. [result reject:error];
  247. }
  248. // Stub the method.
  249. OCMExpect([URLSessionMock gul_dataTaskPromiseWithRequest:URLRequestValidationArg])
  250. .andReturn(result);
  251. }
  252. @end