FIRAppCheckBackoffWrapperTests.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. /*
  2. * Copyright 2021 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 "FBLPromise+Testing.h"
  18. #if __has_include(<FBLPromises/FBLPromises.h>)
  19. #import <FBLPromises/FBLPromises.h>
  20. #else
  21. #import "FBLPromises.h"
  22. #endif
  23. #import "FirebaseAppCheck/Sources/Core/Backoff/FIRAppCheckBackoffWrapper.h"
  24. #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.h"
  25. @interface FIRAppCheckBackoffWrapperTests : XCTestCase
  26. @property(nonatomic, nullable) FIRAppCheckBackoffWrapper *backoffWrapper;
  27. @property(nonatomic) NSDate *currentDate;
  28. /// `NSObject` subclass for resolve the `self.operation` with in the case of success or `NSError`
  29. /// for a failure.
  30. @property(nonatomic) id operationResult;
  31. /// Operation to apply backoff to. It configure with the helper methods during tests.
  32. @property(nonatomic) FIRAppCheckBackoffOperationProvider operationProvider;
  33. /// Expectation to fulfill when operation is completed. It is configured with the `self.operation`
  34. /// in setup helpers.
  35. @property(nonatomic) XCTestExpectation *operationFinishExpectation;
  36. /// Test error handler that returns `self.errorHandlerResult` and fulfills
  37. /// `self.errorHandlerExpectation`.
  38. @property(nonatomic, copy) FIRAppCheckBackoffErrorHandler errorHandler;
  39. /// Expectation to fulfill when error handlers is executed.
  40. @property(nonatomic) XCTestExpectation *errorHandlerExpectation;
  41. @end
  42. @implementation FIRAppCheckBackoffWrapperTests
  43. - (void)setUp {
  44. [super setUp];
  45. __auto_type __weak weakSelf = self;
  46. self.backoffWrapper = [[FIRAppCheckBackoffWrapper alloc] initWithDateProvider:^NSDate *_Nonnull {
  47. return weakSelf.currentDate ?: [NSDate date];
  48. }];
  49. }
  50. - (void)tearDown {
  51. self.backoffWrapper = nil;
  52. self.operationProvider = nil;
  53. [super tearDown];
  54. }
  55. - (void)testBackoffFirstOperationAlwaysExecuted {
  56. // 1. Set up operation success.
  57. [self setUpOperationSuccess];
  58. [self setUpErrorHandlerWithBackoffType:FIRAppCheckBackoffTypeNone];
  59. self.errorHandlerExpectation.inverted = YES;
  60. // 2. Compose operation with backoff.
  61. __auto_type operationWithBackoff =
  62. [self.backoffWrapper applyBackoffToOperation:self.operationProvider
  63. errorHandler:self.errorHandler];
  64. // 3. Wait for operation to complete and check.
  65. [self waitForExpectationsWithTimeout:0.5 handler:NULL];
  66. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  67. XCTAssertEqualObjects(operationWithBackoff.value, self.operationResult);
  68. }
  69. - (void)testBackoff1DayBackoffAfterFailure {
  70. // 0. Set current date.
  71. self.currentDate = [NSDate date];
  72. // 1. Check initial failure.
  73. // 1.1. Set up operation failure.
  74. [self setUpOperationError];
  75. [self setUpErrorHandlerWithBackoffType:FIRAppCheckBackoffType1Day];
  76. // 1.2. Compose operation with backoff.
  77. __auto_type operationWithBackoff =
  78. [self.backoffWrapper applyBackoffToOperation:self.operationProvider
  79. errorHandler:self.errorHandler];
  80. // 1.3. Wait for operation to complete.
  81. [self waitForExpectationsWithTimeout:0.5 handler:NULL];
  82. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  83. // 1.4. Expect the promise to be rejected with the operation error.
  84. XCTAssertEqualObjects(operationWithBackoff.error, self.operationResult);
  85. // 2. Check backoff in 12 hours.
  86. // 2.1. Set up another operation.
  87. [self setUpOperationError];
  88. [self setUpErrorHandlerWithBackoffType:FIRAppCheckBackoffType1Day];
  89. // Don't expect operation to be called.
  90. self.operationFinishExpectation.inverted = YES;
  91. // Don't expect error handler to be called.
  92. self.errorHandlerExpectation.inverted = YES;
  93. // 2.2. Move current date.
  94. self.currentDate = [self.currentDate dateByAddingTimeInterval:12 * 60 * 60];
  95. // 2.3. Compose operation with backoff.
  96. operationWithBackoff = [self.backoffWrapper applyBackoffToOperation:self.operationProvider
  97. errorHandler:self.errorHandler];
  98. // 2.4. Wait for operation to complete.
  99. [self waitForExpectationsWithTimeout:0.5 handler:NULL];
  100. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  101. // 2.5. Expect the promise to be rejected with a backoff error.
  102. XCTAssertTrue(operationWithBackoff.isRejected);
  103. XCTAssertTrue([self isBackoffError:operationWithBackoff.error]);
  104. // 3. Check backoff one minute before allowing retry.
  105. // 3.1. Set up another operation.
  106. [self setUpOperationError];
  107. [self setUpErrorHandlerWithBackoffType:FIRAppCheckBackoffType1Day];
  108. // Don't expect operation to be called.
  109. self.operationFinishExpectation.inverted = YES;
  110. // Don't expect error handler to be called.
  111. self.errorHandlerExpectation.inverted = YES;
  112. // 3.2. Move current date.
  113. self.currentDate = [self.currentDate dateByAddingTimeInterval:11 * 60 * 60 + 59 * 60];
  114. // 3.3. Compose operation with backoff.
  115. operationWithBackoff = [self.backoffWrapper applyBackoffToOperation:self.operationProvider
  116. errorHandler:self.errorHandler];
  117. // 3.4. Wait for operation to complete.
  118. [self waitForExpectationsWithTimeout:0.5 handler:NULL];
  119. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  120. // 3.5. Expect the promise to be rejected with a backoff error.
  121. XCTAssertTrue(operationWithBackoff.isRejected);
  122. XCTAssertTrue([self isBackoffError:operationWithBackoff.error]);
  123. // 4. Check backoff one minute after allowing retry.
  124. // 4.1. Set up another operation.
  125. [self setUpOperationError];
  126. [self setUpErrorHandlerWithBackoffType:FIRAppCheckBackoffType1Day];
  127. // 4.2. Move current date.
  128. self.currentDate = [self.currentDate dateByAddingTimeInterval:12 * 60 * 60 + 1 * 60];
  129. // 4.3. Compose operation with backoff.
  130. operationWithBackoff = [self.backoffWrapper applyBackoffToOperation:self.operationProvider
  131. errorHandler:self.errorHandler];
  132. // 4.4. Wait for operation to complete and check failure.
  133. [self waitForExpectationsWithTimeout:0.5 handler:NULL];
  134. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  135. // 4.5. Expect the promise to be rejected with the operation error.
  136. XCTAssertEqualObjects(operationWithBackoff.error, self.operationResult);
  137. }
  138. #pragma mark - Exponential backoff
  139. - (void)testExponentialBackoff {
  140. // 0. Set current date.
  141. self.currentDate = [NSDate date];
  142. // 1. Check initial failure.
  143. // 1.1. Set up operation failure.
  144. [self setUpOperationError];
  145. [self setUpErrorHandlerWithBackoffType:FIRAppCheckBackoffTypeExponential];
  146. // 1.2. Compose operation with backoff.
  147. __auto_type operationWithBackoff =
  148. [self.backoffWrapper applyBackoffToOperation:self.operationProvider
  149. errorHandler:self.errorHandler];
  150. // 1.4. Wait for operation to complete.
  151. [self waitForExpectationsWithTimeout:0.5 handler:NULL];
  152. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  153. // 1.5. Expect the promise to be rejected with the operation error.
  154. XCTAssertEqualObjects(operationWithBackoff.error, self.operationResult);
  155. // 2. Check exponential backoff.
  156. NSUInteger numberOfAttempts = 20;
  157. NSTimeInterval maximumBackoff = 4 * 60 * 60; // 4 hours.
  158. // The maximum of original backoff interval that can be added.
  159. double maxJitterPortion = 0.5; // Backoff is up to 50% longer.
  160. for (NSUInteger attempt = 0; attempt < numberOfAttempts; attempt++) {
  161. NSTimeInterval expectedMinBackoff = MIN(pow(2, attempt), maximumBackoff);
  162. NSTimeInterval expectedMaxBackoff =
  163. MIN(expectedMinBackoff * (1 + maxJitterPortion), maximumBackoff);
  164. [self assertBackoffIntervalIsAtLeast:expectedMinBackoff andAtMost:expectedMaxBackoff];
  165. }
  166. // 3. Test recovery after success.
  167. // 3.1. Set time after max backoff.
  168. self.currentDate = [self.currentDate dateByAddingTimeInterval:maximumBackoff];
  169. // 3.2. Set up operation success.
  170. [self setUpOperationSuccess];
  171. [self setUpErrorHandlerWithBackoffType:FIRAppCheckBackoffTypeNone];
  172. self.errorHandlerExpectation.inverted = YES;
  173. // 3.3. Compose operation with backoff.
  174. operationWithBackoff = [self.backoffWrapper applyBackoffToOperation:self.operationProvider
  175. errorHandler:self.errorHandler];
  176. // 3.4. Wait for operation to complete.
  177. [self waitForExpectationsWithTimeout:0.5 handler:NULL];
  178. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  179. // 3.5. Expect the promise to be rejected with the operation error.
  180. XCTAssertEqualObjects(operationWithBackoff.value, self.operationResult);
  181. // 3.6. Set up operation failure.
  182. // We expect an operation to be executed with no backoff after a success.
  183. [self setUpOperationError];
  184. [self setUpErrorHandlerWithBackoffType:FIRAppCheckBackoffTypeExponential];
  185. // 3.7. Compose operation with backoff.
  186. operationWithBackoff = [self.backoffWrapper applyBackoffToOperation:self.operationProvider
  187. errorHandler:self.errorHandler];
  188. // 3.8. Wait for operation to complete.
  189. [self waitForExpectationsWithTimeout:0.5 handler:NULL];
  190. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  191. // 3.9. Expect the promise to be rejected with the operation error.
  192. XCTAssertEqualObjects(operationWithBackoff.error, self.operationResult);
  193. }
  194. #pragma mark - Error handling
  195. - (void)testDefaultAppCheckProviderErrorHandler {
  196. __auto_type errorHandler = [self.backoffWrapper defaultAppCheckProviderErrorHandler];
  197. NSError *nonHTTPError = [NSError errorWithDomain:self.name code:1 userInfo:nil];
  198. XCTAssertEqual(errorHandler(nonHTTPError), FIRAppCheckBackoffTypeNone);
  199. FIRAppCheckHTTPError *HTTP400Error = [self httpErrorWithStatusCode:400];
  200. XCTAssertEqual(errorHandler(HTTP400Error), FIRAppCheckBackoffType1Day);
  201. FIRAppCheckHTTPError *HTTP403Error = [self httpErrorWithStatusCode:403];
  202. XCTAssertEqual(errorHandler(HTTP403Error), FIRAppCheckBackoffTypeExponential);
  203. FIRAppCheckHTTPError *HTTP404Error = [self httpErrorWithStatusCode:404];
  204. XCTAssertEqual(errorHandler(HTTP404Error), FIRAppCheckBackoffType1Day);
  205. FIRAppCheckHTTPError *HTTP429Error = [self httpErrorWithStatusCode:429];
  206. XCTAssertEqual(errorHandler(HTTP429Error), FIRAppCheckBackoffTypeExponential);
  207. FIRAppCheckHTTPError *HTTP503Error = [self httpErrorWithStatusCode:503];
  208. XCTAssertEqual(errorHandler(HTTP503Error), FIRAppCheckBackoffTypeExponential);
  209. // Test all other codes from 400 to 599.
  210. for (NSInteger statusCode = 400; statusCode < 600; statusCode++) {
  211. if (statusCode == 400 || statusCode == 404) {
  212. // Skip status codes with non-exponential backoff.
  213. continue;
  214. }
  215. FIRAppCheckHTTPError *HTTPError = [self httpErrorWithStatusCode:statusCode];
  216. XCTAssertEqual(errorHandler(HTTPError), FIRAppCheckBackoffTypeExponential);
  217. }
  218. }
  219. #pragma mark - Helpers
  220. - (void)setUpErrorHandlerWithBackoffType:(FIRAppCheckBackoffType)backoffType {
  221. __auto_type __weak weakSelf = self;
  222. self.errorHandlerExpectation = [self expectationWithDescription:@"Error handler"];
  223. self.errorHandler = ^FIRAppCheckBackoffType(NSError *_Nonnull error) {
  224. [weakSelf.errorHandlerExpectation fulfill];
  225. return backoffType;
  226. };
  227. }
  228. - (void)setUpOperationSuccess {
  229. self.operationFinishExpectation = [self expectationWithDescription:@"Operation performed"];
  230. self.operationResult = [[NSObject alloc] init];
  231. __auto_type __weak weakSelf = self;
  232. self.operationProvider = ^FBLPromise *() {
  233. return [FBLPromise do:^id(void) {
  234. [weakSelf.operationFinishExpectation fulfill];
  235. return weakSelf.operationResult;
  236. }];
  237. };
  238. }
  239. - (void)setUpOperationError {
  240. self.operationFinishExpectation = [self expectationWithDescription:@"Operation performed"];
  241. self.operationResult = [NSError errorWithDomain:self.name code:-1 userInfo:nil];
  242. __auto_type __weak weakSelf = self;
  243. self.operationProvider = ^FBLPromise *() {
  244. return [FBLPromise do:^id(void) {
  245. [weakSelf.operationFinishExpectation fulfill];
  246. return weakSelf.operationResult;
  247. }];
  248. };
  249. }
  250. - (BOOL)isBackoffError:(NSError *)error {
  251. return [error.localizedDescription containsString:@"Too many attempts. Underlying error:"];
  252. }
  253. - (FIRAppCheckHTTPError *)httpErrorWithStatusCode:(NSInteger)statusCode {
  254. NSHTTPURLResponse *httpResponse =
  255. [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"https://localhost"]
  256. statusCode:statusCode
  257. HTTPVersion:nil
  258. headerFields:nil];
  259. FIRAppCheckHTTPError *error = [[FIRAppCheckHTTPError alloc] initWithHTTPResponse:httpResponse
  260. data:nil];
  261. return error;
  262. }
  263. // Asserts that the backoff interval is within the provided range.
  264. // Assumes that `self.currentDate` contains the last failure date.
  265. // Sets `self.currentDate` to the date when the most recent retry happened.
  266. - (void)assertBackoffIntervalIsAtLeast:(NSTimeInterval)minBackoff
  267. andAtMost:(NSTimeInterval)maxBackoff {
  268. NSDate *lastFailureDate = self.currentDate;
  269. // 1. Test backoff before min interval.
  270. // 1.1 Move the date 0.5 sec before the minimum backoff date.
  271. self.currentDate = [lastFailureDate dateByAddingTimeInterval:minBackoff - 0.5];
  272. // 1.2 Set up operation failure.
  273. [self setUpOperationError];
  274. [self setUpErrorHandlerWithBackoffType:FIRAppCheckBackoffTypeExponential];
  275. // 1.3 Don't expect operation to be executed.
  276. self.operationFinishExpectation.inverted = YES;
  277. self.errorHandlerExpectation.inverted = YES;
  278. // 1.4 Compose operation with backoff.
  279. __auto_type operationWithBackoff =
  280. [self.backoffWrapper applyBackoffToOperation:self.operationProvider
  281. errorHandler:self.errorHandler];
  282. // 1.5 Wait for operation to complete.
  283. [self waitForExpectationsWithTimeout:0.5 handler:NULL];
  284. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  285. // 1.6 Expect the promise to be rejected with a backoff error.
  286. XCTAssertTrue(operationWithBackoff.isRejected);
  287. XCTAssertTrue([self isBackoffError:operationWithBackoff.error]);
  288. // 2. Test backoff after max interval.
  289. // 2.1 Move the date 0.5 sec before the minimum backoff date.
  290. self.currentDate = [lastFailureDate dateByAddingTimeInterval:maxBackoff + 0.5];
  291. // 2.2. Set up operation failure and expect it to be completed.
  292. [self setUpOperationError];
  293. [self setUpErrorHandlerWithBackoffType:FIRAppCheckBackoffTypeExponential];
  294. // 2.3 Compose operation with backoff.
  295. operationWithBackoff = [self.backoffWrapper applyBackoffToOperation:self.operationProvider
  296. errorHandler:self.errorHandler];
  297. // 2.4 Wait for operation to complete.
  298. [self waitForExpectationsWithTimeout:0.5 handler:NULL];
  299. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  300. // 2.5 Expect the promise to be rejected with a backoff error.
  301. XCTAssertTrue(operationWithBackoff.isRejected);
  302. XCTAssertFalse([self isBackoffError:operationWithBackoff.error]);
  303. }
  304. @end