OfflineRetrierTests.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. #if canImport(Networking)
  2. import Dispatch
  3. import Testing
  4. @testable import Alamofire
  5. @Suite("OfflineRetrierTests")
  6. struct OfflineRetrierTests {
  7. @Test
  8. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, visionOS 1, *)
  9. func requestIsRetriedWhenConnectivityIsRestored() async throws {
  10. // Given
  11. let didStop = Protected(false)
  12. let monitor = PathMonitor { queue, onResult in
  13. queue.async {
  14. onResult(.pathAvailable)
  15. }
  16. } stop: {
  17. didStop.write(true)
  18. }
  19. // When: retrier considers error to be offline error.
  20. let retrier = OfflineRetrier(monitor: monitor, maximumWait: .milliseconds(100)) { _ in true }
  21. // When: request fails due to error (type doesn't matter).
  22. let request = AF.request(.endpoints(.status(404), .get), interceptor: retrier).validate()
  23. let result = await request.serializingData().result
  24. // Then: request is retried successfully.
  25. #expect(result.isSuccess == true)
  26. // Then: two tasks are created.
  27. #expect(request.tasks.count == 2)
  28. // Then: monitor is stopped.
  29. #expect(didStop.value == true)
  30. }
  31. @Test
  32. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, visionOS 1, *)
  33. func requestIsNotRetriedWhenTheErrorIsNotOfflineError() async throws {
  34. // Given
  35. let didStop = Protected(false)
  36. let monitor = PathMonitor { queue, onResult in
  37. queue.async {
  38. onResult(.pathAvailable)
  39. }
  40. } stop: {
  41. didStop.write(true)
  42. }
  43. // When
  44. let retrier = OfflineRetrier(monitor: monitor, maximumWait: .milliseconds(100))
  45. // When: request fails due to validation.
  46. let request = AF.request(.endpoints(.status(404), .get), interceptor: retrier).validate()
  47. let result = await request.serializingData().result
  48. // Then: request fails since validation failures aren't retried.
  49. #expect(result.isSuccess == false)
  50. // Then: only one task is created.
  51. #expect(request.tasks.count == 1)
  52. // Then: stop not called, as retrier isn't immediately deinit'd.
  53. #expect(didStop.value == false)
  54. }
  55. @Test
  56. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, visionOS 1, *)
  57. func requestIsNotRetriedWhenPathTimesOut() async throws {
  58. // Given
  59. let didStop = Protected(false)
  60. let pathAvailable: Protected<DispatchWorkItem?> = .init(nil)
  61. let monitor = PathMonitor { queue, onResult in
  62. let work = DispatchWorkItem { onResult(.pathAvailable) }
  63. pathAvailable.write(work)
  64. // Given: path available after one second.
  65. queue.asyncAfter(deadline: .now() + .seconds(1), execute: work)
  66. } stop: {
  67. pathAvailable.write { $0?.cancel() }
  68. didStop.write(true)
  69. }
  70. // When: retrier times out after one millisecond.
  71. let retrier = OfflineRetrier(monitor: monitor, maximumWait: .milliseconds(1)) { _ in true }
  72. // When: request fails due to validation but would succeed on retry.
  73. let request = AF.request(.endpoints(.status(404), .get), interceptor: retrier).validate()
  74. let result = await request.serializingData().result
  75. // Then: request fails since it's not retried.
  76. #expect(result.isSuccess == false)
  77. // Then: only one task is created.
  78. #expect(request.tasks.count == 1)
  79. // Then: stop is called since timeout resets retrier.
  80. #expect(didStop.value == true)
  81. }
  82. @Test
  83. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, visionOS 1, *)
  84. func sessionWideRetrierCanRetryMultipleRequests() async throws {
  85. // Given
  86. let didStop = Protected(false)
  87. let pathAvailable: Protected<DispatchWorkItem?> = .init(nil)
  88. let monitor = PathMonitor { queue, onResult in
  89. let work = DispatchWorkItem { onResult(.pathAvailable) }
  90. pathAvailable.write(work)
  91. // Given: path available after ten milliseconds.
  92. queue.asyncAfter(deadline: .now() + .milliseconds(10), execute: work)
  93. } stop: {
  94. pathAvailable.write { $0?.cancel() }
  95. didStop.write(true)
  96. }
  97. // When
  98. let retrier = OfflineRetrier(monitor: monitor, maximumWait: .milliseconds(500)) { _ in true }
  99. let session = Session(interceptor: retrier)
  100. // When: multiple requests are started which initially fail due to validation.
  101. async let first = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  102. async let second = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  103. async let third = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  104. async let fourth = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  105. // Then: all requests succeed after retry.
  106. await #expect(first.isSuccess == true)
  107. await #expect(second.isSuccess == true)
  108. await #expect(third.isSuccess == true)
  109. await #expect(fourth.isSuccess == true)
  110. // Then: monitor has stopped due to `Session` deinit.
  111. #expect(didStop.value == true)
  112. }
  113. @Test
  114. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, visionOS 1, *)
  115. func sessionWideRetrierCanRetryMultipleRequestsTwice() async throws {
  116. // Given
  117. let didStop = Protected(false)
  118. let pathAvailable: Protected<DispatchWorkItem?> = .init(nil)
  119. let monitor = PathMonitor { queue, onResult in
  120. let work = DispatchWorkItem { onResult(.pathAvailable) }
  121. pathAvailable.write(work)
  122. // Given: path available after ten milliseconds.
  123. queue.asyncAfter(deadline: .now() + .milliseconds(10), execute: work)
  124. } stop: {
  125. pathAvailable.write { $0?.cancel() }
  126. didStop.write(true)
  127. }
  128. // When
  129. let retrier = OfflineRetrier(monitor: monitor, maximumWait: .milliseconds(500)) { _ in true }
  130. let session = Session(interceptor: retrier)
  131. // When: multiple requests are started which initially fail due to validation.
  132. async let first = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  133. async let second = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  134. async let third = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  135. async let fourth = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  136. // Then: all requests succeed after retry.
  137. await #expect(first.isSuccess == true)
  138. await #expect(second.isSuccess == true)
  139. await #expect(third.isSuccess == true)
  140. await #expect(fourth.isSuccess == true)
  141. // When: another set of requests are started which initially fail due to validation.
  142. async let fifth = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  143. async let sixth = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  144. async let seventh = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  145. async let eighth = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  146. // Then: second set of requests succeed after retry.
  147. await #expect(fifth.isSuccess == true)
  148. await #expect(sixth.isSuccess == true)
  149. await #expect(seventh.isSuccess == true)
  150. await #expect(eighth.isSuccess == true)
  151. // Then: monitor has stopped due to `Session` deinit.
  152. #expect(didStop.value == true)
  153. }
  154. @Test
  155. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, visionOS 1, *)
  156. func sessionWideRetrierCanTimeOutMultipleRequests() async throws {
  157. // Given
  158. let didStop = Protected(false)
  159. let pathAvailable: Protected<DispatchWorkItem?> = .init(nil)
  160. let monitor = PathMonitor { queue, onResult in
  161. let work = DispatchWorkItem { onResult(.pathAvailable) }
  162. pathAvailable.write(work)
  163. // Given: path available after one second.
  164. queue.asyncAfter(deadline: .now() + .seconds(1), execute: work)
  165. } stop: {
  166. pathAvailable.write { $0?.cancel() }
  167. didStop.write(true)
  168. }
  169. // When
  170. let retrier = OfflineRetrier(monitor: monitor, maximumWait: .milliseconds(10)) { _ in true }
  171. let session = Session(interceptor: retrier)
  172. // When: multiple requests are started which initially fail due to validation.
  173. async let first = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  174. async let second = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  175. async let third = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  176. async let fourth = session.request(.endpoints(.status(404), .get)).validate().serializingData().result
  177. // Then: all requests succeed after retry.
  178. await #expect(first.isSuccess == false)
  179. await #expect(second.isSuccess == false)
  180. await #expect(third.isSuccess == false)
  181. await #expect(fourth.isSuccess == false)
  182. // Then: monitor has stopped due to `Session` deinit.
  183. #expect(didStop.value == true)
  184. }
  185. @Test
  186. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, visionOS 1, *)
  187. func offlineRetrierNeverStartsOrStopsWhenImmediatelyDeinited() async throws {
  188. // Given
  189. let didStart = Protected(false)
  190. let didStop = Protected(false)
  191. let monitor = PathMonitor { _, _ in
  192. didStart.write(true)
  193. } stop: {
  194. didStop.write(true)
  195. }
  196. // When: retrier created with no start and long timeout.
  197. let retrier = OfflineRetrier(monitor: monitor, maximumWait: .seconds(100))
  198. // When: retrier is deinit'd.
  199. _ = consume retrier
  200. // Then: didStart is false.
  201. #expect(didStart.value == false)
  202. // Then: didStop is false.
  203. #expect(didStop.value == false)
  204. }
  205. }
  206. #endif