Files
letro-ios/UnitTests/Sources/AppLock/AppLockTimerTests.swift
2026-01-27 12:50:57 +02:00

194 lines
8.5 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
@testable import ElementX
import XCTest
class AppLockTimerTests: XCTestCase {
var timer: AppLockTimer!
let now = Date.now
var gracePeriod: TimeInterval {
timer.gracePeriod
}
var halfGracePeriod: TimeInterval {
gracePeriod / 2
}
var gracePeriodX2: TimeInterval {
gracePeriod * 2
}
var gracePeriodX10: TimeInterval {
gracePeriod * 10
}
override func tearDown() {
timer = nil
}
func testTimerLockedOnStartup() {
setupTimer(unlocked: false)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now),
"The app should be locked on a fresh launch.")
setupTimer(unlocked: false)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + 1),
"The app should be locked after a fresh launch.")
setupTimer(unlocked: false)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
"The app should be locked after a fresh launch.")
setupTimer(unlocked: false)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
"The app should be locked after a fresh launch.")
setupTimer(unlocked: false)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
"The app should be locked after a fresh launch.")
}
func testTimerBeforeFirstUnlock() {
setupTimer(unlocked: false, backgroundedAt: now)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now),
"The app should always remain locked after backgrounding when locked.")
setupTimer(unlocked: false, backgroundedAt: now)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + 1),
"The app should always remain locked after backgrounding when locked.")
setupTimer(unlocked: false, backgroundedAt: now)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
"The app should always remain locked after backgrounding when locked.")
setupTimer(unlocked: false, backgroundedAt: now)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
"The app should always remain locked after backgrounding when locked.")
setupTimer(unlocked: false, backgroundedAt: now)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
"The app should always remain locked after backgrounding when locked.")
}
func testTimerWhenUnlocked() {
setupTimer(unlocked: true, backgroundedAt: now)
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: now + 1),
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
setupTimer(unlocked: true, backgroundedAt: now)
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
setupTimer(unlocked: true, backgroundedAt: now)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
"The app should become locked when it was unlocked and backgrounded for more than the grace period.")
setupTimer(unlocked: true, backgroundedAt: now)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
"The app should become locked when it was unlocked and backgrounded for more than the grace period.")
}
func testTimerRepeatingWithinGracePeriod() {
setupTimer(unlocked: true, backgroundedAt: now)
var nextCheck = now + halfGracePeriod
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck),
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
timer.applicationDidEnterBackground(date: nextCheck)
nextCheck = now + gracePeriod
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck),
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
timer.applicationDidEnterBackground(date: nextCheck)
nextCheck = now + gracePeriod + halfGracePeriod
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck),
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
timer.applicationDidEnterBackground(date: nextCheck)
nextCheck = now + gracePeriodX2
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck),
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
timer.applicationDidEnterBackground(date: nextCheck)
nextCheck = now + gracePeriodX10
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: nextCheck),
"The app should become locked however when finally staying backgrounded for longer than the grace period.")
}
func testTimerWithLongForeground() {
setupTimer(unlocked: true)
let backgroundDate = now + gracePeriodX10
timer.applicationDidEnterBackground(date: backgroundDate)
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: backgroundDate + 1),
"The grace period should be measured from the time the app was backgrounded, and not when it was unlocked.")
}
func testChangingTimeLocksApp() {
setupTimer(unlocked: true, backgroundedAt: now)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now - 1),
"The the device's clock is changed to before the app was backgrounded, the device should remain locked.")
}
func testNoGracePeriod() {
// Given a timer with no grace period that is in the background.
setupTimer(gracePeriod: 0, unlocked: true)
let backgroundDate = now + 1
timer.applicationDidEnterBackground(date: backgroundDate)
// Then the app should be locked immediately.
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: backgroundDate))
}
func testResignActive() {
// Given a timer with no grace period.
setupTimer(gracePeriod: 0, unlocked: true)
// When entering the background.
timer.applicationDidEnterBackground(date: now)
// Then the app should be locked.
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + 1))
// When the app resigns active but doesn't enter the background.
// (Nothing to do here, we just don't call applicationDidEnterBackground).
// Then the app should also remain locked.
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + 2))
// When unlocking the app and resigning active (but not entering the background)
timer.registerUnlock()
// (Again, nothing to do here for resigning active)
// Then the app should not become locked.
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: now + 3))
}
// MARK: - Helpers
/// Sets up the timer for testing.
/// - Parameters:
/// - gracePeriod: Set up the test with a custom grace period for the timer. Defaults to 3 minutes.
/// - unlocked: Whether the timer should consider itself unlocked or not.
/// - backgroundedDate: If not nil, the timer will consider the app to have been backgrounded at the specified date.
private func setupTimer(gracePeriod: TimeInterval = 180, unlocked: Bool, backgroundedAt backgroundedDate: Date? = nil) {
timer = AppLockTimer(gracePeriod: gracePeriod)
if unlocked {
timer.registerUnlock()
}
if let backgroundedDate {
timer.applicationDidEnterBackground(date: backgroundedDate)
}
}
}