Swift Testing for Unit Tests PART 1 (#5119)

* migrated a lot of unit tests to Swift Testing and added a new implementation for deferred fulfillment

more tests migration

Cleaned the code manually to establish some good patterns

more code improvements

some more code improvements

removed empty tests

update project

* more pr suggestions and cleanups

* removed the TestSetup pattern

* fixing claude not reusing tests

* pr suggestion + added indent rule to swiftformat so that we can prevent AIs to change that
This commit is contained in:
Mauro
2026-02-19 16:20:47 +01:00
committed by GitHub
parent c92e847ed7
commit 173b39a07f
118 changed files with 4630 additions and 4129 deletions

View File

@@ -7,20 +7,21 @@
//
@testable import ElementX
import XCTest
import Testing
@MainActor
class AppLockScreenViewModelTests: XCTestCase {
var appSettings: AppSettings!
var appLockService: AppLockService!
var keychainController: KeychainControllerMock!
var viewModel: AppLockScreenViewModelProtocol!
@Suite
final class AppLockScreenViewModelTests {
var appSettings: AppSettings
var appLockService: AppLockService
var keychainController: KeychainControllerMock
var viewModel: AppLockScreenViewModelProtocol
var context: AppLockScreenViewModelType.Context {
viewModel.context
}
override func setUp() {
init() {
AppSettings.resetAllSettings()
appSettings = AppSettings()
keychainController = KeychainControllerMock()
@@ -28,11 +29,12 @@ class AppLockScreenViewModelTests: XCTestCase {
viewModel = AppLockScreenViewModel(appLockService: appLockService)
}
override func tearDown() {
deinit {
AppSettings.resetAllSettings()
}
func testUnlock() async throws {
@Test
func unlock() async throws {
// Given a valid PIN code.
let pinCode = "2023"
keychainController.pinCodeReturnValue = pinCode
@@ -44,18 +46,19 @@ class AppLockScreenViewModelTests: XCTestCase {
let result = try await deferred.fulfill()
// The app should become unlocked.
XCTAssertEqual(result, .appUnlocked)
#expect(result == .appUnlocked)
}
func testForgotPIN() async throws {
@Test
func forgotPIN() async throws {
// Given a fresh launch of the app.
XCTAssertNil(context.alertInfo, "No alert should be shown initially.")
#expect(context.alertInfo == nil, "No alert should be shown initially.")
// When the user has forgotten their PIN.
context.send(viewAction: .forgotPIN)
// Then an alert should be shown before logging out.
XCTAssertEqual(context.alertInfo?.id, .confirmResetPIN, "An alert should be shown before logging out.")
#expect(context.alertInfo?.id == .confirmResetPIN, "An alert should be shown before logging out.")
// When confirming the logout.
let deferred = deferFulfillment(viewModel.actions) { $0 == .forceLogout }
@@ -65,14 +68,15 @@ class AppLockScreenViewModelTests: XCTestCase {
try await deferred.fulfill()
}
func testUnlockFailure() async throws {
@Test
func unlockFailure() async throws {
// Given an invalid PIN code.
let pinCode = "2024"
keychainController.pinCodeReturnValue = "2023"
keychainController.containsPINCodeBiometricStateReturnValue = false
XCTAssertEqual(context.viewState.numberOfPINAttempts, 0, "The shouldn't be any attempts yet.")
XCTAssertFalse(context.viewState.isSubtitleWarning, "No warning should be shown yet.")
XCTAssertNil(context.alertInfo, "No alert should be shown yet.")
#expect(context.viewState.numberOfPINAttempts == 0, "The shouldn't be any attempts yet.")
#expect(!context.viewState.isSubtitleWarning, "No warning should be shown yet.")
#expect(context.alertInfo == nil, "No alert should be shown yet.")
// When entering it on the lock screen.
var deferred = deferFulfillment(context.$viewState) { $0.numberOfPINAttempts == 1 }
@@ -81,9 +85,9 @@ class AppLockScreenViewModelTests: XCTestCase {
context.send(viewAction: .clearPINCode) // Simulate the animation completion
// Then a failed attempt should be shown.
XCTAssertEqual(context.viewState.numberOfPINAttempts, 1, "A failed attempt should have been recorded.")
XCTAssertTrue(context.viewState.isSubtitleWarning, "A warning should now be shown.")
XCTAssertNil(context.alertInfo, "No alert should be shown yet.")
#expect(context.viewState.numberOfPINAttempts == 1, "A failed attempt should have been recorded.")
#expect(context.viewState.isSubtitleWarning, "A warning should now be shown.")
#expect(context.alertInfo == nil, "No alert should be shown yet.")
// When entering twice more
deferred = deferFulfillment(context.$viewState) { $0.numberOfPINAttempts == 2 }
@@ -96,28 +100,28 @@ class AppLockScreenViewModelTests: XCTestCase {
context.send(viewAction: .clearPINCode) // Simulate the animation completion
// Then an alert should be shown
XCTAssertEqual(context.viewState.numberOfPINAttempts, 3, "All the attempts should have been recorded.")
XCTAssertTrue(context.viewState.isSubtitleWarning, "The warning should still be shown.")
XCTAssertEqual(context.alertInfo?.id, .forcedLogout, "An alert should now be shown.")
#expect(context.viewState.numberOfPINAttempts == 3, "All the attempts should have been recorded.")
#expect(context.viewState.isSubtitleWarning, "The warning should still be shown.")
#expect(context.alertInfo?.id == .forcedLogout, "An alert should now be shown.")
}
func testForceQuitRequiresLogout() async throws {
@Test
func forceQuitRequiresLogout() async throws {
// Given an app with a PIN set where the user attempted to unlock 3 times.
keychainController.pinCodeReturnValue = "2023"
keychainController.containsPINCodeBiometricStateReturnValue = false
appSettings.appLockNumberOfPINAttempts = 2
XCTAssertNil(context.alertInfo)
#expect(context.alertInfo == nil)
let deferred = deferFulfillment(context.$viewState) { $0.numberOfPINAttempts == 3 }
viewModel.context.pinCode = "0000"
try await deferred.fulfill()
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 3, "The app should have 3 failed attempts before the force quit.")
XCTAssertEqual(context.alertInfo?.id, .forcedLogout, "The app should be showing the alert before the force quit.")
#expect(appSettings.appLockNumberOfPINAttempts == 3, "The app should have 3 failed attempts before the force quit.")
#expect(context.alertInfo?.id == .forcedLogout, "The app should be showing the alert before the force quit.")
// When force quitting the app and relaunching.
viewModel = nil
let freshViewModel = AppLockScreenViewModel(appLockService: appLockService)
// Then the alert should remain in place
XCTAssertEqual(freshViewModel.context.alertInfo?.id, .forcedLogout, "The new view model from the fresh launch should also show the alert")
#expect(freshViewModel.context.alertInfo?.id == .forcedLogout, "The new view model from the fresh launch should also show the alert")
}
}

View File

@@ -7,15 +7,17 @@
//
@testable import ElementX
import XCTest
import Foundation
import Testing
@MainActor
class AppLockServiceTests: XCTestCase {
var keychainController: KeychainController!
var appSettings: AppSettings!
var service: AppLockService!
@Suite
final class AppLockServiceTests {
private var keychainController: KeychainController
private var appSettings: AppSettings
private var service: AppLockService
override func setUp() {
init() {
AppSettings.resetAllSettings()
appSettings = AppSettings()
@@ -26,34 +28,36 @@ class AppLockServiceTests: XCTestCase {
service.disable()
}
override func tearDown() {
deinit {
AppSettings.resetAllSettings()
}
// MARK: - PIN Code
func testValidPINCode() {
@Test
func validPINCode() {
// Given a service that hasn't been enabled.
XCTAssertFalse(service.isEnabled, "The service shouldn't be enabled to begin with.")
#expect(!service.isEnabled, "The service shouldn't be enabled to begin with.")
// When setting a PIN code.
let pinCode = "2023" // Highly secure PIN that is rotated every 12 months.
guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.")
Issue.record("The PIN should be valid.")
return
}
// Then service should be enabled and only the provided PIN should work to unlock the app.
XCTAssertTrue(service.isEnabled, "The service should become enabled when setting a PIN.")
XCTAssertTrue(service.unlock(with: pinCode), "The provided PIN code should work.")
XCTAssertFalse(service.unlock(with: "2024"), "No other PIN code should work.")
XCTAssertFalse(service.unlock(with: "1234"), "No other PIN code should work.")
XCTAssertFalse(service.unlock(with: "9999"), "No other PIN code should work.")
#expect(service.isEnabled, "The service should become enabled when setting a PIN.")
#expect(service.unlock(with: pinCode), "The provided PIN code should work.")
#expect(!service.unlock(with: "2024"), "No other PIN code should work.")
#expect(!service.unlock(with: "1234"), "No other PIN code should work.")
#expect(!service.unlock(with: "9999"), "No other PIN code should work.")
}
func testWeakPINCode() {
@Test
func weakPINCode() {
// Given a service that hasn't been enabled.
XCTAssertFalse(service.isEnabled, "The service shouldn't be enabled to begin with.")
#expect(!service.isEnabled, "The service shouldn't be enabled to begin with.")
// When setting a PIN code that is in the block list.
let pinCode = appSettings.appLockPINCodeBlockList[0]
@@ -61,16 +65,17 @@ class AppLockServiceTests: XCTestCase {
// Then the setup should fail and the service be left as disabled.
guard case let .failure(error) = result else {
XCTFail("The call should have failed.")
Issue.record("The call should have failed.")
return
}
XCTAssertEqual(error, .weakPIN, "The PIN should be rejected as weak.")
XCTAssertFalse(service.isEnabled, "The service should remain disabled.")
#expect(error == .weakPIN, "The PIN should be rejected as weak.")
#expect(!service.isEnabled, "The service should remain disabled.")
}
func testShortPINCode() {
@Test
func shortPINCode() {
// Given a service that hasn't been enabled.
XCTAssertFalse(service.isEnabled, "The service shouldn't be enabled to begin with.")
#expect(!service.isEnabled, "The service shouldn't be enabled to begin with.")
// When setting a PIN code that is too short
let pinCode = "123"
@@ -78,16 +83,17 @@ class AppLockServiceTests: XCTestCase {
// Then the setup should fail and the service be left as disabled.
guard case let .failure(error) = result else {
XCTFail("The call should have failed.")
Issue.record("The call should have failed.")
return
}
XCTAssertEqual(error, .invalidPIN, "The PIN should be rejected as invalid.")
XCTAssertFalse(service.isEnabled, "The service should remain disabled.")
#expect(error == .invalidPIN, "The PIN should be rejected as invalid.")
#expect(!service.isEnabled, "The service should remain disabled.")
}
func testNonNumericPINCode() {
@Test
func nonNumericPINCode() {
// Given a service that hasn't been enabled.
XCTAssertFalse(service.isEnabled, "The service shouldn't be enabled to begin with.")
#expect(!service.isEnabled, "The service shouldn't be enabled to begin with.")
// When setting a PIN code that is too short
let pinCode = "abcd"
@@ -95,116 +101,121 @@ class AppLockServiceTests: XCTestCase {
// Then the setup should fail and the service be left as disabled.
guard case let .failure(error) = result else {
XCTFail("The call should have failed.")
Issue.record("The call should have failed.")
return
}
XCTAssertEqual(error, .invalidPIN, "The PIN should be rejected as invalid.")
XCTAssertFalse(service.isEnabled, "The service should remain disabled.")
#expect(error == .invalidPIN, "The PIN should be rejected as invalid.")
#expect(!service.isEnabled, "The service should remain disabled.")
}
func testChangePINCode() {
@Test
func changePINCode() {
// Given a service that is already enabled with a PIN.
let pinCode = "2023"
let newPINCode = "2024"
guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.")
Issue.record("The PIN should be valid.")
return
}
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
XCTAssertTrue(service.unlock(with: pinCode), "The initial PIN should work.")
XCTAssertFalse(service.unlock(with: newPINCode), "The PIN we're about to set should not work.")
#expect(service.isEnabled, "The service should be enabled.")
#expect(service.unlock(with: pinCode), "The initial PIN should work.")
#expect(!service.unlock(with: newPINCode), "The PIN we're about to set should not work.")
// When updating the PIN code.
guard case .success = service.setupPINCode(newPINCode) else {
XCTFail("The PIN should be valid.")
Issue.record("The PIN should be valid.")
return
}
// Then the old code should not be accepted.
XCTAssertTrue(service.isEnabled, "The service should remain enabled.")
XCTAssertTrue(service.unlock(with: newPINCode), "The new PIN should work.")
XCTAssertFalse(service.unlock(with: pinCode), "The original PIN should be rejected.")
#expect(service.isEnabled, "The service should remain enabled.")
#expect(service.unlock(with: newPINCode), "The new PIN should work.")
#expect(!service.unlock(with: pinCode), "The original PIN should be rejected.")
}
func testInvalidChangePINCode() {
@Test
func invalidChangePINCode() {
// Given a service that is already enabled with a PIN.
let pinCode = "2023"
let invalidPIN = appSettings.appLockPINCodeBlockList[0]
guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.")
Issue.record("The PIN should be valid.")
return
}
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
XCTAssertTrue(service.unlock(with: pinCode), "The initial PIN should work.")
XCTAssertFalse(service.unlock(with: invalidPIN), "The PIN we're about to set should not work.")
#expect(service.isEnabled, "The service should be enabled.")
#expect(service.unlock(with: pinCode), "The initial PIN should work.")
#expect(!service.unlock(with: invalidPIN), "The PIN we're about to set should not work.")
// When updating the PIN code that is in the block list.
let result = service.setupPINCode(invalidPIN)
// Then it should fail and nothing should change.
guard case let .failure(error) = result else {
XCTFail("The call should have failed.")
Issue.record("The call should have failed.")
return
}
XCTAssertEqual(error, .weakPIN, "The PIN should be rejected as weak.")
XCTAssertTrue(service.isEnabled, "The service should remain enabled.")
XCTAssertFalse(service.unlock(with: invalidPIN), "The rejected PIN shouldn't work.")
XCTAssertTrue(service.unlock(with: pinCode), "The original PIN should continue to work.")
#expect(error == .weakPIN, "The PIN should be rejected as weak.")
#expect(service.isEnabled, "The service should remain enabled.")
#expect(!service.unlock(with: invalidPIN), "The rejected PIN shouldn't work.")
#expect(service.unlock(with: pinCode), "The original PIN should continue to work.")
}
func testDisablePINCode() {
@Test
func disablePINCode() {
// Given a service that is already enabled with a PIN.
let pinCode = "2023"
guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.")
Issue.record("The PIN should be valid.")
return
}
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
XCTAssertTrue(service.unlock(with: pinCode), "The initial PIN should work.")
#expect(service.isEnabled, "The service should be enabled.")
#expect(service.unlock(with: pinCode), "The initial PIN should work.")
// When disabling the PIN code.
service.disable()
// Then the PIN code should be removed.
XCTAssertFalse(service.isEnabled, "The service should no longer be enabled.")
XCTAssertFalse(service.unlock(with: pinCode), "The initial PIN shouldn't work any more.")
#expect(!service.isEnabled, "The service should no longer be enabled.")
#expect(!service.unlock(with: pinCode), "The initial PIN shouldn't work any more.")
}
// MARK: - Biometric Unlock
func testEnableBiometricUnlock() async {
@Test
func enableBiometricUnlock() async {
// Given a service with the PIN code already set.
let context = LAContextMock()
context.biometryTypeValue = .touchID
context.evaluatedPolicyDomainStateValue = Data("👆".utf8)
service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context)
guard case .success = service.setupPINCode("2023") else {
XCTFail("The PIN should be valid.")
Issue.record("The PIN should be valid.")
return
}
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should be in sync with the mock.")
XCTAssertFalse(service.biometricUnlockEnabled, "Biometric unlock should not be enabled.")
XCTAssertFalse(service.biometricUnlockTrusted, "Biometric unlock should not be trusted.")
#expect(service.isEnabled, "The service should be enabled.")
#expect(service.biometryType == .touchID, "The biometry type should be in sync with the mock.")
#expect(!service.biometricUnlockEnabled, "Biometric unlock should not be enabled.")
#expect(!service.biometricUnlockTrusted, "Biometric unlock should not be trusted.")
// When enabling biometric unlock.
guard case .success = service.enableBiometricUnlock() else {
XCTFail("The biometric lock should enable.")
Issue.record("The biometric lock should enable.")
return
}
context.evaluatePolicyReturnValue = true
// Then the service should be unlockable with biometrics.
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should not change.")
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should now be enabled.")
XCTAssertTrue(service.biometricUnlockTrusted, "Biometric unlock should now be trusted.")
#expect(service.biometryType == .touchID, "The biometry type should not change.")
#expect(service.biometricUnlockEnabled, "Biometric unlock should now be enabled.")
#expect(service.biometricUnlockTrusted, "Biometric unlock should now be trusted.")
guard await service.unlockWithBiometrics() == .unlocked else {
XCTFail("The biometric unlock should work.")
Issue.record("The biometric unlock should work.")
return
}
}
func testBiometricUnlockTrust() {
@Test
func biometricUnlockTrust() {
// Given a service with the PIN code already set.
let context = LAContextMock()
context.biometryTypeValue = .touchID
@@ -212,129 +223,133 @@ class AppLockServiceTests: XCTestCase {
service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context)
let pinCode = "2023"
guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.")
Issue.record("The PIN should be valid.")
return
}
guard case .success = service.enableBiometricUnlock() else {
XCTFail("The biometric lock should enable.")
Issue.record("The biometric lock should enable.")
return
}
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should be in sync with the mock.")
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should be enabled.")
XCTAssertTrue(service.biometricUnlockTrusted, "Biometric unlock should be trusted.")
#expect(service.isEnabled, "The service should be enabled.")
#expect(service.biometryType == .touchID, "The biometry type should be in sync with the mock.")
#expect(service.biometricUnlockEnabled, "Biometric unlock should be enabled.")
#expect(service.biometricUnlockTrusted, "Biometric unlock should be trusted.")
// When the user changes biometric data.
context.evaluatedPolicyDomainStateValue = Data("👈".utf8)
// Then biometric lock should remain enabled but untrusted.
XCTAssertTrue(service.isEnabled, "The service should remain enabled.")
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should not change.")
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should remain enabled.")
XCTAssertFalse(service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.")
#expect(service.isEnabled, "The service should remain enabled.")
#expect(service.biometryType == .touchID, "The biometry type should not change.")
#expect(service.biometricUnlockEnabled, "Biometric unlock should remain enabled.")
#expect(!service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.")
// When the user confirms their PIN code.
XCTAssertTrue(service.unlock(with: pinCode), "The PIN code should be accepted")
#expect(service.unlock(with: pinCode), "The PIN code should be accepted")
// Then the biometric lock should once again be trusted.
XCTAssertTrue(service.isEnabled, "The service should remain enabled.")
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should not change.")
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should remain enabled.")
XCTAssertTrue(service.biometricUnlockTrusted, "Biometric unlock should once again be trusted.")
#expect(service.isEnabled, "The service should remain enabled.")
#expect(service.biometryType == .touchID, "The biometry type should not change.")
#expect(service.biometricUnlockEnabled, "Biometric unlock should remain enabled.")
#expect(service.biometricUnlockTrusted, "Biometric unlock should once again be trusted.")
}
func testDisableBiometricUnlock() {
@Test
func disableBiometricUnlock() {
// Given a service with the PIN code already set.
let context = LAContextMock()
context.biometryTypeValue = .touchID
context.evaluatedPolicyDomainStateValue = Data("👆".utf8)
service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context)
guard case .success = service.setupPINCode("2023") else {
XCTFail("The PIN should be valid.")
Issue.record("The PIN should be valid.")
return
}
guard case .success = service.enableBiometricUnlock() else {
XCTFail("The biometric lock should enable.")
Issue.record("The biometric lock should enable.")
return
}
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should be in sync with the mock.")
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should be enabled.")
XCTAssertTrue(service.biometricUnlockTrusted, "Biometric unlock should be trusted.")
#expect(service.isEnabled, "The service should be enabled.")
#expect(service.biometryType == .touchID, "The biometry type should be in sync with the mock.")
#expect(service.biometricUnlockEnabled, "Biometric unlock should be enabled.")
#expect(service.biometricUnlockTrusted, "Biometric unlock should be trusted.")
// When disabling biometric unlock.
service.disableBiometricUnlock()
// Then only PIN unlock should remain enabled.
XCTAssertTrue(service.isEnabled, "The service should remain enabled.")
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should not change.")
XCTAssertFalse(service.biometricUnlockEnabled, "Biometric unlock should become disabled.")
XCTAssertFalse(service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.")
#expect(service.isEnabled, "The service should remain enabled.")
#expect(service.biometryType == .touchID, "The biometry type should not change.")
#expect(!service.biometricUnlockEnabled, "Biometric unlock should become disabled.")
#expect(!service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.")
}
func testDisablePINWithBiometricUnlock() {
@Test
func disablePINWithBiometricUnlock() {
// Given a service with the PIN code already set.
let context = LAContextMock()
context.biometryTypeValue = .touchID
context.evaluatedPolicyDomainStateValue = Data("👆".utf8)
service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context)
guard case .success = service.setupPINCode("2023") else {
XCTFail("The PIN should be valid.")
Issue.record("The PIN should be valid.")
return
}
guard case .success = service.enableBiometricUnlock() else {
XCTFail("The biometric lock should enable.")
Issue.record("The biometric lock should enable.")
return
}
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should be enabled.")
XCTAssertTrue(service.biometricUnlockTrusted, "Biometric unlock should be trusted.")
#expect(service.isEnabled, "The service should be enabled.")
#expect(service.biometricUnlockEnabled, "Biometric unlock should be enabled.")
#expect(service.biometricUnlockTrusted, "Biometric unlock should be trusted.")
// When disabling the PIN lock.
service.disable()
// Then both PIN and biometric unlock should be disabled.
XCTAssertFalse(service.isEnabled, "The service should remain enabled.")
XCTAssertFalse(service.biometricUnlockEnabled, "Biometric unlock should become disabled.")
XCTAssertFalse(service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.")
#expect(!service.isEnabled, "The service should remain enabled.")
#expect(!service.biometricUnlockEnabled, "Biometric unlock should become disabled.")
#expect(!service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.")
}
// MARK: - Attempt failures
func testResetAttemptsOnUnlock() {
@Test
func resetAttemptsOnUnlock() {
// Given a service that is enabled and has failed unlock attempts.
let pinCode = "2023"
guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.")
Issue.record("The PIN should be valid.")
return
}
appSettings.appLockNumberOfPINAttempts = 2
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 2, "The initial conditions should be stored.")
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
#expect(appSettings.appLockNumberOfPINAttempts == 2, "The initial conditions should be stored.")
#expect(service.isEnabled, "The service should be enabled.")
// When unlocking the service
XCTAssertTrue(service.unlock(with: pinCode), "The PIN should work.")
#expect(service.unlock(with: pinCode), "The PIN should work.")
// Then the attempts counts should both be reset.
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 0, "The PIN attempts should be reset.")
#expect(appSettings.appLockNumberOfPINAttempts == 0, "The PIN attempts should be reset.")
}
func testResetAttemptsOnDisable() {
@Test
func resetAttemptsOnDisable() {
// Given a service that is enabled and has failed unlock attempts.
let pinCode = "2023"
guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.")
Issue.record("The PIN should be valid.")
return
}
appSettings.appLockNumberOfPINAttempts = 2
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 2, "The initial conditions should be stored.")
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
#expect(appSettings.appLockNumberOfPINAttempts == 2, "The initial conditions should be stored.")
#expect(service.isEnabled, "The service should be enabled.")
// When disabling the service
service.disable()
XCTAssertFalse(service.isEnabled, "The service should be disabled.")
#expect(!service.isEnabled, "The service should be disabled.")
// Then the attempts counts should both be reset.
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 0, "The PIN attempts should be reset.")
#expect(appSettings.appLockNumberOfPINAttempts == 0, "The PIN attempts should be reset.")
}
}

View File

@@ -7,39 +7,40 @@
//
@testable import ElementX
import XCTest
import Testing
@MainActor
class AppLockSetupSettingsScreenViewModelTests: XCTestCase {
var appLockService: AppLockServiceProtocol!
var keychainController: KeychainControllerMock!
var viewModel: AppLockSetupSettingsScreenViewModelProtocol!
@Suite
struct AppLockSetupSettingsScreenViewModelTests {
var appLockService: AppLockServiceProtocol
var keychainController: KeychainControllerMock
var viewModel: AppLockSetupSettingsScreenViewModelProtocol
var context: AppLockSetupSettingsScreenViewModelType.Context {
viewModel.context
}
override func setUpWithError() throws {
init() {
keychainController = KeychainControllerMock()
appLockService = AppLockService(keychainController: keychainController, appSettings: AppSettings())
viewModel = AppLockSetupSettingsScreenViewModel(appLockService: AppLockServiceMock.mock())
}
func testDisablingShowsAlert() {
@Test
func disablingShowsAlert() {
// Given a fresh screen with the PIN code enabled.
let pinCode = "2023"
keychainController.pinCodeReturnValue = pinCode
keychainController.containsPINCodeReturnValue = true
XCTAssertNil(context.alertInfo)
XCTAssertTrue(appLockService.isEnabled)
#expect(context.alertInfo == nil)
#expect(appLockService.isEnabled)
// When disabling the PIN code lock.
context.send(viewAction: .disable)
// Then an alert should be shown before disabling it.
XCTAssertNotNil(context.alertInfo)
XCTAssertTrue(appLockService.isEnabled)
#expect(context.alertInfo != nil)
#expect(appLockService.isEnabled)
}
}

View File

@@ -7,18 +7,19 @@
//
@testable import ElementX
import XCTest
import Testing
@MainActor
class AppLockSetupBiometricsScreenViewModelTests: XCTestCase {
var appLockService: AppLockServiceMock!
var viewModel: AppLockSetupBiometricsScreenViewModelProtocol!
@Suite
final class AppLockSetupBiometricsScreenViewModelTests {
var appLockService: AppLockServiceMock
var viewModel: AppLockSetupBiometricsScreenViewModelProtocol
var context: AppLockSetupBiometricsScreenViewModelType.Context {
viewModel.context
}
override func setUp() {
init() {
AppSettings.resetAllSettings()
appLockService = AppLockServiceMock()
@@ -28,27 +29,29 @@ class AppLockSetupBiometricsScreenViewModelTests: XCTestCase {
viewModel = AppLockSetupBiometricsScreenViewModel(appLockService: appLockService)
}
override func tearDown() {
deinit {
AppSettings.resetAllSettings()
}
func testAllow() async throws {
@Test
func allow() async throws {
// When allowing Touch/Face ID.
let deferred = deferFulfillment(viewModel.actions) { $0 == .continue }
context.send(viewAction: .allow)
try await deferred.fulfill()
// Then the service should now have biometric unlock enabled.
XCTAssertEqual(appLockService.enableBiometricUnlockCallsCount, 1)
#expect(appLockService.enableBiometricUnlockCallsCount == 1)
}
func testSkip() async throws {
@Test
func skip() async throws {
// When skipping biometrics.
let deferred = deferFulfillment(viewModel.actions) { $0 == .continue }
context.send(viewAction: .skip)
try await deferred.fulfill()
// Then the service should now have biometric unlock enabled.
XCTAssertEqual(appLockService.enableBiometricUnlockCallsCount, 0)
#expect(appLockService.enableBiometricUnlockCallsCount == 0)
}
}

View File

@@ -7,10 +7,11 @@
//
@testable import ElementX
import XCTest
import Testing
@MainActor
class AppLockSetupPINScreenViewModelTests: XCTestCase {
@Suite
final class AppLockSetupPINScreenViewModelTests {
var appLockService: AppLockService!
var keychainController: KeychainControllerMock!
var viewModel: AppLockSetupPINScreenViewModelProtocol!
@@ -19,42 +20,40 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
viewModel.context
}
override func setUp() {
AppSettings.resetAllSettings()
keychainController = KeychainControllerMock()
appLockService = AppLockService(keychainController: keychainController, appSettings: AppSettings())
}
override func tearDown() {
deinit {
AppSettings.resetAllSettings()
}
func testCreatePIN() async throws {
@Test
func createPIN() async throws {
setup(mode: .create)
// Given the screen in create mode.
viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, isMandatory: false, appLockService: appLockService)
XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.")
#expect(context.viewState.mode == .create, "The mode should start as creation.")
// When entering an new PIN.
let createDeferred = deferFulfillment(context.$viewState, message: "A valid PIN needs confirming.") { $0.mode == .confirm }
let createDeferred = deferFulfillment(context.$viewState) { $0.mode == .confirm }
context.pinCode = "2023"
try await createDeferred.fulfill()
// Then the screen should transition to the confirm mode.
XCTAssertEqual(context.viewState.mode, .confirm, "The mode should transition to confirmation.")
#expect(context.viewState.mode == .confirm, "The mode should transition to confirmation.")
// When re-entering that PIN.
let confirmDeferred = deferFulfillment(viewModel.actions, message: "The screen should be finished.") { $0 == .complete }
let confirmDeferred = deferFulfillment(viewModel.actions) { $0 == .complete }
context.pinCode = "2023"
// Then the screen should signal it is complete.
try await confirmDeferred.fulfill()
}
func testCreateWeakPIN() async throws {
@Test
func createWeakPIN() async throws {
setup(mode: .create)
// Given the screen in create mode.
viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, isMandatory: false, appLockService: appLockService)
XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.")
XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.")
#expect(context.viewState.mode == .create, "The mode should start as creation.")
#expect(context.alertInfo == nil, "There shouldn't be an alert to begin with.")
// When entering a weak PIN on the blocklist.
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
@@ -62,22 +61,24 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
try await deferred.fulfill()
// Then the PIN should be rejected and the user alerted.
XCTAssertEqual(context.alertInfo?.id, .weakPIN, "The weak PIN should be rejected.")
XCTAssertEqual(context.viewState.mode, .create, "The mode shouldn't transition after an invalid PIN code.")
#expect(context.alertInfo?.id == .weakPIN, "The weak PIN should be rejected.")
#expect(context.viewState.mode == .create, "The mode shouldn't transition after an invalid PIN code.")
}
func testCreatePINMismatch() async throws {
// Given the confirm mode after entering a new PIN.
viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, isMandatory: false, appLockService: appLockService)
XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.")
XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.")
@Test
func createPINMismatch() async throws {
setup(mode: .create)
let createDeferred = deferFulfillment(context.$viewState, message: "A valid PIN needs confirming.") { $0.mode == .confirm }
// Given the confirm mode after entering a new PIN.
#expect(context.viewState.mode == .create, "The mode should start as creation.")
#expect(context.alertInfo == nil, "There shouldn't be an alert to begin with.")
let createDeferred = deferFulfillment(context.$viewState) { $0.mode == .confirm }
context.pinCode = "2023"
try await createDeferred.fulfill()
XCTAssertEqual(context.viewState.mode, .confirm, "The mode should transition to confirmation.")
XCTAssertEqual(context.viewState.numberOfConfirmAttempts, 0, "The mode should start with zero attempts.")
XCTAssertNil(context.alertInfo, "There shouldn't be an alert after a valid initial PIN.")
#expect(context.viewState.mode == .confirm, "The mode should transition to confirmation.")
#expect(context.viewState.numberOfConfirmAttempts == 0, "The mode should start with zero attempts.")
#expect(context.alertInfo == nil, "There shouldn't be an alert after a valid initial PIN.")
// When entering the new PIN incorrectly
var deferred = deferFulfillment(context.$viewState) { $0.numberOfConfirmAttempts == 1 }
@@ -85,8 +86,8 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
try await deferred.fulfill()
// Then the user should be alerted.
XCTAssertEqual(context.viewState.numberOfConfirmAttempts, 1, "The mismatch should be counted.")
XCTAssertEqual(context.alertInfo?.id, .pinMismatch, "A PIN mismatch should be rejected.")
#expect(context.viewState.numberOfConfirmAttempts == 1, "The mismatch should be counted.")
#expect(context.alertInfo?.id == .pinMismatch, "A PIN mismatch should be rejected.")
// When dismissing the alert and repeating twice more.
context.alertInfo?.primaryButton.action?()
@@ -97,42 +98,46 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
deferred = deferFulfillment(context.$viewState) { $0.numberOfConfirmAttempts == 3 }
context.pinCode = "2024"
try await deferred.fulfill()
XCTAssertEqual(context.viewState.numberOfConfirmAttempts, 3, "All the mismatches should be counted.")
XCTAssertEqual(context.alertInfo?.id, .pinMismatch, "A PIN mismatch should be rejected.")
#expect(context.viewState.numberOfConfirmAttempts == 3, "All the mismatches should be counted.")
#expect(context.alertInfo?.id == .pinMismatch, "A PIN mismatch should be rejected.")
// Then tapping the alert button should reset back to create mode.
context.alertInfo?.primaryButton.action?()
XCTAssertEqual(context.viewState.mode, .create, "The mode should revert back to creation.")
#expect(context.viewState.mode == .create, "The mode should revert back to creation.")
}
func testUnlock() async throws {
@Test
func unlock() async throws {
setup(mode: .unlock)
// Given the screen in unlock mode.
viewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, isMandatory: false, appLockService: appLockService)
let pinCode = "2023"
keychainController.pinCodeReturnValue = pinCode
keychainController.containsPINCodeReturnValue = true
keychainController.containsPINCodeBiometricStateReturnValue = false
// When entering the configured PIN.
let deferred = deferFulfillment(viewModel.actions, message: "The screen should be finished.") { $0 == .complete }
let deferred = deferFulfillment(viewModel.actions) { $0 == .complete }
context.pinCode = pinCode
// Then the screen should signal it is complete.
try await deferred.fulfill()
}
func testForgotPIN() async throws {
@Test
func forgotPIN() async throws {
setup(mode: .unlock)
// Given the screen in unlock mode.
viewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, isMandatory: false, appLockService: appLockService)
XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.")
XCTAssertFalse(context.viewState.isLoggingOut, "The view should not start disabled.")
#expect(context.alertInfo == nil, "There shouldn't be an alert to begin with.")
#expect(!context.viewState.isLoggingOut, "The view should not start disabled.")
// When the user has forgotten their PIN.
context.send(viewAction: .forgotPIN)
// Then an alert should be shown before logging out.
XCTAssertEqual(context.alertInfo?.id, .confirmResetPIN, "The weak PIN should be rejected.")
XCTAssertFalse(context.viewState.isLoggingOut, "The view should not be disabled until the user confirms.")
#expect(context.alertInfo?.id == .confirmResetPIN, "The weak PIN should be rejected.")
#expect(!context.viewState.isLoggingOut, "The view should not be disabled until the user confirms.")
// When confirming the logout.
let deferred = deferFulfillment(viewModel.actions) { $0 == .forceLogout }
@@ -140,44 +145,52 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
// Then a force logout should be initiated.
try await deferred.fulfill()
XCTAssertTrue(context.viewState.isLoggingOut, "The view should become disabled.")
#expect(context.viewState.isLoggingOut, "The view should become disabled.")
}
func testUnlockFailed() async throws {
@Test
func unlockFailed() async throws {
setup(mode: .unlock)
// Given the screen in unlock mode.
viewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, isMandatory: false, appLockService: appLockService)
keychainController.pinCodeReturnValue = "2023"
keychainController.containsPINCodeReturnValue = true
keychainController.containsPINCodeBiometricStateReturnValue = false
XCTAssertEqual(context.viewState.numberOfUnlockAttempts, 0, "The screen should start with zero attempts.")
XCTAssertFalse(context.viewState.isSubtitleWarning, "The subtitle should start without a warning.")
XCTAssertFalse(context.viewState.isLoggingOut, "The view should not start disabled.")
#expect(context.viewState.numberOfUnlockAttempts == 0, "The screen should start with zero attempts.")
#expect(!context.viewState.isSubtitleWarning, "The subtitle should start without a warning.")
#expect(!context.viewState.isLoggingOut, "The view should not start disabled.")
// When entering a different PIN.
var deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""],
message: "The PIN should be entered and then cleared by the view model.")
var deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""])
context.pinCode = "2024"
try await deferred.fulfill()
// Then the PIN should be rejected and the user notified.
XCTAssertEqual(context.viewState.numberOfUnlockAttempts, 1, "An invalid attempt should be counted.")
XCTAssertTrue(context.viewState.isSubtitleWarning, "The subtitle should then show a warning.")
XCTAssertFalse(context.viewState.isLoggingOut, "The view should still work.")
#expect(context.viewState.numberOfUnlockAttempts == 1, "An invalid attempt should be counted.")
#expect(context.viewState.isSubtitleWarning, "The subtitle should then show a warning.")
#expect(!context.viewState.isLoggingOut, "The view should still work.")
// When entering the same incorrect PIN twice more
deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""],
message: "The PIN should be entered and then cleared by the view model.")
deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""])
context.pinCode = "2024"
try await deferred.fulfill()
deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""],
message: "The PIN should be entered and then cleared by the view model.")
deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""])
context.pinCode = "2024"
try await deferred.fulfill()
// Then the user should be alerted that they're being signed out.
XCTAssertEqual(context.viewState.numberOfUnlockAttempts, 3, "All invalid attempts should be counted.")
XCTAssertTrue(context.viewState.isSubtitleWarning, "The subtitle should continue showing a warning.")
XCTAssertEqual(context.alertInfo?.id, .forceLogout, "An alert should be shown about a force logout.")
XCTAssertTrue(context.viewState.isLoggingOut, "The view should become disabled.")
#expect(context.viewState.numberOfUnlockAttempts == 3, "All invalid attempts should be counted.")
#expect(context.viewState.isSubtitleWarning, "The subtitle should continue showing a warning.")
#expect(context.alertInfo?.id == .forceLogout, "An alert should be shown about a force logout.")
#expect(context.viewState.isLoggingOut, "The view should become disabled.")
}
// MARK: - Helpers
private func setup(mode: AppLockSetupPINScreenMode) {
AppSettings.resetAllSettings()
keychainController = KeychainControllerMock()
appLockService = AppLockService(keychainController: keychainController, appSettings: AppSettings())
viewModel = AppLockSetupPINScreenViewModel(initialMode: mode, isMandatory: false, appLockService: appLockService)
}
}

View File

@@ -7,150 +7,155 @@
//
@testable import ElementX
import XCTest
import Foundation
import Testing
class AppLockTimerTests: XCTestCase {
var timer: AppLockTimer!
let now = Date.now
@Suite
struct AppLockTimerTests {
private let now = Date.now
private var timer: AppLockTimer!
var gracePeriod: TimeInterval {
timer.gracePeriod
}
var halfGracePeriod: TimeInterval {
gracePeriod / 2
timer.gracePeriod / 2
}
var gracePeriodX2: TimeInterval {
gracePeriod * 2
timer.gracePeriod * 2
}
var gracePeriodX10: TimeInterval {
gracePeriod * 10
timer.gracePeriod * 10
}
override func tearDown() {
timer = nil
@Test
mutating func timerLockedOnStartup() {
setupTimer(unlocked: false)
#expect(timer.computeLockState(didBecomeActiveAt: now),
"The app should be locked on a fresh launch.")
setupTimer(unlocked: false)
#expect(timer.computeLockState(didBecomeActiveAt: now + 1),
"The app should be locked after a fresh launch.")
setupTimer(unlocked: false)
#expect(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
"The app should be locked after a fresh launch.")
setupTimer(unlocked: false)
#expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
"The app should be locked after a fresh launch.")
setupTimer(unlocked: false)
#expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
"The app should be locked after a fresh launch.")
}
func testTimerLockedOnStartup() {
setupTimer(unlocked: false)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now),
"The app should be locked on a fresh launch.")
@Test
mutating func timerBeforeFirstUnlock() {
setupTimer(unlocked: false, backgroundedAt: now)
#expect(timer.computeLockState(didBecomeActiveAt: now),
"The app should always remain locked after backgrounding when locked.")
setupTimer(unlocked: false)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + 1),
"The app should be locked after a fresh launch.")
setupTimer(unlocked: false, backgroundedAt: now)
#expect(timer.computeLockState(didBecomeActiveAt: now + 1),
"The app should always remain locked after backgrounding when locked.")
setupTimer(unlocked: false)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
"The app should be locked after a fresh launch.")
setupTimer(unlocked: false, backgroundedAt: now)
#expect(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
"The app should always remain locked after backgrounding when locked.")
setupTimer(unlocked: false)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
"The app should be locked after a fresh launch.")
setupTimer(unlocked: false, backgroundedAt: now)
#expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
"The app should always remain locked after backgrounding when locked.")
setupTimer(unlocked: false)
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
"The app should be locked after a fresh launch.")
setupTimer(unlocked: false, backgroundedAt: now)
#expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
"The app should always remain locked after backgrounding when locked.")
}
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() {
@Test
mutating func timerWhenUnlocked() {
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.")
#expect(!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.")
#expect(!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.")
#expect(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.")
#expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
"The app should become locked when it was unlocked and backgrounded for more than the grace period.")
}
func testTimerRepeatingWithinGracePeriod() {
@Test
mutating func timerRepeatingWithinGracePeriod() {
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.")
#expect(!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.")
#expect(!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.")
#expect(!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.")
#expect(!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.")
#expect(timer.computeLockState(didBecomeActiveAt: nextCheck),
"The app should become locked however when finally staying backgrounded for longer than the grace period.")
}
func testTimerWithLongForeground() {
@Test
mutating func timerWithLongForeground() {
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.")
#expect(!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() {
@Test
mutating func changingTimeLocksApp() {
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.")
#expect(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() {
@Test
mutating func noGracePeriod() {
// 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))
#expect(timer.computeLockState(didBecomeActiveAt: backgroundDate))
}
func testResignActive() {
@Test
mutating func resignActive() {
// Given a timer with no grace period.
setupTimer(gracePeriod: 0, unlocked: true)
@@ -158,36 +163,32 @@ class AppLockTimerTests: XCTestCase {
timer.applicationDidEnterBackground(date: now)
// Then the app should be locked.
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + 1))
#expect(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))
#expect(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))
#expect(!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)
private mutating func setupTimer(gracePeriod: TimeInterval = 180, unlocked: Bool, backgroundedAt backgroundedDate: Date? = nil) {
let timer = AppLockTimer(gracePeriod: gracePeriod)
if unlocked {
timer.registerUnlock()
}
if let backgroundedDate {
timer.applicationDidEnterBackground(date: backgroundedDate)
}
self.timer = timer
}
}

View File

@@ -7,16 +7,18 @@
//
@testable import ElementX
import XCTest
import Testing
class PINTextFieldTests: XCTestCase {
func testSanitize() {
@Suite
struct PINTextFieldTests {
@Test
func sanitize() {
let textField = PINTextField(pinCode: .constant(""))
XCTAssertEqual(textField.sanitize("2"), "2")
XCTAssertEqual(textField.sanitize("2023"), "2023")
XCTAssertEqual(textField.sanitize("20233"), "2023")
XCTAssertEqual(textField.sanitize("20x"), "20")
XCTAssertEqual(textField.sanitize("20!"), "20")
XCTAssertEqual(textField.sanitize("boop"), "")
#expect(textField.sanitize("2") == "2")
#expect(textField.sanitize("2023") == "2023")
#expect(textField.sanitize("20233") == "2023")
#expect(textField.sanitize("20x") == "20")
#expect(textField.sanitize("20!") == "20")
#expect(textField.sanitize("boop") == "")
}
}