HeartbeatControllerTests.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. // Copyright 2021 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. @testable import FirebaseCoreInternal
  15. import XCTest
  16. class HeartbeatControllerTests: XCTestCase {
  17. // 2021-11-01 @ 00:00:00 (EST)
  18. let date = Date(timeIntervalSince1970: 1_635_739_200)
  19. func testFlush_WhenEmpty_ReturnsEmptyPayload() throws {
  20. // Given
  21. let controller = HeartbeatController(storage: HeartbeatStorageFake())
  22. // Then
  23. assertHeartbeatControllerFlushesEmptyPayload(controller)
  24. }
  25. func testLogAndFlush() throws {
  26. // Given
  27. let controller = HeartbeatController(
  28. storage: HeartbeatStorageFake(),
  29. dateProvider: { self.date }
  30. )
  31. assertHeartbeatControllerFlushesEmptyPayload(controller)
  32. // When
  33. controller.log("dummy_agent")
  34. let heartbeatPayload = controller.flush()
  35. // Then
  36. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  37. heartbeatPayload.headerValue(),
  38. """
  39. {
  40. "version": 2,
  41. "heartbeats": [
  42. {
  43. "agent": "dummy_agent",
  44. "dates": ["2021-11-01"]
  45. }
  46. ]
  47. }
  48. """
  49. )
  50. assertHeartbeatControllerFlushesEmptyPayload(controller)
  51. }
  52. @available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, *)
  53. func testLogAndFlushAsync() async throws {
  54. // Given
  55. let controller = HeartbeatController(
  56. storage: HeartbeatStorageFake(),
  57. dateProvider: { self.date }
  58. )
  59. assertHeartbeatControllerFlushesEmptyPayload(controller)
  60. // When
  61. controller.log("dummy_agent")
  62. let heartbeatPayload = await controller.flushAsync()
  63. // Then
  64. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  65. heartbeatPayload.headerValue(),
  66. """
  67. {
  68. "version": 2,
  69. "heartbeats": [
  70. {
  71. "agent": "dummy_agent",
  72. "dates": ["2021-11-01"]
  73. }
  74. ]
  75. }
  76. """
  77. )
  78. assertHeartbeatControllerFlushesEmptyPayload(controller)
  79. }
  80. func testLogAtEndOfTimePeriodAndAcceptAtStartOfNextOne() throws {
  81. // Given
  82. var testDate = date
  83. let controller = HeartbeatController(
  84. storage: HeartbeatStorageFake(),
  85. dateProvider: { testDate }
  86. )
  87. assertHeartbeatControllerFlushesEmptyPayload(controller)
  88. // When
  89. // - Clock time 2021-11-01 @ 00:00:00 (EST)
  90. controller.log("dummy_agent")
  91. // - Advance to 2021-11-01 @ 23:59:59 (EST)
  92. testDate.addTimeInterval(60 * 60 * 24 - 1)
  93. controller.log("dummy_agent")
  94. // - Advance to 2021-11-02 @ 00:00:00 (EST)
  95. testDate.addTimeInterval(1)
  96. controller.log("dummy_agent")
  97. // Then
  98. let heartbeatPayload = controller.flush()
  99. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  100. heartbeatPayload.headerValue(),
  101. """
  102. {
  103. "version": 2,
  104. "heartbeats": [
  105. {
  106. "agent": "dummy_agent",
  107. "dates": [
  108. "2021-11-01",
  109. "2021-11-02"
  110. ]
  111. }
  112. ]
  113. }
  114. """
  115. )
  116. assertHeartbeatControllerFlushesEmptyPayload(controller)
  117. }
  118. func testDoNotLogMoreThanOnceInACalendarDay() throws {
  119. // Given
  120. let controller = HeartbeatController(
  121. storage: HeartbeatStorageFake(),
  122. dateProvider: { self.date }
  123. )
  124. // When
  125. controller.log("dummy_agent")
  126. controller.log("dummy_agent")
  127. // Then
  128. let heartbeatPayload = controller.flush()
  129. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  130. heartbeatPayload.headerValue(),
  131. """
  132. {
  133. "version": 2,
  134. "heartbeats": [
  135. {
  136. "agent": "dummy_agent",
  137. "dates": ["2021-11-01"]
  138. }
  139. ]
  140. }
  141. """
  142. )
  143. }
  144. func testDoNotLogMoreThanOnceInACalendarDay_AfterFlushing() throws {
  145. // Given
  146. let controller = HeartbeatController(
  147. storage: HeartbeatStorageFake(),
  148. dateProvider: { self.date }
  149. )
  150. // When
  151. controller.log("dummy_agent")
  152. let heartbeatPayload = controller.flush()
  153. controller.log("dummy_agent")
  154. // Then
  155. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  156. heartbeatPayload.headerValue(),
  157. """
  158. {
  159. "version": 2,
  160. "heartbeats": [
  161. {
  162. "agent": "dummy_agent",
  163. "dates": ["2021-11-01"]
  164. }
  165. ]
  166. }
  167. """
  168. )
  169. // Below assertion asserts that duplicate was not logged.
  170. assertHeartbeatControllerFlushesEmptyPayload(controller)
  171. }
  172. func testHeartbeatDatesAreStandardizedForUTC() throws {
  173. // Given
  174. let newYorkDate = try XCTUnwrap(
  175. DateComponents(
  176. calendar: .current,
  177. timeZone: TimeZone(identifier: "America/New_York"),
  178. year: 2021,
  179. month: 11,
  180. day: 01,
  181. hour: 23
  182. ).date // 2021-11-01 @ 11 PM (EST)
  183. )
  184. let heartbeatController = HeartbeatController(
  185. storage: HeartbeatStorageFake(),
  186. dateProvider: { newYorkDate }
  187. )
  188. // When
  189. heartbeatController.log("dummy_agent")
  190. let payload = heartbeatController.flush()
  191. // Then
  192. // Note below how the date was interpreted as UTC - 2021-11-02.
  193. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  194. payload.headerValue(),
  195. """
  196. {
  197. "version": 2,
  198. "heartbeats": [
  199. {
  200. "agent": "dummy_agent",
  201. "dates": ["2021-11-02"]
  202. }
  203. ]
  204. }
  205. """
  206. )
  207. }
  208. func testDoNotLogMoreThanOnceInACalendarDay_WhenTravelingAcrossTimeZones() throws {
  209. // Given
  210. let newYorkDate = try XCTUnwrap(
  211. DateComponents(
  212. calendar: .current,
  213. timeZone: TimeZone(identifier: "America/New_York"),
  214. year: 2021,
  215. month: 11,
  216. day: 01,
  217. hour: 23
  218. ).date // 2021-11-01 @ 11 PM (New York time zone)
  219. )
  220. let tokyoDate = try XCTUnwrap(
  221. DateComponents(
  222. calendar: .current,
  223. timeZone: TimeZone(identifier: "Asia/Tokyo"),
  224. year: 2021,
  225. month: 11,
  226. day: 02,
  227. hour: 23
  228. ).date // 2021-11-02 @ 11 PM (Tokyo time zone)
  229. )
  230. var testDate = newYorkDate
  231. let heartbeatController = HeartbeatController(
  232. storage: HeartbeatStorageFake(),
  233. dateProvider: { testDate }
  234. )
  235. // When
  236. heartbeatController.log("dummy_agent")
  237. // Device travels from NYC to Tokyo.
  238. testDate = tokyoDate
  239. heartbeatController.log("dummy_agent")
  240. // Then
  241. let payload = heartbeatController.flush()
  242. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  243. payload.headerValue(),
  244. """
  245. {
  246. "version" : 2,
  247. "heartbeats" : [
  248. {
  249. "agent" : "dummy_agent",
  250. "dates" : [
  251. "2021-11-02"
  252. ]
  253. }
  254. ]
  255. }
  256. """
  257. )
  258. }
  259. func testLoggingDependsOnDateNotUserAgent() throws {
  260. // Given
  261. var testDate = date
  262. let heartbeatController = HeartbeatController(
  263. storage: HeartbeatStorageFake(),
  264. dateProvider: { testDate }
  265. )
  266. // When
  267. // - Day 1
  268. heartbeatController.log("dummy_agent")
  269. // - Day 2
  270. testDate.addTimeInterval(60 * 60 * 24)
  271. heartbeatController.log("some_other_agent")
  272. // - Day 3
  273. testDate.addTimeInterval(60 * 60 * 24)
  274. heartbeatController.log("dummy_agent")
  275. // Then
  276. let payload = heartbeatController.flush()
  277. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  278. payload.headerValue(),
  279. """
  280. {
  281. "version": 2,
  282. "heartbeats": [
  283. {
  284. "agent": "dummy_agent",
  285. "dates": [
  286. "2021-11-01",
  287. "2021-11-03"
  288. ]
  289. },
  290. {
  291. "agent": "some_other_agent",
  292. "dates": [
  293. "2021-11-02"
  294. ]
  295. }
  296. ]
  297. }
  298. """
  299. )
  300. }
  301. func testFlushHeartbeatFromToday_WhenTodayHasAHeartbeat_ReturnsPayloadWithOnlyTodaysHeartbeat() throws {
  302. // Given
  303. let yesterdaysDate = date.addingTimeInterval(-1 * 60 * 60 * 24)
  304. let todaysDate = date
  305. let tomorrowsDate = date.addingTimeInterval(60 * 60 * 24)
  306. var testDate = yesterdaysDate
  307. let heartbeatController = HeartbeatController(
  308. storage: HeartbeatStorageFake(),
  309. dateProvider: { testDate }
  310. )
  311. // When
  312. heartbeatController.log("yesterdays_dummy_agent")
  313. testDate = todaysDate
  314. heartbeatController.log("todays_dummy_agent")
  315. testDate = tomorrowsDate
  316. heartbeatController.log("tomorrows_dummy_agent")
  317. testDate = todaysDate
  318. // Then
  319. let payload = heartbeatController.flushHeartbeatFromToday()
  320. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  321. payload.headerValue(),
  322. """
  323. {
  324. "version": 2,
  325. "heartbeats": [
  326. {
  327. "agent": "todays_dummy_agent",
  328. "dates": ["2021-11-01"]
  329. }
  330. ]
  331. }
  332. """
  333. )
  334. let remainingPayload = heartbeatController.flush()
  335. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  336. remainingPayload.headerValue(),
  337. """
  338. {
  339. "version": 2,
  340. "heartbeats": [
  341. {
  342. "agent": "tomorrows_dummy_agent",
  343. "dates": ["2021-11-02"]
  344. },
  345. {
  346. "agent": "yesterdays_dummy_agent",
  347. "dates": ["2021-10-31"]
  348. }
  349. ]
  350. }
  351. """
  352. )
  353. assertHeartbeatControllerFlushesEmptyPayload(heartbeatController)
  354. }
  355. func testFlushHeartbeatFromToday_WhenTodayDoesNotHaveAHeartbeat_ReturnsEmptyPayload() throws {
  356. // Given
  357. let heartbeatController = HeartbeatController(id: #function, dateProvider: { self.date })
  358. // When
  359. heartbeatController.flushHeartbeatFromToday()
  360. // Then
  361. assertHeartbeatControllerFlushesEmptyPayload(heartbeatController)
  362. }
  363. }
  364. // MARK: - Fakes
  365. private class HeartbeatStorageFake: HeartbeatStorageProtocol {
  366. private var heartbeatsBundle: HeartbeatsBundle?
  367. func readAndWriteSync(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?) {
  368. heartbeatsBundle = transform(heartbeatsBundle)
  369. }
  370. func readAndWriteAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?) {
  371. heartbeatsBundle = transform(heartbeatsBundle)
  372. }
  373. func getAndSet(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?) throws
  374. -> HeartbeatsBundle? {
  375. let oldHeartbeatsBundle = heartbeatsBundle
  376. heartbeatsBundle = transform(heartbeatsBundle)
  377. return oldHeartbeatsBundle
  378. }
  379. func getAndSetAsync(using transform: @escaping (FirebaseCoreInternal.HeartbeatsBundle?)
  380. -> FirebaseCoreInternal.HeartbeatsBundle?,
  381. completion: @escaping (Result<
  382. FirebaseCoreInternal.HeartbeatsBundle?,
  383. any Error
  384. >) -> Void) {
  385. let oldHeartbeatsBundle = heartbeatsBundle
  386. heartbeatsBundle = transform(heartbeatsBundle)
  387. completion(.success(oldHeartbeatsBundle))
  388. }
  389. }