mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Initial service implementation for using a PIN code. (#1912)
* Initial service implementation for using a PIN code. * Tweak Danger for commit size 600-800 lines is perfectly normal for our PRs, up it to 1000.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
class AppLockScreenViewModelTests: XCTestCase {
|
||||
var appLockService: AppLockService!
|
||||
var keychainController: KeychainControllerMock!
|
||||
var viewModel: AppLockScreenViewModelProtocol!
|
||||
|
||||
var context: AppLockScreenViewModelType.Context { viewModel.context }
|
||||
|
||||
override func setUp() {
|
||||
AppSettings.reset()
|
||||
keychainController = KeychainControllerMock()
|
||||
appLockService = AppLockService(keychainController: keychainController, appSettings: AppSettings())
|
||||
viewModel = AppLockScreenViewModel(appLockService: appLockService)
|
||||
}
|
||||
|
||||
func testUnlock() async throws {
|
||||
// Given a valid PIN code.
|
||||
let pinCode = "2023"
|
||||
keychainController.pinCodeReturnValue = pinCode
|
||||
|
||||
// When entering it on the lock screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .appUnlocked }
|
||||
context.send(viewAction: .submitPINCode(pinCode))
|
||||
let result = try await deferred.fulfill()
|
||||
|
||||
// The app should become unlocked.
|
||||
XCTAssertEqual(result, .appUnlocked)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
class AppLockServiceTests: XCTestCase {
|
||||
var appSettings: AppSettings!
|
||||
var service: AppLockService!
|
||||
|
||||
override func setUp() {
|
||||
AppSettings.reset()
|
||||
appSettings = AppSettings()
|
||||
appSettings.appLockFlowEnabled = true
|
||||
|
||||
let keychainController = KeychainController(service: .tests, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
||||
|
||||
service = AppLockService(keychainController: keychainController, appSettings: appSettings)
|
||||
service.disable()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
AppSettings.reset()
|
||||
}
|
||||
|
||||
func testValidPINCode() {
|
||||
// Given a service that hasn't been enabled.
|
||||
XCTAssertFalse(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.")
|
||||
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.")
|
||||
}
|
||||
|
||||
func testWeakPINCode() {
|
||||
// Given a service that hasn't been enabled.
|
||||
XCTAssertFalse(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 {
|
||||
XCTFail("The call should have failed.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(error, .weakPIN, "The PIN should be rejected as weak.")
|
||||
XCTAssertFalse(service.isEnabled, "The service should remain disabled.")
|
||||
}
|
||||
|
||||
func testShortPINCode() {
|
||||
// Given a service that hasn't been enabled.
|
||||
XCTAssertFalse(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 {
|
||||
XCTFail("The call should have failed.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(error, .invalidPIN, "The PIN should be rejected as invalid.")
|
||||
XCTAssertFalse(service.isEnabled, "The service should remain disabled.")
|
||||
}
|
||||
|
||||
func testNonNumericPINCode() {
|
||||
// Given a service that hasn't been enabled.
|
||||
XCTAssertFalse(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 {
|
||||
XCTFail("The call should have failed.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(error, .invalidPIN, "The PIN should be rejected as invalid.")
|
||||
XCTAssertFalse(service.isEnabled, "The service should remain disabled.")
|
||||
}
|
||||
|
||||
func testChangePINCode() {
|
||||
// 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.")
|
||||
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.")
|
||||
|
||||
// When updating the PIN code.
|
||||
guard case .success = service.setupPINCode(newPINCode) else {
|
||||
XCTFail("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.")
|
||||
}
|
||||
|
||||
func testInvalidChangePINCode() {
|
||||
// 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.")
|
||||
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.")
|
||||
|
||||
// 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.")
|
||||
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.")
|
||||
}
|
||||
|
||||
func testDisablePINCode() {
|
||||
// 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.")
|
||||
return
|
||||
}
|
||||
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
|
||||
XCTAssertTrue(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.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
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(willEnterForegroundAt: now),
|
||||
"The app should be locked on a fresh launch.")
|
||||
|
||||
setupTimer(unlocked: false)
|
||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + 1),
|
||||
"The app should be locked after a fresh launch.")
|
||||
|
||||
setupTimer(unlocked: false)
|
||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + halfGracePeriod),
|
||||
"The app should be locked after a fresh launch.")
|
||||
|
||||
setupTimer(unlocked: false)
|
||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriod),
|
||||
"The app should be locked after a fresh launch.")
|
||||
|
||||
setupTimer(unlocked: false)
|
||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriodX10),
|
||||
"The app should be locked after a fresh launch.")
|
||||
}
|
||||
|
||||
func testTimerBeforeFirstUnlock() {
|
||||
setupTimer(unlocked: false, backgroundedAt: now)
|
||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now),
|
||||
"The app should always remain locked after backgrounding when locked.")
|
||||
|
||||
setupTimer(unlocked: false, backgroundedAt: now)
|
||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + 1),
|
||||
"The app should always remain locked after backgrounding when locked.")
|
||||
|
||||
setupTimer(unlocked: false, backgroundedAt: now)
|
||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + halfGracePeriod),
|
||||
"The app should always remain locked after backgrounding when locked.")
|
||||
|
||||
setupTimer(unlocked: false, backgroundedAt: now)
|
||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriod),
|
||||
"The app should always remain locked after backgrounding when locked.")
|
||||
|
||||
setupTimer(unlocked: false, backgroundedAt: now)
|
||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriodX10),
|
||||
"The app should always remain locked after backgrounding when locked.")
|
||||
}
|
||||
|
||||
func testTimerWhenUnlocked() {
|
||||
setupTimer(unlocked: true, backgroundedAt: now)
|
||||
XCTAssertFalse(timer.computeLockState(willEnterForegroundAt: 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(willEnterForegroundAt: 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(willEnterForegroundAt: 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(willEnterForegroundAt: 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(willEnterForegroundAt: 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(willEnterForegroundAt: 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(willEnterForegroundAt: 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(willEnterForegroundAt: 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(willEnterForegroundAt: 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(willEnterForegroundAt: 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(willEnterForegroundAt: now - 1),
|
||||
"The the device's clock is changed to before the app was backgrounded, the device should remain locked.")
|
||||
}
|
||||
|
||||
/// Sets up the timer for testing.
|
||||
/// - Parameters:
|
||||
/// - 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(unlocked: Bool, backgroundedAt backgroundedDate: Date? = nil) {
|
||||
timer = AppLockTimer(gracePeriod: 180)
|
||||
if unlocked {
|
||||
timer.registerUnlock()
|
||||
}
|
||||
if let backgroundedDate {
|
||||
timer.applicationDidEnterBackground(date: backgroundedDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user