OfflineRetrierTests.swift 9.6 KB

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