Files
letro-ios/UnitTests/Sources/AppLock/AppLockServiceTests.swift
Mauro 173b39a07f 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
2026-02-19 16:20:47 +01:00

356 lines
15 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 Foundation
import Testing
@MainActor
@Suite
final class AppLockServiceTests {
private var keychainController: KeychainController
private var appSettings: AppSettings
private var service: AppLockService
init() {
AppSettings.resetAllSettings()
appSettings = AppSettings()
keychainController = KeychainController(service: .tests, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
keychainController.resetSecrets()
service = AppLockService(keychainController: keychainController, appSettings: appSettings)
service.disable()
}
deinit {
AppSettings.resetAllSettings()
}
// MARK: - PIN Code
@Test
func validPINCode() {
// Given a service that hasn't been enabled.
#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 {
Issue.record("The PIN should be valid.")
return
}
// Then service should be enabled and only the provided PIN should work to unlock the app.
#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.")
}
@Test
func weakPINCode() {
// Given a service that hasn't been enabled.
#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]
let result = service.setupPINCode(pinCode)
// Then the setup should fail and the service be left as disabled.
guard case let .failure(error) = result else {
Issue.record("The call should have failed.")
return
}
#expect(error == .weakPIN, "The PIN should be rejected as weak.")
#expect(!service.isEnabled, "The service should remain disabled.")
}
@Test
func shortPINCode() {
// Given a service that hasn't been enabled.
#expect(!service.isEnabled, "The service shouldn't be enabled to begin with.")
// When setting a PIN code that is too short
let pinCode = "123"
let result = service.setupPINCode(pinCode)
// Then the setup should fail and the service be left as disabled.
guard case let .failure(error) = result else {
Issue.record("The call should have failed.")
return
}
#expect(error == .invalidPIN, "The PIN should be rejected as invalid.")
#expect(!service.isEnabled, "The service should remain disabled.")
}
@Test
func nonNumericPINCode() {
// Given a service that hasn't been enabled.
#expect(!service.isEnabled, "The service shouldn't be enabled to begin with.")
// When setting a PIN code that is too short
let pinCode = "abcd"
let result = service.setupPINCode(pinCode)
// Then the setup should fail and the service be left as disabled.
guard case let .failure(error) = result else {
Issue.record("The call should have failed.")
return
}
#expect(error == .invalidPIN, "The PIN should be rejected as invalid.")
#expect(!service.isEnabled, "The service should remain disabled.")
}
@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 {
Issue.record("The PIN should be valid.")
return
}
#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 {
Issue.record("The PIN should be valid.")
return
}
// Then the old code should not be accepted.
#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.")
}
@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 {
Issue.record("The PIN should be valid.")
return
}
#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 {
Issue.record("The call should have failed.")
return
}
#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.")
}
@Test
func disablePINCode() {
// Given a service that is already enabled with a PIN.
let pinCode = "2023"
guard case .success = service.setupPINCode(pinCode) else {
Issue.record("The PIN should be valid.")
return
}
#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.
#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
@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 {
Issue.record("The PIN should be valid.")
return
}
#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 {
Issue.record("The biometric lock should enable.")
return
}
context.evaluatePolicyReturnValue = true
// Then the service should be unlockable with biometrics.
#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 {
Issue.record("The biometric unlock should work.")
return
}
}
@Test
func biometricUnlockTrust() {
// 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)
let pinCode = "2023"
guard case .success = service.setupPINCode(pinCode) else {
Issue.record("The PIN should be valid.")
return
}
guard case .success = service.enableBiometricUnlock() else {
Issue.record("The biometric lock should enable.")
return
}
#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.
#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.
#expect(service.unlock(with: pinCode), "The PIN code should be accepted")
// Then the biometric lock 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.")
}
@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 {
Issue.record("The PIN should be valid.")
return
}
guard case .success = service.enableBiometricUnlock() else {
Issue.record("The biometric lock should enable.")
return
}
#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.
#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.")
}
@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 {
Issue.record("The PIN should be valid.")
return
}
guard case .success = service.enableBiometricUnlock() else {
Issue.record("The biometric lock should enable.")
return
}
#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.
#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
@Test
func resetAttemptsOnUnlock() {
// Given a service that is enabled and has failed unlock attempts.
let pinCode = "2023"
guard case .success = service.setupPINCode(pinCode) else {
Issue.record("The PIN should be valid.")
return
}
appSettings.appLockNumberOfPINAttempts = 2
#expect(appSettings.appLockNumberOfPINAttempts == 2, "The initial conditions should be stored.")
#expect(service.isEnabled, "The service should be enabled.")
// When unlocking the service
#expect(service.unlock(with: pinCode), "The PIN should work.")
// Then the attempts counts should both be reset.
#expect(appSettings.appLockNumberOfPINAttempts == 0, "The PIN attempts should be reset.")
}
@Test
func resetAttemptsOnDisable() {
// Given a service that is enabled and has failed unlock attempts.
let pinCode = "2023"
guard case .success = service.setupPINCode(pinCode) else {
Issue.record("The PIN should be valid.")
return
}
appSettings.appLockNumberOfPINAttempts = 2
#expect(appSettings.appLockNumberOfPINAttempts == 2, "The initial conditions should be stored.")
#expect(service.isEnabled, "The service should be enabled.")
// When disabling the service
service.disable()
#expect(!service.isEnabled, "The service should be disabled.")
// Then the attempts counts should both be reset.
#expect(appSettings.appLockNumberOfPINAttempts == 0, "The PIN attempts should be reset.")
}
}