FIRAppCheckAPIServiceTests.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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 = [FIRFixtureLoader loadFixtureNamed:@"DeviceCheckResponseSuccess.json"];
  158. XCTAssertNotNil(responseBody);
  159. NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:200];
  160. GULURLSessionDataResponse *APIResponse =
  161. [[GULURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody];
  162. // 2. Expected result.
  163. NSString *expectedFACToken = @"valid_app_check_token";
  164. // 3. Parse API response.
  165. __auto_type tokenPromise = [self.APIService appCheckTokenWithAPIResponse:APIResponse];
  166. // 4. Verify.
  167. XCTAssert(FBLWaitForPromisesWithTimeout(1));
  168. XCTAssertTrue(tokenPromise.isFulfilled);
  169. XCTAssertNil(tokenPromise.error);
  170. XCTAssertEqualObjects(tokenPromise.value.token, expectedFACToken);
  171. XCTAssertTrue([FIRDateTestUtils isDate:tokenPromise.value.expirationDate
  172. approximatelyEqualCurrentPlusTimeInterval:1800
  173. precision:10]);
  174. }
  175. - (void)testAppCheckTokenWithAPIResponseInvalidFormat {
  176. // 1. Prepare input parameters.
  177. NSString *responseBodyString = @"Token verification failed.";
  178. NSData *responseBody = [responseBodyString dataUsingEncoding:NSUTF8StringEncoding];
  179. NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:200];
  180. GULURLSessionDataResponse *APIResponse =
  181. [[GULURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody];
  182. // 2. Parse API response.
  183. __auto_type tokenPromise = [self.APIService appCheckTokenWithAPIResponse:APIResponse];
  184. // 3. Verify.
  185. XCTAssert(FBLWaitForPromisesWithTimeout(1));
  186. XCTAssertTrue(tokenPromise.isRejected);
  187. XCTAssertNil(tokenPromise.value);
  188. XCTAssertNotNil(tokenPromise.error);
  189. XCTAssertEqualObjects(tokenPromise.error.domain, kFIRAppCheckErrorDomain);
  190. XCTAssertEqual(tokenPromise.error.code, FIRAppCheckErrorCodeUnknown);
  191. // Expect response body and HTTP status code to be included in the error.
  192. NSString *failureReason = tokenPromise.error.userInfo[NSLocalizedFailureReasonErrorKey];
  193. XCTAssertEqualObjects(failureReason, @"JSON serialization error.");
  194. }
  195. - (void)testAppCheckTokenResponseMissingFields {
  196. [self assertMissingFieldErrorWithFixture:@"DeviceCheckResponseMissingToken.json"
  197. missingField:@"attestationToken"];
  198. [self assertMissingFieldErrorWithFixture:@"DeviceCheckResponseMissingTimeToLive.json"
  199. missingField:@"ttl"];
  200. }
  201. - (void)assertMissingFieldErrorWithFixture:(NSString *)fixtureName
  202. missingField:(NSString *)fieldName {
  203. // 1. Parse API response.
  204. NSData *missingFiledBody = [FIRFixtureLoader loadFixtureNamed:fixtureName];
  205. XCTAssertNotNil(missingFiledBody);
  206. NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:200];
  207. GULURLSessionDataResponse *APIResponse =
  208. [[GULURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:missingFiledBody];
  209. // 2. Parse API response.
  210. __auto_type tokenPromise = [self.APIService appCheckTokenWithAPIResponse:APIResponse];
  211. // 3. Verify.
  212. XCTAssert(FBLWaitForPromisesWithTimeout(1));
  213. XCTAssertTrue(tokenPromise.isRejected);
  214. XCTAssertNil(tokenPromise.value);
  215. XCTAssertNotNil(tokenPromise.error);
  216. XCTAssertEqualObjects(tokenPromise.error.domain, kFIRAppCheckErrorDomain);
  217. XCTAssertEqual(tokenPromise.error.code, FIRAppCheckErrorCodeUnknown);
  218. // Expect missing field name to be included in the error.
  219. NSString *failureReason = tokenPromise.error.userInfo[NSLocalizedFailureReasonErrorKey];
  220. NSString *fieldNameString = [NSString stringWithFormat:@"`%@`", fieldName];
  221. XCTAssertTrue([failureReason containsString:fieldNameString],
  222. @"Fixture `%@`: expected missing field %@ error not found", fixtureName,
  223. fieldNameString);
  224. }
  225. #pragma mark - Helpers
  226. - (void)stubURLSessionDataTaskPromiseWithResponse:(NSHTTPURLResponse *)HTTPResponse
  227. body:(NSData *)body
  228. error:(NSError *)error
  229. URLSessionMock:(id)URLSessionMock
  230. requestValidationBlock:
  231. (FIRRequestValidationBlock)requestValidationBlock {
  232. // Validate request content.
  233. FIRRequestValidationBlock nonOptionalRequestValidationBlock =
  234. requestValidationBlock ?: ^BOOL(id request) {
  235. return YES;
  236. };
  237. id URLRequestValidationArg = [OCMArg checkWithBlock:nonOptionalRequestValidationBlock];
  238. // Result promise.
  239. FBLPromise<GULURLSessionDataResponse *> *result = [FBLPromise pendingPromise];
  240. if (error == nil) {
  241. GULURLSessionDataResponse *response =
  242. [[GULURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:body];
  243. [result fulfill:response];
  244. } else {
  245. [result reject:error];
  246. }
  247. // Stub the method.
  248. OCMExpect([URLSessionMock gul_dataTaskPromiseWithRequest:URLRequestValidationArg])
  249. .andReturn(result);
  250. }
  251. @end