TM-SGNL-iOS/SignalServiceKit/tests/Calls/DeletedCallRecord/DeletedCallRecordCleanupManagerTest.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

195 lines
7.9 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import XCTest
@testable import SignalServiceKit
final class DeletedCallRecordCleanupManagerTest: XCTestCase {
private var timeIntervalProvider: DeletedCallRecordCleanupManagerImpl.TimeIntervalProvider = { owsFail("Not implemented!") }
private var dateProvider: DateProvider = { owsFail("Not implemented!") }
private var db: InMemoryDB!
private var deletedCallRecordStore: MockDeletedCallRecordStore!
private var testScheduler: TestScheduler!
private var cleanupManager: DeletedCallRecordCleanupManagerImpl!
override func setUp() {
db = InMemoryDB()
deletedCallRecordStore = MockDeletedCallRecordStore()
testScheduler = TestScheduler()
cleanupManager = DeletedCallRecordCleanupManagerImpl(
minimumSecondsBetweenCleanupPasses: { self.timeIntervalProvider() },
callLinkStore: CallLinkRecordStoreImpl(),
dateProvider: { self.dateProvider() },
db: db,
deletedCallRecordStore: deletedCallRecordStore,
schedulers: TestSchedulers(scheduler: testScheduler)
)
}
func testCleanupIfNecessary_noActionIfNoExpiredRecords() {
deletedCallRecordStore.deleteMock = { _ in
XCTFail("Shouldn't be deleting!")
}
testScheduler.start()
cleanupManager.startCleanupIfNecessary()
}
func testCleanupIfNecessary() {
timeIntervalProvider = { 2 }
deletedCallRecordStore.deletedCallRecords = [
.fixture(deletedAtSeconds: 3),
.fixture(deletedAtSeconds: 4),
.fixture(deletedAtSeconds: 5),
.fixture(deletedAtSeconds: 6),
.fixture(deletedAtSeconds: 7),
.fixture(deletedAtSeconds: 9),
]
/// This will dispatch async cleanup to start whenever we start the
/// scheduler.
cleanupManager.startCleanupIfNecessary()
/// This will run the on-start async cleanup work, which will delete
/// records 3, 4, and 5. Cleanup for record 6 will be scheduled two
/// ticks from now, due to the debounce.
dateProvider = { .fixture(seconds: 5) }
testScheduler.advance(to: 0)
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords.count, 3)
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords[0].deletedAtTimestamp, 6000)
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords[1].deletedAtTimestamp, 7000)
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords[2].deletedAtTimestamp, 9000)
/// This should do nothing, since we're in the debounce.
dateProvider = { .fixture(seconds: 6) }
testScheduler.tick()
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords.count, 3)
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords[0].deletedAtTimestamp, 6000)
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords[1].deletedAtTimestamp, 7000)
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords[2].deletedAtTimestamp, 9000)
/// This will clean up record 6 _and_ record 7, and schedule record 9
/// for cleanup after two more ticks.
dateProvider = { .fixture(seconds: 7) }
testScheduler.tick()
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords.count, 1)
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords[0].deletedAtTimestamp, 9000)
/// This should do nothing, since record 9 is still scheduled for future
/// deletion.
dateProvider = { .fixture(seconds: 8) }
testScheduler.tick()
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords.count, 1)
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords[0].deletedAtTimestamp, 9000)
/// This should delete record 9, after which we should have no more work
/// scheduled.
dateProvider = { .fixture(seconds: 9) }
testScheduler.tick()
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords.count, 0)
deletedCallRecordStore.deleteMock = { _ in XCTFail("Shouldn't be deleting!") }
dateProvider = { .fixture(seconds: 10) }
testScheduler.tick()
}
/// A call to start cleanup while cleanup is already scheduled should bail
/// out immediately. To test this we'll create a scenario in which a record
/// is scheduled for deletion, then attempt to re-start cleanup this
/// shouldn't even ask for the next record.
func testReentrance() {
timeIntervalProvider = { 1 }
/// There's nothing to clean up, so this should do nothing.
cleanupManager.startCleanupIfNecessary()
testScheduler.advance(to: 0)
deletedCallRecordStore.deletedCallRecords = [
.fixture(deletedAtSeconds: 4),
.fixture(deletedAtSeconds: 5),
]
/// This will delete record 4 and schedule record 5 for deletion for one
/// tick from now...
cleanupManager.startCleanupIfNecessary()
dateProvider = { .fixture(seconds: 4) }
testScheduler.tick()
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords.count, 1)
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords[0].deletedAtTimestamp, 5000)
/// ...which means this identical call shouldn't do anything...
deletedCallRecordStore.deletedCallRecords = [
.fixture(deletedAtSeconds: 4),
.fixture(deletedAtSeconds: 5),
]
deletedCallRecordStore.nextDeletedRecordMock = {
XCTFail("Shouldn't be asking for next deleted!")
return nil
}
cleanupManager.startCleanupIfNecessary()
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords.count, 2)
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords[0].deletedAtTimestamp, 4000)
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords[1].deletedAtTimestamp, 5000)
/// ...but if we reset the mocks and tick, we should perform the
/// scheduled delete of record 5...
deletedCallRecordStore.nextDeletedRecordMock = nil
deletedCallRecordStore.deletedCallRecords = [.fixture(deletedAtSeconds: 5)]
dateProvider = { .fixture(seconds: 5) }
testScheduler.tick()
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords.count, 0)
/// ...after which subsequent ticking should do nothing...
deletedCallRecordStore.nextDeletedRecordMock = {
XCTFail("Shouldn't be asking for next deleted!")
return nil
}
dateProvider = { .fixture(seconds: 6) }
testScheduler.tick()
/// ...but when we reset the mocks with a new record to delete and
/// re-start cleanup, we should be back in business.
deletedCallRecordStore.deletedCallRecords = [.fixture(deletedAtSeconds: 7)]
deletedCallRecordStore.nextDeletedRecordMock = nil
cleanupManager.startCleanupIfNecessary()
dateProvider = { .fixture(seconds: 7) }
testScheduler.tick()
XCTAssertEqual(deletedCallRecordStore.deletedCallRecords.count, 0)
}
}
private extension DeletedCallRecord {
static func fixture(deletedAtSeconds: UInt64) -> DeletedCallRecord {
return DeletedCallRecord(
callId: .maxRandom,
conversationId: .thread(threadRowId: .maxRandom),
deletedAtTimestamp: deletedAtSeconds.milliseconds
)
}
}
private extension Date {
/// Adds the "lifetime" for a deleted call record to the given time. That
/// lifetime constant is copied from the one hardcoded into
/// ``DeletedCallRecord`` in the cleanup manager.
static func fixture(seconds: UInt64) -> Date {
return Date(millisecondsSince1970: seconds.milliseconds + UInt64(8 * kHourInterval).milliseconds)
}
}
private extension UInt64 {
var milliseconds: UInt64 {
return self * 1000
}
}