HeartbeatControllerTests.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  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. func testFlush_WhenEmpty_ReturnsEmptyPayload() throws {
  18. // Given
  19. let controller = HeartbeatController(storage: HeartbeatStorageFake())
  20. // Then
  21. assertHeartbeatControllerFlushesEmptyPayload(controller)
  22. }
  23. func testLogAndFlush() throws {
  24. // Given
  25. let date = Date(timeIntervalSince1970: 1_635_739_200) // 2021-11-01 @ 00:00:00 (EST)
  26. let controller = HeartbeatController(
  27. storage: HeartbeatStorageFake(),
  28. dateProvider: { date }
  29. )
  30. assertHeartbeatControllerFlushesEmptyPayload(controller)
  31. // When
  32. controller.log("dummy_agent")
  33. let heartbeatPayload = controller.flush()
  34. // Then
  35. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  36. heartbeatPayload.headerValue(),
  37. """
  38. {
  39. "version": 2,
  40. "heartbeats": [
  41. {
  42. "agent": "dummy_agent",
  43. "dates": ["2021-11-01"]
  44. }
  45. ]
  46. }
  47. """
  48. )
  49. assertHeartbeatControllerFlushesEmptyPayload(controller)
  50. }
  51. @MainActor func testLogAndFlushAsync() throws {
  52. // Given
  53. let date = Date(timeIntervalSince1970: 1_635_739_200) // 2021-11-01 @ 00:00:00 (EST)
  54. let controller = HeartbeatController(
  55. storage: HeartbeatStorageFake(),
  56. dateProvider: { date }
  57. )
  58. let expectation = expectation(description: #function)
  59. assertHeartbeatControllerFlushesEmptyPayload(controller)
  60. // When
  61. controller.log("dummy_agent")
  62. controller.flushAsync { heartbeatPayload in
  63. // Then
  64. do {
  65. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  66. heartbeatPayload.headerValue(),
  67. """
  68. {
  69. "version": 2,
  70. "heartbeats": [
  71. {
  72. "agent": "dummy_agent",
  73. "dates": ["2021-11-01"]
  74. }
  75. ]
  76. }
  77. """
  78. )
  79. expectation.fulfill()
  80. } catch {
  81. XCTFail("Unexpected error: \(error)")
  82. }
  83. }
  84. waitForExpectations(timeout: 1.0)
  85. assertHeartbeatControllerFlushesEmptyPayload(controller)
  86. }
  87. func testLogAtEndOfTimePeriodAndAcceptAtStartOfNextOne() throws {
  88. // Given
  89. let date = Date(timeIntervalSince1970: 1_635_739_200) // 2021-11-01 @ 00:00:00 (EST)
  90. let testDate = AdjustableDate(date: date)
  91. let controller = HeartbeatController(
  92. storage: HeartbeatStorageFake(),
  93. dateProvider: { testDate.date }
  94. )
  95. assertHeartbeatControllerFlushesEmptyPayload(controller)
  96. // When
  97. // - Clock time 2021-11-01 @ 00:00:00 (EST)
  98. controller.log("dummy_agent")
  99. // - Advance to 2021-11-01 @ 23:59:59 (EST)
  100. testDate.date.addTimeInterval(60 * 60 * 24 - 1)
  101. controller.log("dummy_agent")
  102. // - Advance to 2021-11-02 @ 00:00:00 (EST)
  103. testDate.date.addTimeInterval(1)
  104. controller.log("dummy_agent")
  105. // Then
  106. let heartbeatPayload = controller.flush()
  107. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  108. heartbeatPayload.headerValue(),
  109. """
  110. {
  111. "version": 2,
  112. "heartbeats": [
  113. {
  114. "agent": "dummy_agent",
  115. "dates": [
  116. "2021-11-01",
  117. "2021-11-02"
  118. ]
  119. }
  120. ]
  121. }
  122. """
  123. )
  124. assertHeartbeatControllerFlushesEmptyPayload(controller)
  125. }
  126. func testDoNotLogMoreThanOnceInACalendarDay() throws {
  127. // Given
  128. let date = Date(timeIntervalSince1970: 1_635_739_200) // 2021-11-01 @ 00:00:00 (EST)
  129. let controller = HeartbeatController(
  130. storage: HeartbeatStorageFake(),
  131. dateProvider: { date }
  132. )
  133. // When
  134. controller.log("dummy_agent")
  135. controller.log("dummy_agent")
  136. // Then
  137. let heartbeatPayload = controller.flush()
  138. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  139. heartbeatPayload.headerValue(),
  140. """
  141. {
  142. "version": 2,
  143. "heartbeats": [
  144. {
  145. "agent": "dummy_agent",
  146. "dates": ["2021-11-01"]
  147. }
  148. ]
  149. }
  150. """
  151. )
  152. }
  153. func testDoNotLogMoreThanOnceInACalendarDay_AfterFlushing() throws {
  154. // Given
  155. let date = Date(timeIntervalSince1970: 1_635_739_200) // 2021-11-01 @ 00:00:00 (EST)
  156. let controller = HeartbeatController(
  157. storage: HeartbeatStorageFake(),
  158. dateProvider: { date }
  159. )
  160. // When
  161. controller.log("dummy_agent")
  162. let heartbeatPayload = controller.flush()
  163. controller.log("dummy_agent")
  164. // Then
  165. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  166. heartbeatPayload.headerValue(),
  167. """
  168. {
  169. "version": 2,
  170. "heartbeats": [
  171. {
  172. "agent": "dummy_agent",
  173. "dates": ["2021-11-01"]
  174. }
  175. ]
  176. }
  177. """
  178. )
  179. // Below assertion asserts that duplicate was not logged.
  180. assertHeartbeatControllerFlushesEmptyPayload(controller)
  181. }
  182. func testHeartbeatDatesAreStandardizedForUTC() throws {
  183. // Given
  184. let newYorkDate = try XCTUnwrap(
  185. DateComponents(
  186. calendar: .current,
  187. timeZone: TimeZone(identifier: "America/New_York"),
  188. year: 2021,
  189. month: 11,
  190. day: 01,
  191. hour: 23
  192. ).date // 2021-11-01 @ 11 PM (EST)
  193. )
  194. let heartbeatController = HeartbeatController(
  195. storage: HeartbeatStorageFake(),
  196. dateProvider: { newYorkDate }
  197. )
  198. // When
  199. heartbeatController.log("dummy_agent")
  200. let payload = heartbeatController.flush()
  201. // Then
  202. // Note below how the date was interpreted as UTC - 2021-11-02.
  203. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  204. payload.headerValue(),
  205. """
  206. {
  207. "version": 2,
  208. "heartbeats": [
  209. {
  210. "agent": "dummy_agent",
  211. "dates": ["2021-11-02"]
  212. }
  213. ]
  214. }
  215. """
  216. )
  217. }
  218. func testDoNotLogMoreThanOnceInACalendarDay_WhenTravelingAcrossTimeZones() throws {
  219. // Given
  220. let newYorkDate = try XCTUnwrap(
  221. DateComponents(
  222. calendar: .current,
  223. timeZone: TimeZone(identifier: "America/New_York"),
  224. year: 2021,
  225. month: 11,
  226. day: 01,
  227. hour: 23
  228. ).date // 2021-11-01 @ 11 PM (New York time zone)
  229. )
  230. let tokyoDate = try XCTUnwrap(
  231. DateComponents(
  232. calendar: .current,
  233. timeZone: TimeZone(identifier: "Asia/Tokyo"),
  234. year: 2021,
  235. month: 11,
  236. day: 02,
  237. hour: 23
  238. ).date // 2021-11-02 @ 11 PM (Tokyo time zone)
  239. )
  240. let testDate = AdjustableDate(date: newYorkDate)
  241. let heartbeatController = HeartbeatController(
  242. storage: HeartbeatStorageFake(),
  243. dateProvider: { testDate.date }
  244. )
  245. // When
  246. heartbeatController.log("dummy_agent")
  247. // Device travels from NYC to Tokyo.
  248. testDate.date = tokyoDate
  249. heartbeatController.log("dummy_agent")
  250. // Then
  251. let payload = heartbeatController.flush()
  252. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  253. payload.headerValue(),
  254. """
  255. {
  256. "version" : 2,
  257. "heartbeats" : [
  258. {
  259. "agent" : "dummy_agent",
  260. "dates" : [
  261. "2021-11-02"
  262. ]
  263. }
  264. ]
  265. }
  266. """
  267. )
  268. }
  269. func testLoggingDependsOnDateNotUserAgent() throws {
  270. // Given
  271. let date = Date(timeIntervalSince1970: 1_635_739_200) // 2021-11-01 @ 00:00:00 (EST)
  272. let testDate = AdjustableDate(date: date)
  273. let heartbeatController = HeartbeatController(
  274. storage: HeartbeatStorageFake(),
  275. dateProvider: { testDate.date }
  276. )
  277. // When
  278. // - Day 1
  279. heartbeatController.log("dummy_agent")
  280. // - Day 2
  281. testDate.date.addTimeInterval(60 * 60 * 24)
  282. heartbeatController.log("some_other_agent")
  283. // - Day 3
  284. testDate.date.addTimeInterval(60 * 60 * 24)
  285. heartbeatController.log("dummy_agent")
  286. // Then
  287. let payload = heartbeatController.flush()
  288. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  289. payload.headerValue(),
  290. """
  291. {
  292. "version": 2,
  293. "heartbeats": [
  294. {
  295. "agent": "dummy_agent",
  296. "dates": [
  297. "2021-11-01",
  298. "2021-11-03"
  299. ]
  300. },
  301. {
  302. "agent": "some_other_agent",
  303. "dates": [
  304. "2021-11-02"
  305. ]
  306. }
  307. ]
  308. }
  309. """
  310. )
  311. }
  312. func testFlushHeartbeatFromToday_WhenTodayHasAHeartbeat_ReturnsPayloadWithOnlyTodaysHeartbeat() throws {
  313. // Given
  314. let date = Date(timeIntervalSince1970: 1_635_739_200) // 2021-11-01 @ 00:00:00 (EST)
  315. let yesterdaysDate = date.addingTimeInterval(-1 * 60 * 60 * 24)
  316. let todaysDate = date
  317. let tomorrowsDate = date.addingTimeInterval(60 * 60 * 24)
  318. let testDate = AdjustableDate(date: yesterdaysDate)
  319. let heartbeatController = HeartbeatController(
  320. storage: HeartbeatStorageFake(),
  321. dateProvider: { testDate.date }
  322. )
  323. // When
  324. heartbeatController.log("yesterdays_dummy_agent")
  325. testDate.date = todaysDate
  326. heartbeatController.log("todays_dummy_agent")
  327. testDate.date = tomorrowsDate
  328. heartbeatController.log("tomorrows_dummy_agent")
  329. testDate.date = todaysDate
  330. // Then
  331. let payload = heartbeatController.flushHeartbeatFromToday()
  332. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  333. payload.headerValue(),
  334. """
  335. {
  336. "version": 2,
  337. "heartbeats": [
  338. {
  339. "agent": "todays_dummy_agent",
  340. "dates": ["2021-11-01"]
  341. }
  342. ]
  343. }
  344. """
  345. )
  346. let remainingPayload = heartbeatController.flush()
  347. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  348. remainingPayload.headerValue(),
  349. """
  350. {
  351. "version": 2,
  352. "heartbeats": [
  353. {
  354. "agent": "tomorrows_dummy_agent",
  355. "dates": ["2021-11-02"]
  356. },
  357. {
  358. "agent": "yesterdays_dummy_agent",
  359. "dates": ["2021-10-31"]
  360. }
  361. ]
  362. }
  363. """
  364. )
  365. assertHeartbeatControllerFlushesEmptyPayload(heartbeatController)
  366. }
  367. func testFlushHeartbeatFromToday_WhenTodayDoesNotHaveAHeartbeat_ReturnsEmptyPayload() throws {
  368. // Given
  369. let date = Date(timeIntervalSince1970: 1_635_739_200) // 2021-11-01 @ 00:00:00 (EST)
  370. let heartbeatController = HeartbeatController(id: #function, dateProvider: { date })
  371. // When
  372. heartbeatController.flushHeartbeatFromToday()
  373. // Then
  374. assertHeartbeatControllerFlushesEmptyPayload(heartbeatController)
  375. }
  376. }
  377. // MARK: - Fakes
  378. private final class HeartbeatStorageFake: HeartbeatStorageProtocol, @unchecked Sendable {
  379. // The unchecked Sendable conformance is used to prevent warnings for the below var, which
  380. // violates the class's Sendable conformance. Ignoring this violation should be okay for
  381. // testing purposes.
  382. private var heartbeatsBundle: HeartbeatsBundle?
  383. func readAndWriteSync(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?) {
  384. heartbeatsBundle = transform(heartbeatsBundle)
  385. }
  386. func readAndWriteAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?) {
  387. heartbeatsBundle = transform(heartbeatsBundle)
  388. }
  389. func getAndSet(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?) throws
  390. -> HeartbeatsBundle? {
  391. let oldHeartbeatsBundle = heartbeatsBundle
  392. heartbeatsBundle = transform(heartbeatsBundle)
  393. return oldHeartbeatsBundle
  394. }
  395. func getAndSetAsync(using transform: @escaping (FirebaseCoreInternal.HeartbeatsBundle?)
  396. -> FirebaseCoreInternal.HeartbeatsBundle?,
  397. completion: @escaping (Result<
  398. FirebaseCoreInternal.HeartbeatsBundle?,
  399. any Error
  400. >) -> Void) {
  401. let oldHeartbeatsBundle = heartbeatsBundle
  402. heartbeatsBundle = transform(heartbeatsBundle)
  403. completion(.success(oldHeartbeatsBundle))
  404. }
  405. }