FIRAppCheckAPIServiceTests.m 13 KB

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