// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import XCTest @testable import FirebaseCoreInternal import HeartbeatLoggingTestUtils class HeartbeatControllerTests: XCTestCase { // 2021-11-01 @ 00:00:00 (EST) let date = Date(timeIntervalSince1970: 1_635_739_200) func testFlush_WhenEmpty_ReturnsEmptyPayload() throws { // Given let controller = HeartbeatController(storage: HeartbeatStorageFake()) // Then assertHeartbeatControllerFlushesEmptyPayload(controller) } func testLogAndFlush() throws { // Given let controller = HeartbeatController( storage: HeartbeatStorageFake(), dateProvider: { self.date } ) assertHeartbeatControllerFlushesEmptyPayload(controller) // When controller.log("dummy_agent") let heartbeatPayload = controller.flush() // Then try HeartbeatLoggingTestUtils.assertEqualPayloadStrings( heartbeatPayload.headerValue(), """ { "version": 2, "heartbeats": [ { "agent": "dummy_agent", "dates": ["2021-11-01"] } ] } """ ) assertHeartbeatControllerFlushesEmptyPayload(controller) } func testLogAtEndOfTimePeriodAndAcceptAtStartOfNextOne() throws { // Given var testDate = date let controller = HeartbeatController( storage: HeartbeatStorageFake(), dateProvider: { testDate } ) assertHeartbeatControllerFlushesEmptyPayload(controller) // When // - Clock time 2021-11-01 @ 00:00:00 (EST) controller.log("dummy_agent") // - Advance to 2021-11-01 @ 23:59:59 (EST) testDate.addTimeInterval(60 * 60 * 24 - 1) controller.log("dummy_agent") // - Advance to 2021-11-02 @ 00:00:00 (EST) testDate.addTimeInterval(1) controller.log("dummy_agent") // Then let heartbeatPayload = controller.flush() try HeartbeatLoggingTestUtils.assertEqualPayloadStrings( heartbeatPayload.headerValue(), """ { "version": 2, "heartbeats": [ { "agent": "dummy_agent", "dates": [ "2021-11-01", "2021-11-02" ] } ] } """ ) assertHeartbeatControllerFlushesEmptyPayload(controller) } func testDoNotLogMoreThanOnceInACalendarDay() throws { // Given let controller = HeartbeatController( storage: HeartbeatStorageFake(), dateProvider: { self.date } ) // When controller.log("dummy_agent") controller.log("dummy_agent") // Then let heartbeatPayload = controller.flush() try HeartbeatLoggingTestUtils.assertEqualPayloadStrings( heartbeatPayload.headerValue(), """ { "version": 2, "heartbeats": [ { "agent": "dummy_agent", "dates": ["2021-11-01"] } ] } """ ) } func testDoNotLogMoreThanOnceInACalendarDay_AfterFlushing() throws { // Given let controller = HeartbeatController( storage: HeartbeatStorageFake(), dateProvider: { self.date } ) // When controller.log("dummy_agent") let heartbeatPayload = controller.flush() controller.log("dummy_agent") // Then try HeartbeatLoggingTestUtils.assertEqualPayloadStrings( heartbeatPayload.headerValue(), """ { "version": 2, "heartbeats": [ { "agent": "dummy_agent", "dates": ["2021-11-01"] } ] } """ ) // Below assertion asserts that duplicate was not logged. assertHeartbeatControllerFlushesEmptyPayload(controller) } func testHeartbeatDatesAreStandardizedForUTC() throws { // Given let newYorkDate = try XCTUnwrap( DateComponents( calendar: .current, timeZone: TimeZone(identifier: "America/New_York"), year: 2021, month: 11, day: 01, hour: 23 ).date // 2021-11-01 @ 11 PM (EST) ) let heartbeatController = HeartbeatController( storage: HeartbeatStorageFake(), dateProvider: { newYorkDate } ) // When heartbeatController.log("dummy_agent") let payload = heartbeatController.flush() // Then // Note below how the date was intepreted as UTC - 2021-11-02. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings( payload.headerValue(), """ { "version": 2, "heartbeats": [ { "agent": "dummy_agent", "dates": ["2021-11-02"] } ] } """ ) } func testDoNotLogMoreThanOnceInACalendarDay_WhenTravelingAcrossTimeZones() throws { // Given let newYorkDate = try XCTUnwrap( DateComponents( calendar: .current, timeZone: TimeZone(identifier: "America/New_York"), year: 2021, month: 11, day: 01, hour: 23 ).date // 2021-11-01 @ 11 PM (New York time zone) ) let tokyoDate = try XCTUnwrap( DateComponents( calendar: .current, timeZone: TimeZone(identifier: "Asia/Tokyo"), year: 2021, month: 11, day: 02, hour: 23 ).date // 2021-11-02 @ 11 PM (Tokyo time zone) ) var testDate = newYorkDate let heartbeatController = HeartbeatController( storage: HeartbeatStorageFake(), dateProvider: { testDate } ) // When heartbeatController.log("dummy_agent") // Device travels from NYC to Tokyo. testDate = tokyoDate heartbeatController.log("dummy_agent") // Then let payload = heartbeatController.flush() try HeartbeatLoggingTestUtils.assertEqualPayloadStrings( payload.headerValue(), """ { "version" : 2, "heartbeats" : [ { "agent" : "dummy_agent", "dates" : [ "2021-11-02" ] } ] } """ ) } func testLoggingDependsOnDateNotUserAgent() throws { // Given var testDate = date let heartbeatController = HeartbeatController( storage: HeartbeatStorageFake(), dateProvider: { testDate } ) // When // - Day 1 heartbeatController.log("dummy_agent") // - Day 2 testDate.addTimeInterval(60 * 60 * 24) heartbeatController.log("some_other_agent") // - Day 3 testDate.addTimeInterval(60 * 60 * 24) heartbeatController.log("dummy_agent") // Then let payload = heartbeatController.flush() try HeartbeatLoggingTestUtils.assertEqualPayloadStrings( payload.headerValue(), """ { "version": 2, "heartbeats": [ { "agent": "dummy_agent", "dates": [ "2021-11-01", "2021-11-03" ] }, { "agent": "some_other_agent", "dates": [ "2021-11-02" ] } ] } """ ) } func testFlushHeartbeatFromToday_WhenTodayHasAHeartbeat_ReturnsPayloadWithOnlyTodaysHeartbeat() throws { // Given let yesterdaysDate = date.addingTimeInterval(-1 * 60 * 60 * 24) let todaysDate = date let tomorrowsDate = date.addingTimeInterval(60 * 60 * 24) var testDate = yesterdaysDate let heartbeatController = HeartbeatController( storage: HeartbeatStorageFake(), dateProvider: { testDate } ) // When heartbeatController.log("yesterdays_dummy_agent") testDate = todaysDate heartbeatController.log("todays_dummy_agent") testDate = tomorrowsDate heartbeatController.log("tomorrows_dummy_agent") testDate = todaysDate // Then let payload = heartbeatController.flushHeartbeatFromToday() try HeartbeatLoggingTestUtils.assertEqualPayloadStrings( payload.headerValue(), """ { "version": 2, "heartbeats": [ { "agent": "todays_dummy_agent", "dates": ["2021-11-01"] } ] } """ ) let remainingPayload = heartbeatController.flush() try HeartbeatLoggingTestUtils.assertEqualPayloadStrings( remainingPayload.headerValue(), """ { "version": 2, "heartbeats": [ { "agent": "tomorrows_dummy_agent", "dates": ["2021-11-02"] }, { "agent": "yesterdays_dummy_agent", "dates": ["2021-10-31"] } ] } """ ) assertHeartbeatControllerFlushesEmptyPayload(heartbeatController) } func testFlushHeartbeatFromToday_WhenTodayDoesNotHaveAHeartbeat_ReturnsEmptyPayload() throws { // Given let heartbeatController = HeartbeatController(id: #function, dateProvider: { self.date }) // When heartbeatController.flushHeartbeatFromToday() // Then assertHeartbeatControllerFlushesEmptyPayload(heartbeatController) } } // MARK: - Fakes private class HeartbeatStorageFake: HeartbeatStorageProtocol { private var heartbeatsBundle: HeartbeatsBundle? func readAndWriteSync(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?) { heartbeatsBundle = transform(heartbeatsBundle) } func readAndWriteAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?) { heartbeatsBundle = transform(heartbeatsBundle) } func getAndSet(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?) throws -> HeartbeatsBundle? { let oldHeartbeatsBundle = heartbeatsBundle heartbeatsBundle = transform(heartbeatsBundle) return oldHeartbeatsBundle } }