Files
element-x-ios/UnitTests/Sources/AppLock/AppLockScreenViewModelTests.swift
T
Michael 22d14ff8e4 AppSettings are Mockable (#5589)
* create DependenciesProtocol and replace ServiceLocator registration with it

replace mocking calls for ServiceLocator to Dependencies

replace ServiceLocator with instance level calls

replace ServiceLocator configs with TestDependencies config in Tests

replace servicelocator usage in analytics service

replace remaining ServiceLocator calls in unit tests

that depended on the UnitTestAppCoordinator configuration with per suite dependencies instances

fix swiftlint warning

remove remaining references to ServiceLocator

make dependencies properties consistent in tests

delete Dependencies and DependenciesProtocol

fix whitespace modifications

updates from PR feedback

a pass over converting AppSettings and AnalyticsService test instance properties to per test instantiation

fix a missed PR feedback

Sync up after rebase

create DependenciesProtocol and replace ServiceLocator registration with it

replace mocking calls for ServiceLocator to Dependencies

replace ServiceLocator with instance level calls

replace ServiceLocator configs with TestDependencies config in Tests

replace servicelocator usage in analytics service

replace remaining ServiceLocator calls in unit tests

that depended on the UnitTestAppCoordinator configuration with per suite dependencies instances

fix swiftlint warning

remove remaining references to ServiceLocator

make dependencies properties consistent in tests

delete Dependencies and DependenciesProtocol

* AppSettings is instance level mockable now

* replace mocked and preview usages of reset + AppSettings global init with

AppSettings(store: UserDefaultsMock())

* AppSettings.UserDefaultsKeys is now a RawRep struct, making it extensible

* update tests with new AppSettings so it builds

* update remaining AppSettings test/mock inits

* AppSettings.UserDefaultsKey conformed to ExpressibleByStringLiteral

* addressed issues found in first pass ai review

* update AppSettings instantiations after rebase

* fix accessibility tests coordinator after rebase

* Rename to VolatileUserDefaults

* AppSettings init uses mandatory parameter

* Revert UserDefaultsKey to enum

* Add TrackedUserDefaults to be able to reset the defaults object

* Added volatile static func to AppSettings

* Use .volatile() static func instead of full init

* review fixups

* Resolve build issues after rebase

* updated enterprise module

* remove the static reset settings call from the preview tests

---------

Co-authored-by: Mauro Romito <mauro.romito@element.io>
2026-05-22 15:14:08 +02:00

122 lines
5.3 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-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 Testing
@MainActor
final class AppLockScreenViewModelTests {
var appSettings: AppSettings
var appLockService: AppLockService
var keychainController: KeychainControllerMock
var viewModel: AppLockScreenViewModelProtocol
var context: AppLockScreenViewModelType.Context {
viewModel.context
}
init() {
appSettings = AppSettings.volatile()
keychainController = KeychainControllerMock()
appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings)
viewModel = AppLockScreenViewModel(appLockService: appLockService)
}
@Test
func unlock() async throws {
// Given a valid PIN code.
let pinCode = "2023"
keychainController.pinCodeReturnValue = pinCode
keychainController.containsPINCodeBiometricStateReturnValue = false
// When entering it on the lock screen.
let deferred = deferFulfillment(viewModel.actions) { $0 == .appUnlocked }
viewModel.context.pinCode = pinCode
let result = try await deferred.fulfill()
// The app should become unlocked.
#expect(result == .appUnlocked)
}
@Test
func forgotPIN() async throws {
// Given a fresh launch of the app.
#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.
#expect(context.alertInfo?.id == .confirmResetPIN, "An alert should be shown before logging out.")
// When confirming the logout.
let deferred = deferFulfillment(viewModel.actions) { $0 == .forceLogout }
context.alertInfo?.primaryButton.action?()
// Then a force logout should be initiated.
try await deferred.fulfill()
}
@Test
func unlockFailure() async throws {
// Given an invalid PIN code.
let pinCode = "2024"
keychainController.pinCodeReturnValue = "2023"
keychainController.containsPINCodeBiometricStateReturnValue = false
#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 }
viewModel.context.pinCode = pinCode
try await deferred.fulfill()
context.send(viewAction: .clearPINCode) // Simulate the animation completion
// Then a failed attempt should be shown.
#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 }
viewModel.context.pinCode = pinCode
try await deferred.fulfill()
context.send(viewAction: .clearPINCode) // Simulate the animation completion
deferred = deferFulfillment(context.$viewState) { $0.numberOfPINAttempts == 3 }
viewModel.context.pinCode = pinCode
try await deferred.fulfill()
context.send(viewAction: .clearPINCode) // Simulate the animation completion
// Then an alert should 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.")
}
@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
#expect(context.alertInfo == nil)
let deferred = deferFulfillment(context.$viewState) { $0.numberOfPINAttempts == 3 }
viewModel.context.pinCode = "0000"
try await deferred.fulfill()
#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.
let freshViewModel = AppLockScreenViewModel(appLockService: appLockService)
// Then the alert should remain in place
#expect(freshViewModel.context.alertInfo?.id == .forcedLogout, "The new view model from the fresh launch should also show the alert")
}
}