mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-15 14:00:25 -06:00
[PM-21681] Handle automatic timeout logout in BWA (#1659)
This commit is contained in:
parent
5356ac33da
commit
702e43b689
@ -1,5 +1,6 @@
|
||||
import BitwardenKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
// MARK: - AuthenticatorBridgeItemService
|
||||
@ -93,6 +94,9 @@ public class DefaultAuthenticatorBridgeItemService: AuthenticatorBridgeItemServi
|
||||
/// The keychain repository for working with the shared key.
|
||||
let sharedKeychainRepository: SharedKeychainRepository
|
||||
|
||||
/// A service that manages account timeout between apps.
|
||||
let sharedTimeoutService: SharedTimeoutService
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initialize a `DefaultAuthenticatorBridgeItemService`
|
||||
@ -101,13 +105,16 @@ public class DefaultAuthenticatorBridgeItemService: AuthenticatorBridgeItemServi
|
||||
/// - cryptoService: Cryptography service for encrypting/decrypting items.
|
||||
/// - dataStore: The CoreData store for working with shared data
|
||||
/// - sharedKeychainRepository: The keychain repository for working with the shared key.
|
||||
/// - sharedTimeoutService: The shared timeout service for managing session timeouts.
|
||||
///
|
||||
public init(cryptoService: SharedCryptographyService,
|
||||
dataStore: AuthenticatorBridgeDataStore,
|
||||
sharedKeychainRepository: SharedKeychainRepository) {
|
||||
sharedKeychainRepository: SharedKeychainRepository,
|
||||
sharedTimeoutService: SharedTimeoutService) {
|
||||
self.cryptoService = cryptoService
|
||||
self.dataStore = dataStore
|
||||
self.sharedKeychainRepository = sharedKeychainRepository
|
||||
self.sharedTimeoutService = sharedTimeoutService
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
@ -192,6 +199,7 @@ public class DefaultAuthenticatorBridgeItemService: AuthenticatorBridgeItemServi
|
||||
|
||||
public func sharedItemsPublisher() async throws ->
|
||||
AnyPublisher<[AuthenticatorBridgeItemDataView], any Error> {
|
||||
try await checkForLogout()
|
||||
let fetchRequest = AuthenticatorBridgeItemData.fetchRequest(
|
||||
predicate: NSPredicate(
|
||||
format: "userId != %@", DefaultAuthenticatorBridgeItemService.temporaryUserId
|
||||
@ -210,4 +218,25 @@ public class DefaultAuthenticatorBridgeItemService: AuthenticatorBridgeItemServi
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// MARK: Private Functions
|
||||
|
||||
/// Iterates through all of the users with shared items and determines if they've passed their
|
||||
/// logout timeout. If so, then their shared items are deleted.
|
||||
///
|
||||
private func checkForLogout() async throws {
|
||||
let fetchRequest = NSFetchRequest<NSDictionary>(entityName: AuthenticatorBridgeItemData.entityName)
|
||||
fetchRequest.propertiesToFetch = ["userId"]
|
||||
fetchRequest.returnsDistinctResults = true
|
||||
fetchRequest.resultType = .dictionaryResultType
|
||||
|
||||
let results = try dataStore.persistentContainer.viewContext.fetch(fetchRequest)
|
||||
let userIds = results.compactMap { ($0 as? [String: Any])?["userId"] as? String }
|
||||
|
||||
try await userIds.asyncForEach { userId in
|
||||
if try await sharedTimeoutService.hasPassedTimeout(userId: userId) {
|
||||
try await deleteAllForUserId(userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import Combine
|
||||
|
||||
public class MockAuthenticatorBridgeItemService: AuthenticatorBridgeItemService {
|
||||
public var errorToThrow: Error?
|
||||
public var replaceAllCalled = false
|
||||
public var sharedItemsSubject = CurrentValueSubject<[AuthenticatorBridgeItemDataView], Error>([])
|
||||
public var storedItems: [String: [AuthenticatorBridgeItemDataView]] = [:]
|
||||
public var syncOn = false
|
||||
public var tempItem: AuthenticatorBridgeItemDataView?
|
||||
|
||||
public init() {}
|
||||
|
||||
public func checkForLogout() async throws {
|
||||
|
||||
}
|
||||
|
||||
public func deleteAllForUserId(_ userId: String) async throws {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
storedItems[userId] = []
|
||||
}
|
||||
|
||||
public func fetchAllForUserId(_ userId: String) async throws -> [AuthenticatorBridgeItemDataView] {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
return storedItems[userId] ?? []
|
||||
}
|
||||
|
||||
public func fetchTemporaryItem() async throws -> AuthenticatorBridgeItemDataView? {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
return tempItem
|
||||
}
|
||||
|
||||
public func insertTemporaryItem(_ item: AuthenticatorBridgeItemDataView) async throws {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
tempItem = item
|
||||
}
|
||||
|
||||
public func insertItems(_ items: [AuthenticatorBridgeItemDataView], forUserId userId: String) async throws {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
storedItems[userId] = items
|
||||
}
|
||||
|
||||
public func isSyncOn() async -> Bool {
|
||||
syncOn
|
||||
}
|
||||
|
||||
public func replaceAllItems(with items: [AuthenticatorBridgeItemDataView], forUserId userId: String) async throws {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
storedItems[userId] = items
|
||||
replaceAllCalled = true
|
||||
}
|
||||
|
||||
public func sharedItemsPublisher() async throws ->
|
||||
AnyPublisher<[AuthenticatorBridgeKit.AuthenticatorBridgeItemDataView], any Error> {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
|
||||
return sharedItemsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
39
AuthenticatorBridgeKit/Mocks/MockSharedTimeoutService.swift
Normal file
39
AuthenticatorBridgeKit/Mocks/MockSharedTimeoutService.swift
Normal file
@ -0,0 +1,39 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
public final class MockSharedTimeoutService: SharedTimeoutService {
|
||||
public var clearTimeoutUserIds = [String]()
|
||||
public var clearTimeoutError: Error?
|
||||
public var hasPassedTimeoutResult: Result<[String: Bool], Error> = .success([:])
|
||||
public var updateTimeoutUserId: String?
|
||||
public var updateTimeoutLastActiveDate: Date?
|
||||
public var updateTimeoutTimeoutLength: SessionTimeoutValue?
|
||||
public var updateTimeoutError: Error?
|
||||
|
||||
public init() {}
|
||||
|
||||
public func clearTimeout(forUserId userId: String) async throws {
|
||||
if let clearTimeoutError {
|
||||
throw clearTimeoutError
|
||||
}
|
||||
clearTimeoutUserIds.append(userId)
|
||||
}
|
||||
|
||||
public func hasPassedTimeout(userId: String) async throws -> Bool {
|
||||
try hasPassedTimeoutResult.get()[userId] ?? false
|
||||
}
|
||||
|
||||
public func updateTimeout(
|
||||
forUserId userId: String,
|
||||
lastActiveDate: Date?,
|
||||
timeoutLength: SessionTimeoutValue
|
||||
) async throws {
|
||||
if let updateTimeoutError {
|
||||
throw updateTimeoutError
|
||||
}
|
||||
updateTimeoutUserId = userId
|
||||
updateTimeoutLastActiveDate = lastActiveDate
|
||||
updateTimeoutTimeoutLength = timeoutLength
|
||||
}
|
||||
}
|
||||
@ -107,16 +107,18 @@ public class DefaultSharedKeychainStorage: SharedKeychainStorage {
|
||||
)
|
||||
|
||||
guard let resultDictionary = foundItem as? [String: Any],
|
||||
let data = resultDictionary[kSecValueData as String] as? T else {
|
||||
let data = resultDictionary[kSecValueData as String] as? Data else {
|
||||
throw SharedKeychainServiceError.keyNotFound(item)
|
||||
}
|
||||
|
||||
return data
|
||||
let object = try JSONDecoder.defaultDecoder.decode(T.self, from: data)
|
||||
return object
|
||||
}
|
||||
|
||||
public func setValue<T: Codable>(_ value: T, for item: SharedKeychainItem) async throws {
|
||||
let valueData = try JSONEncoder.defaultEncoder.encode(value)
|
||||
let query = [
|
||||
kSecValueData: value,
|
||||
kSecValueData: valueData,
|
||||
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
||||
kSecAttrAccessGroup: sharedAppGroupIdentifier,
|
||||
kSecAttrAccount: item.unformattedKey,
|
||||
|
||||
@ -52,8 +52,9 @@ final class SharedKeychainStorageTests: BitwardenTestCase {
|
||||
func test_getValue_success() async throws {
|
||||
let key = SymmetricKey(size: .bits256)
|
||||
let data = key.withUnsafeBytes { Data(Array($0)) }
|
||||
let encodedData = try JSONEncoder.defaultEncoder.encode(data)
|
||||
|
||||
keychainService.setSearchResultData(data)
|
||||
keychainService.setSearchResultData(encodedData)
|
||||
|
||||
let returnData: Data = try await subject.getValue(for: .authenticatorKey)
|
||||
XCTAssertEqual(returnData, data)
|
||||
@ -113,6 +114,7 @@ final class SharedKeychainStorageTests: BitwardenTestCase {
|
||||
func test_setAuthenticatorKey_success() async throws {
|
||||
let key = SymmetricKey(size: .bits256)
|
||||
let data = key.withUnsafeBytes { Data(Array($0)) }
|
||||
let encodedData = try JSONEncoder.defaultEncoder.encode(data)
|
||||
try await subject.setValue(data, for: .authenticatorKey)
|
||||
|
||||
let attributes = try XCTUnwrap(keychainService.addAttributes as? [CFString: Any])
|
||||
@ -123,6 +125,6 @@ final class SharedKeychainStorageTests: BitwardenTestCase {
|
||||
SharedKeychainItem.authenticatorKey.unformattedKey)
|
||||
try XCTAssertEqual(XCTUnwrap(attributes[kSecClass] as? String),
|
||||
String(kSecClassGenericPassword))
|
||||
try XCTAssertEqual(XCTUnwrap(attributes[kSecValueData] as? Data), data)
|
||||
try XCTAssertEqual(XCTUnwrap(attributes[kSecValueData] as? Data), encodedData)
|
||||
}
|
||||
}
|
||||
|
||||
81
AuthenticatorBridgeKit/SharedTimeoutService.swift
Normal file
81
AuthenticatorBridgeKit/SharedTimeoutService.swift
Normal file
@ -0,0 +1,81 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - HasSharedTimeoutService
|
||||
|
||||
/// Protocol for an object that provides a `SharedTimeoutService`
|
||||
///
|
||||
public protocol HasSharedTimeoutService {
|
||||
/// The service for managing account timeout between apps.
|
||||
var sharedTimeoutService: SharedTimeoutService { get }
|
||||
}
|
||||
|
||||
// MARK: - SharedTimeoutService
|
||||
|
||||
/// A service that manages account timeout between apps.
|
||||
///
|
||||
public protocol SharedTimeoutService {
|
||||
/// Clears the shared timeout for a user.
|
||||
/// - Parameters:
|
||||
/// - userId: The user's ID
|
||||
func clearTimeout(forUserId userId: String) async throws
|
||||
|
||||
/// Determines if a user has passed their timeout, using the current time and the saved shared time.
|
||||
/// If the current time is equal to the timeout time, then it is considered passed. If there is no
|
||||
/// saved time, then this will always return false.
|
||||
/// - Parameters:
|
||||
/// - userId: The user's ID
|
||||
/// - Returns: whether or not the user has passed their timeout
|
||||
func hasPassedTimeout(userId: String) async throws -> Bool
|
||||
|
||||
/// Updates the shared timeout for a user.
|
||||
/// - Parameters:
|
||||
/// - userId: The user's ID
|
||||
/// - lastActiveDate: The last time the user was active
|
||||
/// - timeoutLength: The user's preferred timeout length
|
||||
func updateTimeout(forUserId userId: String, lastActiveDate: Date?, timeoutLength: SessionTimeoutValue) async throws
|
||||
}
|
||||
|
||||
// MARK: - DefaultTimeoutService
|
||||
|
||||
public final class DefaultSharedTimeoutService: SharedTimeoutService {
|
||||
/// A repository for managing keychain items to be shared between Password Manager and Authenticator.
|
||||
private let sharedKeychainRepository: SharedKeychainRepository
|
||||
|
||||
/// A service for providing the current time.
|
||||
private let timeProvider: TimeProvider
|
||||
|
||||
public init(
|
||||
sharedKeychainRepository: SharedKeychainRepository,
|
||||
timeProvider: TimeProvider
|
||||
) {
|
||||
self.sharedKeychainRepository = sharedKeychainRepository
|
||||
self.timeProvider = timeProvider
|
||||
}
|
||||
|
||||
public func clearTimeout(forUserId userId: String) async throws {
|
||||
try await sharedKeychainRepository.setAccountAutoLogoutTime(nil, userId: userId)
|
||||
}
|
||||
|
||||
public func hasPassedTimeout(userId: String) async throws -> Bool {
|
||||
guard let autoLogoutTime = try await sharedKeychainRepository.getAccountAutoLogoutTime(userId: userId) else {
|
||||
return false
|
||||
}
|
||||
return timeProvider.presentTime >= autoLogoutTime
|
||||
}
|
||||
|
||||
public func updateTimeout(
|
||||
forUserId userId: String,
|
||||
lastActiveDate: Date?,
|
||||
timeoutLength: SessionTimeoutValue
|
||||
) async throws {
|
||||
guard let lastActiveDate else {
|
||||
try await clearTimeout(forUserId: userId)
|
||||
return
|
||||
}
|
||||
|
||||
let timeout = lastActiveDate.addingTimeInterval(TimeInterval(timeoutLength.seconds))
|
||||
|
||||
try await sharedKeychainRepository.setAccountAutoLogoutTime(timeout, userId: userId)
|
||||
}
|
||||
}
|
||||
115
AuthenticatorBridgeKit/SharedTimeoutServiceTests.swift
Normal file
115
AuthenticatorBridgeKit/SharedTimeoutServiceTests.swift
Normal file
@ -0,0 +1,115 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import AuthenticatorBridgeKitMocks
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import TestHelpers
|
||||
import XCTest
|
||||
|
||||
final class SharedTimeoutServiceTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var sharedKeychainRepository: MockSharedKeychainRepository!
|
||||
var subject: SharedTimeoutService!
|
||||
var timeProvider: MockTimeProvider!
|
||||
|
||||
// MARK: Set up & Tear down
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
sharedKeychainRepository = MockSharedKeychainRepository()
|
||||
timeProvider = MockTimeProvider(.mockTime(Date(year: 2024, month: 6, day: 20)))
|
||||
|
||||
subject = DefaultSharedTimeoutService(
|
||||
sharedKeychainRepository: sharedKeychainRepository,
|
||||
timeProvider: timeProvider
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
sharedKeychainRepository = nil
|
||||
subject = nil
|
||||
timeProvider = nil
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// `clearTimeout(:)` clears the timeout for a user.
|
||||
func test_clearTimeout() async throws {
|
||||
sharedKeychainRepository.accountAutoLogoutTime["1"] = timeProvider.presentTime
|
||||
try await subject.clearTimeout(forUserId: "1")
|
||||
XCTAssertNil(sharedKeychainRepository.accountAutoLogoutTime["1"])
|
||||
}
|
||||
|
||||
/// `clearTimeout(:)` throws errors.
|
||||
func test_clearTimeout_error() async throws {
|
||||
sharedKeychainRepository.errorToThrow = BitwardenTestError.example
|
||||
await assertAsyncThrows(error: BitwardenTestError.example) {
|
||||
try await subject.clearTimeout(forUserId: "1")
|
||||
}
|
||||
}
|
||||
|
||||
/// `hasPassedTimeout(:)` uses the current time to determine if the timeout has passed.
|
||||
/// If the current time is the timeout, then it is considered passed.
|
||||
func test_hasPassedTimeout() async throws {
|
||||
sharedKeychainRepository.accountAutoLogoutTime["1"] = timeProvider.presentTime.addingTimeInterval(-1)
|
||||
var value = try await subject.hasPassedTimeout(userId: "1")
|
||||
XCTAssertTrue(value)
|
||||
|
||||
sharedKeychainRepository.accountAutoLogoutTime["1"] = timeProvider.presentTime
|
||||
value = try await subject.hasPassedTimeout(userId: "1")
|
||||
XCTAssertTrue(value)
|
||||
|
||||
sharedKeychainRepository.accountAutoLogoutTime["1"] = timeProvider.presentTime.addingTimeInterval(1)
|
||||
value = try await subject.hasPassedTimeout(userId: "1")
|
||||
XCTAssertFalse(value)
|
||||
}
|
||||
|
||||
/// `hasPassedTimeout(:)` returns false if there is no timeout.
|
||||
func test_hasPassedTimeout_noTimeout() async throws {
|
||||
let value = try await subject.hasPassedTimeout(userId: "1")
|
||||
XCTAssertFalse(value)
|
||||
}
|
||||
|
||||
/// `hasPassedTimeout(:)` throws errors.
|
||||
func test_hasPassedTimeout_error() async throws {
|
||||
sharedKeychainRepository.errorToThrow = BitwardenTestError.example
|
||||
await assertAsyncThrows(error: BitwardenTestError.example) {
|
||||
_ = try await subject.hasPassedTimeout(userId: "1")
|
||||
}
|
||||
}
|
||||
|
||||
/// `updateTimeout(:::)` updates the timeout accordingly.
|
||||
func test_updateTimeout() async throws {
|
||||
try await subject.updateTimeout(
|
||||
forUserId: "1",
|
||||
lastActiveDate: timeProvider.presentTime,
|
||||
timeoutLength: .fourHours
|
||||
)
|
||||
XCTAssertEqual(
|
||||
sharedKeychainRepository.accountAutoLogoutTime["1"],
|
||||
timeProvider.presentTime.addingTimeInterval(TimeInterval(SessionTimeoutValue.fourHours.seconds))
|
||||
)
|
||||
}
|
||||
|
||||
/// `updateTimeout(:::)` clears the timeout if the date is nil.
|
||||
func test_updateTimeout_nil() async throws {
|
||||
sharedKeychainRepository.accountAutoLogoutTime["1"] = timeProvider.presentTime
|
||||
try await subject.updateTimeout(forUserId: "1", lastActiveDate: nil, timeoutLength: .fourHours)
|
||||
XCTAssertNil(sharedKeychainRepository.accountAutoLogoutTime["1"])
|
||||
}
|
||||
|
||||
/// `updateTimeout(:::)` throws errors.
|
||||
func test_updateTimeout_error() async throws {
|
||||
sharedKeychainRepository.errorToThrow = BitwardenTestError.example
|
||||
await assertAsyncThrows(error: BitwardenTestError.example) {
|
||||
try await self.subject.updateTimeout(
|
||||
forUserId: "1",
|
||||
lastActiveDate: timeProvider.presentTime,
|
||||
timeoutLength: .fourHours
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import AuthenticatorBridgeKitMocks
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
@ -31,7 +30,8 @@ final class AuthenticatorBridgeItemDataTests: AuthenticatorBridgeKitTestCase {
|
||||
itemService = DefaultAuthenticatorBridgeItemService(
|
||||
cryptoService: cryptoService,
|
||||
dataStore: dataStore,
|
||||
sharedKeychainRepository: MockSharedKeychainRepository()
|
||||
sharedKeychainRepository: MockSharedKeychainRepository(),
|
||||
sharedTimeoutService: MockSharedTimeoutService()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -3,9 +3,10 @@ import AuthenticatorBridgeKitMocks
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import Foundation
|
||||
import TestHelpers
|
||||
import XCTest
|
||||
|
||||
@testable import AuthenticatorBridgeKit
|
||||
// swiftlint:disable file_length type_body_length function_body_length
|
||||
|
||||
final class AuthenticatorBridgeItemServiceTests: AuthenticatorBridgeKitTestCase {
|
||||
// MARK: Properties
|
||||
@ -15,6 +16,7 @@ final class AuthenticatorBridgeItemServiceTests: AuthenticatorBridgeKitTestCase
|
||||
var dataStore: AuthenticatorBridgeDataStore!
|
||||
var errorReporter: ErrorReporter!
|
||||
var keychainRepository: MockSharedKeychainRepository!
|
||||
var sharedTimeoutService: MockSharedTimeoutService!
|
||||
var subject: AuthenticatorBridgeItemService!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
@ -29,10 +31,12 @@ final class AuthenticatorBridgeItemServiceTests: AuthenticatorBridgeKitTestCase
|
||||
storeType: .memory
|
||||
)
|
||||
keychainRepository = MockSharedKeychainRepository()
|
||||
sharedTimeoutService = MockSharedTimeoutService()
|
||||
subject = DefaultAuthenticatorBridgeItemService(
|
||||
cryptoService: cryptoService,
|
||||
dataStore: dataStore,
|
||||
sharedKeychainRepository: keychainRepository
|
||||
sharedKeychainRepository: keychainRepository,
|
||||
sharedTimeoutService: sharedTimeoutService
|
||||
)
|
||||
}
|
||||
|
||||
@ -41,6 +45,7 @@ final class AuthenticatorBridgeItemServiceTests: AuthenticatorBridgeKitTestCase
|
||||
dataStore = nil
|
||||
errorReporter = nil
|
||||
keychainRepository = nil
|
||||
sharedTimeoutService = nil
|
||||
subject = nil
|
||||
super.tearDown()
|
||||
}
|
||||
@ -340,4 +345,65 @@ final class AuthenticatorBridgeItemServiceTests: AuthenticatorBridgeKitTestCase
|
||||
XCTAssertEqual(results[0], initialItems)
|
||||
XCTAssertEqual(results[1], replacedItems)
|
||||
}
|
||||
|
||||
/// The shared items publisher deletes items if the user is timed out.
|
||||
///
|
||||
func test_sharedItemsPublisher_deletesItemsOnTimeout() async throws {
|
||||
let pastTimeoutItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
|
||||
let withinTimeoutItems = [AuthenticatorBridgeItemDataView.fixture(name: "New Item")]
|
||||
try await subject.insertItems(pastTimeoutItems, forUserId: "pastTimeoutUserId")
|
||||
try await subject.replaceAllItems(with: withinTimeoutItems, forUserId: "withinTimeoutUserId")
|
||||
|
||||
sharedTimeoutService.hasPassedTimeoutResult = .success([
|
||||
"pastTimeoutUserId": true,
|
||||
"withinTimeoutUserId": false,
|
||||
])
|
||||
|
||||
var results: [[AuthenticatorBridgeItemDataView]] = []
|
||||
let publisher = try await subject.sharedItemsPublisher()
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { value in
|
||||
results.append(value)
|
||||
}
|
||||
)
|
||||
defer { publisher.cancel() }
|
||||
|
||||
// Verify items are removed for "userId"
|
||||
let itemsForPastTimeoutUser = try await subject.fetchAllForUserId("pastTimeoutUserId")
|
||||
|
||||
XCTAssertNotNil(itemsForPastTimeoutUser)
|
||||
XCTAssertEqual(itemsForPastTimeoutUser.count, 0)
|
||||
|
||||
// Verify items are still present for "differentUserId"
|
||||
let itemsForWithinTimeoutUser = try await subject.fetchAllForUserId("withinTimeoutUserId")
|
||||
|
||||
XCTAssertNotNil(itemsForWithinTimeoutUser)
|
||||
XCTAssertEqual(itemsForWithinTimeoutUser.count, withinTimeoutItems.count)
|
||||
}
|
||||
|
||||
/// `sharedItemsPublisher()` throws if checking for logout throws
|
||||
///
|
||||
func test_sharedItemsPublisher_logoutError() async throws {
|
||||
let initialItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
|
||||
try await subject.insertItems(initialItems, forUserId: "userId")
|
||||
|
||||
sharedTimeoutService.hasPassedTimeoutResult = .failure(BitwardenTestError.example)
|
||||
|
||||
await assertAsyncThrows(error: BitwardenTestError.example) {
|
||||
var results: [[AuthenticatorBridgeItemDataView]] = []
|
||||
let publisher = try await subject.sharedItemsPublisher()
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { value in
|
||||
results.append(value)
|
||||
}
|
||||
)
|
||||
publisher.cancel()
|
||||
}
|
||||
|
||||
XCTAssertFalse(cryptoService.decryptCalled)
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:enable file_length type_body_length function_body_length
|
||||
|
||||
@ -245,10 +245,16 @@ public class ServiceContainer: Services {
|
||||
storeType: .persisted
|
||||
)
|
||||
|
||||
let sharedTimeoutService = DefaultSharedTimeoutService(
|
||||
sharedKeychainRepository: sharedKeychainRepository,
|
||||
timeProvider: timeProvider
|
||||
)
|
||||
|
||||
let sharedItemService = DefaultAuthenticatorBridgeItemService(
|
||||
cryptoService: sharedCryptographyService,
|
||||
dataStore: sharedDataStore,
|
||||
sharedKeychainRepository: sharedKeychainRepository
|
||||
sharedKeychainRepository: sharedKeychainRepository,
|
||||
sharedTimeoutService: sharedTimeoutService
|
||||
)
|
||||
|
||||
let authenticatorItemRepository = DefaultAuthenticatorItemRepository(
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import AuthenticatorBridgeKitMocks
|
||||
import BitwardenKitMocks
|
||||
import InlineSnapshotTesting
|
||||
import TestHelpers
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import Combine
|
||||
|
||||
class MockAuthenticatorBridgeItemService: AuthenticatorBridgeItemService {
|
||||
var errorToThrow: Error?
|
||||
var replaceAllCalled = false
|
||||
var sharedItemsSubject = CurrentValueSubject<[AuthenticatorBridgeItemDataView], Error>([])
|
||||
var storedItems: [String: [AuthenticatorBridgeItemDataView]] = [:]
|
||||
var syncOn = false
|
||||
var tempItem: AuthenticatorBridgeItemDataView?
|
||||
|
||||
func deleteAllForUserId(_ userId: String) async throws {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
storedItems[userId] = []
|
||||
}
|
||||
|
||||
func fetchAllForUserId(_ userId: String) async throws -> [AuthenticatorBridgeItemDataView] {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
return storedItems[userId] ?? []
|
||||
}
|
||||
|
||||
func fetchTemporaryItem() async throws -> AuthenticatorBridgeItemDataView? {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
return tempItem
|
||||
}
|
||||
|
||||
func insertTemporaryItem(_ item: AuthenticatorBridgeItemDataView) async throws {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
tempItem = item
|
||||
}
|
||||
|
||||
func insertItems(_ items: [AuthenticatorBridgeItemDataView], forUserId userId: String) async throws {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
storedItems[userId] = items
|
||||
}
|
||||
|
||||
func isSyncOn() async -> Bool {
|
||||
syncOn
|
||||
}
|
||||
|
||||
func replaceAllItems(with items: [AuthenticatorBridgeItemDataView], forUserId userId: String) async throws {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
storedItems[userId] = items
|
||||
replaceAllCalled = true
|
||||
}
|
||||
|
||||
func sharedItemsPublisher() async throws ->
|
||||
AnyPublisher<[AuthenticatorBridgeKit.AuthenticatorBridgeItemDataView], any Error> {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
|
||||
return sharedItemsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
@ -723,7 +723,7 @@ extension DefaultAuthRepository: AuthRepository {
|
||||
}
|
||||
|
||||
func isPinUnlockAvailable(userId: String?) async throws -> Bool {
|
||||
try await stateService.pinProtectedUserKey(userId: userId) != nil
|
||||
try await vaultTimeoutService.isPinUnlockAvailable(userId: userId)
|
||||
}
|
||||
|
||||
func isUserManagedByOrganization() async throws -> Bool {
|
||||
@ -784,20 +784,7 @@ extension DefaultAuthRepository: AuthRepository {
|
||||
}
|
||||
|
||||
func sessionTimeoutAction(userId: String?) async throws -> SessionTimeoutAction {
|
||||
let hasMasterPassword = try await stateService.getUserHasMasterPassword(userId: userId)
|
||||
let timeoutAction = try await stateService.getTimeoutAction(userId: userId)
|
||||
guard hasMasterPassword else {
|
||||
let isBiometricsEnabled = try await biometricsRepository.getBiometricUnlockStatus().isEnabled
|
||||
let isPinEnabled = try await isPinUnlockAvailable()
|
||||
if isPinEnabled || isBiometricsEnabled {
|
||||
return timeoutAction
|
||||
} else {
|
||||
// If the user doesn't have a master password and hasn't enabled a pin or
|
||||
// biometrics, their timeout action needs to be logout.
|
||||
return .logout
|
||||
}
|
||||
}
|
||||
return timeoutAction
|
||||
try await vaultTimeoutService.sessionTimeoutAction(userId: userId)
|
||||
}
|
||||
|
||||
func requestOtp() async throws {
|
||||
|
||||
@ -150,7 +150,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// `.canBeLocked(userId:)` shoulr reutrn true when user has face ID.
|
||||
/// `.canBeLocked(userId:)` should return true when user has face ID.
|
||||
func test_canBeLocked_hasFaceId() async {
|
||||
stateService.userHasMasterPassword["1"] = false
|
||||
stateService.pinProtectedUserKeyValue["1"] = "123"
|
||||
@ -460,12 +460,13 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
|
||||
stateService.activeAccount = beeAccount
|
||||
stateService.timeoutAction = [anneAccount.profile.userId: .logout]
|
||||
vaultTimeoutService.shouldSessionTimeout[anneAccount.profile.userId] = true
|
||||
vaultTimeoutService.sessionTimeoutAction[anneAccount.profile.userId] = .logout
|
||||
await subject.checkSessionTimeouts(handleActiveUser: nil)
|
||||
XCTAssertTrue(vaultTimeoutService.removedIds.contains(anneAccount.profile.userId))
|
||||
XCTAssertTrue(stateService.accountsLoggedOut.contains(anneAccount.profile.userId))
|
||||
}
|
||||
|
||||
/// `checkSessionTimeout()` takes no action to an active account when the session timeout if the `handleActiveUser`
|
||||
/// `checkSessionTimeout()` takes no action to an active account when the session timeout if the `handleActiveUser`
|
||||
/// closure is nil.
|
||||
func test_checkSessionTimeout_activeAccount() async {
|
||||
stateService.accounts = [anneAccount, beeAccount]
|
||||
@ -624,6 +625,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
|
||||
anneAccount.profile.userId: true,
|
||||
beeAccount.profile.userId: true,
|
||||
]
|
||||
|
||||
vaultTimeoutService.isClientLocked = [
|
||||
anneAccount.profile.userId: true,
|
||||
beeAccount.profile.userId: true,
|
||||
@ -633,9 +635,9 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
|
||||
anneAccount.profile.userId: false,
|
||||
beeAccount.profile.userId: false,
|
||||
]
|
||||
stateService.pinProtectedUserKeyValue = [
|
||||
beeAccount.profile.userId: "123",
|
||||
]
|
||||
|
||||
vaultTimeoutService.pinUnlockAvailabilityResult = .success([beeAccount.profile.userId: true])
|
||||
|
||||
let accounts = await subject.getProfilesState(
|
||||
allowLockAndLogout: true,
|
||||
isVisible: true,
|
||||
@ -1020,22 +1022,26 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
|
||||
}
|
||||
}
|
||||
|
||||
/// `isPinUnlockAvailable` returns the pin unlock availability for the active user.
|
||||
func test_isPinUnlockAvailable_noValue() async throws {
|
||||
stateService.activeAccount = .fixture()
|
||||
let value = try await subject.isPinUnlockAvailable()
|
||||
/// `isPinUnlockAvailable` calls the VaultTimeoutService.
|
||||
func test_isPinUnlockAvailable() async throws {
|
||||
stateService.activeAccount = .fixture(profile: .fixture(userId: "1"))
|
||||
vaultTimeoutService.pinUnlockAvailabilityResult = .success(["1": false])
|
||||
var value = try await subject.isPinUnlockAvailable(userId: "1")
|
||||
XCTAssertFalse(value)
|
||||
|
||||
vaultTimeoutService.pinUnlockAvailabilityResult = .success(["1": true])
|
||||
value = try await subject.isPinUnlockAvailable(userId: "1")
|
||||
XCTAssertTrue(value)
|
||||
}
|
||||
|
||||
/// `isPinUnlockAvailable` returns the pin unlock availability for the active user.
|
||||
func test_isPinUnlockAvailable_value() async throws {
|
||||
let active = Account.fixture()
|
||||
stateService.activeAccount = active
|
||||
stateService.pinProtectedUserKeyValue = [
|
||||
active.profile.userId: "123",
|
||||
]
|
||||
let value = try await subject.isPinUnlockAvailable()
|
||||
XCTAssertTrue(value)
|
||||
/// `isPinUnlockAvailable` throws errors.
|
||||
func test_isPinUnlockAvailable_error() async throws {
|
||||
stateService.activeAccount = .fixture(profile: .fixture(userId: "1"))
|
||||
vaultTimeoutService.pinUnlockAvailabilityResult = .failure(BitwardenTestError.example)
|
||||
|
||||
await assertAsyncThrows(error: BitwardenTestError.example) {
|
||||
_ = try await subject.isPinUnlockAvailable(userId: "1")
|
||||
}
|
||||
}
|
||||
|
||||
/// `isUserManagedByOrganization` returns false when the feature flag is off.
|
||||
@ -1583,12 +1589,12 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
|
||||
XCTAssertNil(clientService.mockAuthUserId)
|
||||
}
|
||||
|
||||
/// `sessionTimeoutAction()` returns the session timeout action for a user.
|
||||
/// `sessionTimeoutAction()` uses the VaultTimeoutService.
|
||||
func test_sessionTimeoutAction() async throws {
|
||||
stateService.activeAccount = .fixture(profile: .fixture(userId: "1"))
|
||||
stateService.accounts = [.fixture(profile: .fixture(userId: "2"))]
|
||||
stateService.timeoutAction["1"] = .lock
|
||||
stateService.timeoutAction["2"] = .logout
|
||||
vaultTimeoutService.sessionTimeoutAction["1"] = .lock
|
||||
vaultTimeoutService.sessionTimeoutAction["2"] = .logout
|
||||
|
||||
var timeoutAction = try await subject.sessionTimeoutAction()
|
||||
XCTAssertEqual(timeoutAction, .lock)
|
||||
@ -1597,49 +1603,13 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
|
||||
XCTAssertEqual(timeoutAction, .logout)
|
||||
}
|
||||
|
||||
/// `sessionTimeoutAction()` defaults to logout if the user doesn't have a master password and
|
||||
/// hasn't enabled pin or biometrics unlock.
|
||||
func test_sessionTimeoutAction_noMasterPassword() async throws {
|
||||
stateService.activeAccount = .fixture(profile: .fixture(userId: "1"))
|
||||
stateService.timeoutAction["1"] = .lock
|
||||
stateService.userHasMasterPassword["1"] = false
|
||||
/// `sessionTimeoutAction()` throws errors.
|
||||
func test_sessionTimeoutAction_error() async throws {
|
||||
vaultTimeoutService.sessionTimeoutActionError = BitwardenTestError.example
|
||||
|
||||
let timeoutAction = try await subject.sessionTimeoutAction()
|
||||
XCTAssertEqual(timeoutAction, .logout)
|
||||
}
|
||||
|
||||
/// `sessionTimeoutAction()` allows lock or logout if the user doesn't have a master password
|
||||
/// and has biometrics unlock enabled.
|
||||
func test_sessionTimeoutAction_noMasterPassword_biometricsEnabled() async throws {
|
||||
stateService.activeAccount = .fixture(profile: .fixture(userId: "1"))
|
||||
stateService.timeoutAction["1"] = .lock
|
||||
stateService.userHasMasterPassword["1"] = false
|
||||
biometricsRepository.biometricUnlockStatus = .success(
|
||||
.available(.faceID, enabled: true)
|
||||
)
|
||||
|
||||
var timeoutAction = try await subject.sessionTimeoutAction()
|
||||
XCTAssertEqual(timeoutAction, .lock)
|
||||
|
||||
stateService.timeoutAction["1"] = .logout
|
||||
timeoutAction = try await subject.sessionTimeoutAction()
|
||||
XCTAssertEqual(timeoutAction, .logout)
|
||||
}
|
||||
|
||||
/// `sessionTimeoutAction()` allows lock or logout if the user doesn't have a master password
|
||||
/// and has pin unlock enabled.
|
||||
func test_sessionTimeoutAction_noMasterPassword_pinEnabled() async throws {
|
||||
stateService.activeAccount = .fixture(profile: .fixture(userId: "1"))
|
||||
stateService.pinProtectedUserKeyValue["1"] = "KEY"
|
||||
stateService.timeoutAction["1"] = .lock
|
||||
stateService.userHasMasterPassword["1"] = false
|
||||
|
||||
var timeoutAction = try await subject.sessionTimeoutAction()
|
||||
XCTAssertEqual(timeoutAction, .lock)
|
||||
|
||||
stateService.timeoutAction["1"] = .logout
|
||||
timeoutAction = try await subject.sessionTimeoutAction()
|
||||
XCTAssertEqual(timeoutAction, .logout)
|
||||
await assertAsyncThrows(error: BitwardenTestError.example) {
|
||||
_ = try await subject.sessionTimeoutAction(userId: "1")
|
||||
}
|
||||
}
|
||||
|
||||
/// `setActiveAccount(userId: )` loads the environment URLs for the active account.
|
||||
|
||||
@ -148,6 +148,9 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
|
||||
/// The repository used by the application to manage data for the UI layer.
|
||||
let settingsRepository: SettingsRepository
|
||||
|
||||
/// The service that manages account timeout between apps.
|
||||
public let sharedTimeoutService: SharedTimeoutService
|
||||
|
||||
/// The service used by the application to manage account state.
|
||||
let stateService: StateService
|
||||
|
||||
@ -242,6 +245,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
|
||||
/// - policyService: The service for managing the polices for the user.
|
||||
/// - sendRepository: The repository used by the application to manage send data for the UI layer.
|
||||
/// - settingsRepository: The repository used by the application to manage data for the UI layer.
|
||||
/// - sharedTimeoutService: The service that manages account timeout between apps.
|
||||
/// - stateService: The service used by the application to manage account state.
|
||||
/// - syncService: The service used to handle syncing vault data with the API.
|
||||
/// - systemDevice: The object used by the application to retrieve information about this device.
|
||||
@ -299,6 +303,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
|
||||
reviewPromptService: ReviewPromptService,
|
||||
sendRepository: SendRepository,
|
||||
settingsRepository: SettingsRepository,
|
||||
sharedTimeoutService: SharedTimeoutService,
|
||||
stateService: StateService,
|
||||
syncService: SyncService,
|
||||
systemDevice: SystemDevice,
|
||||
@ -355,6 +360,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
|
||||
self.reviewPromptService = reviewPromptService
|
||||
self.sendRepository = sendRepository
|
||||
self.settingsRepository = settingsRepository
|
||||
self.sharedTimeoutService = sharedTimeoutService
|
||||
self.stateService = stateService
|
||||
self.syncService = syncService
|
||||
self.systemDevice = systemDevice
|
||||
@ -538,9 +544,25 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
|
||||
tokenService: tokenService
|
||||
)
|
||||
|
||||
let sharedKeychainStorage = DefaultSharedKeychainStorage(
|
||||
keychainService: keychainService,
|
||||
sharedAppGroupIdentifier: Bundle.main.sharedAppGroupIdentifier
|
||||
)
|
||||
|
||||
let sharedKeychainRepository = DefaultSharedKeychainRepository(
|
||||
storage: sharedKeychainStorage
|
||||
)
|
||||
|
||||
let sharedTimeoutService = DefaultSharedTimeoutService(
|
||||
sharedKeychainRepository: sharedKeychainRepository,
|
||||
timeProvider: timeProvider
|
||||
)
|
||||
|
||||
let vaultTimeoutService = DefaultVaultTimeoutService(
|
||||
biometricsRepository: biometricsRepository,
|
||||
clientService: clientService,
|
||||
errorReporter: errorReporter,
|
||||
sharedTimeoutService: sharedTimeoutService,
|
||||
stateService: stateService,
|
||||
timeProvider: timeProvider
|
||||
)
|
||||
@ -777,15 +799,6 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
|
||||
storeType: .persisted
|
||||
)
|
||||
|
||||
let sharedKeychainStorage = DefaultSharedKeychainStorage(
|
||||
keychainService: keychainService,
|
||||
sharedAppGroupIdentifier: Bundle.main.sharedAppGroupIdentifier
|
||||
)
|
||||
|
||||
let sharedKeychainRepository = DefaultSharedKeychainRepository(
|
||||
storage: sharedKeychainStorage
|
||||
)
|
||||
|
||||
let sharedCryptographyService = DefaultAuthenticatorCryptographyService(
|
||||
sharedKeychainRepository: sharedKeychainRepository
|
||||
)
|
||||
@ -793,7 +806,8 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
|
||||
let authBridgeItemService = DefaultAuthenticatorBridgeItemService(
|
||||
cryptoService: sharedCryptographyService,
|
||||
dataStore: authenticatorDataStore,
|
||||
sharedKeychainRepository: sharedKeychainRepository
|
||||
sharedKeychainRepository: sharedKeychainRepository,
|
||||
sharedTimeoutService: sharedTimeoutService
|
||||
)
|
||||
|
||||
let authenticatorSyncService = DefaultAuthenticatorSyncService(
|
||||
@ -852,6 +866,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
|
||||
reviewPromptService: reviewPromptService,
|
||||
sendRepository: sendRepository,
|
||||
settingsRepository: settingsRepository,
|
||||
sharedTimeoutService: sharedTimeoutService,
|
||||
stateService: stateService,
|
||||
syncService: syncService,
|
||||
systemDevice: UIDevice.current,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
|
||||
@ -45,6 +46,7 @@ typealias Services = HasAPIService
|
||||
& HasReviewPromptService
|
||||
& HasSendRepository
|
||||
& HasSettingsRepository
|
||||
& HasSharedTimeoutService
|
||||
& HasStateService
|
||||
& HasSyncService
|
||||
& HasSystemDevice
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import BitwardenShared
|
||||
import Combine
|
||||
|
||||
class MockAuthenticatorBridgeItemService: AuthenticatorBridgeItemService {
|
||||
var errorToThrow: Error?
|
||||
var replaceAllCalled = false
|
||||
var sharedItemsSubject = CurrentValueSubject<[AuthenticatorBridgeItemDataView], Error>([])
|
||||
var storedItems: [String: [AuthenticatorBridgeItemDataView]] = [:]
|
||||
var syncOn = false
|
||||
var tempItem: AuthenticatorBridgeItemDataView?
|
||||
|
||||
func deleteAllForUserId(_ userId: String) async throws {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
storedItems[userId] = []
|
||||
}
|
||||
|
||||
func fetchAllForUserId(_ userId: String) async throws -> [AuthenticatorBridgeItemDataView] {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
return storedItems[userId] ?? []
|
||||
}
|
||||
|
||||
func fetchTemporaryItem() async throws -> AuthenticatorBridgeItemDataView? {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
return tempItem
|
||||
}
|
||||
|
||||
func insertTemporaryItem(_ item: AuthenticatorBridgeItemDataView) async throws {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
tempItem = item
|
||||
}
|
||||
|
||||
func insertItems(_ items: [AuthenticatorBridgeItemDataView], forUserId userId: String) async throws {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
storedItems[userId] = items
|
||||
}
|
||||
|
||||
func isSyncOn() async -> Bool {
|
||||
syncOn
|
||||
}
|
||||
|
||||
func replaceAllItems(with items: [AuthenticatorBridgeItemDataView], forUserId userId: String) async throws {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
storedItems[userId] = items
|
||||
replaceAllCalled = true
|
||||
}
|
||||
|
||||
func sharedItemsPublisher() async throws ->
|
||||
AnyPublisher<[AuthenticatorBridgeKit.AuthenticatorBridgeItemDataView], any Error> {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
|
||||
return sharedItemsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
@ -67,6 +67,7 @@ class MockStateService: StateService { // swiftlint:disable:this type_body_lengt
|
||||
var passwordGenerationOptions = [String: PasswordGenerationOptions]()
|
||||
var pendingAppIntentActions: [PendingAppIntentAction]?
|
||||
var pendingAppIntentActionsSubject = CurrentValueSubject<[PendingAppIntentAction]?, Never>(nil)
|
||||
var pinProtectedUserKeyError: Error?
|
||||
var pinProtectedUserKeyValue = [String: String]()
|
||||
var preAuthEnvironmentURLs: EnvironmentURLData?
|
||||
var accountCreationEnvironmentURLs = [String: EnvironmentURLData]()
|
||||
@ -87,6 +88,8 @@ class MockStateService: StateService { // swiftlint:disable:this type_body_lengt
|
||||
var setAppRehydrationStateError: Error?
|
||||
var setBiometricAuthenticationEnabledResult: Result<Void, Error> = .success(())
|
||||
var setBiometricIntegrityStateError: Error?
|
||||
var setLastActiveTimeError: Error?
|
||||
var setVaultTimeoutError: Error?
|
||||
var settingsBadgeSubject = CurrentValueSubject<SettingsBadgeState, Never>(.fixture())
|
||||
var shouldTrustDevice = [String: Bool?]()
|
||||
var syncToAuthenticatorByUserId = [String: Bool]()
|
||||
@ -97,6 +100,7 @@ class MockStateService: StateService { // swiftlint:disable:this type_body_lengt
|
||||
var updateProfileResponse: ProfileResponseModel?
|
||||
var updateProfileUserId: String?
|
||||
var userHasMasterPassword = [String: Bool]()
|
||||
var userHasMasterPasswordError: Error?
|
||||
var userIds = [String]()
|
||||
var usernameGenerationOptions = [String: UsernameGenerationOptions]()
|
||||
var usesKeyConnector = [String: Bool]()
|
||||
@ -381,6 +385,7 @@ class MockStateService: StateService { // swiftlint:disable:this type_body_lengt
|
||||
}
|
||||
|
||||
func getUserHasMasterPassword(userId: String?) async throws -> Bool {
|
||||
if let userHasMasterPasswordError { throw userHasMasterPasswordError }
|
||||
let userId = try unwrapUserId(userId)
|
||||
return userHasMasterPassword[userId] ?? true
|
||||
}
|
||||
@ -417,6 +422,7 @@ class MockStateService: StateService { // swiftlint:disable:this type_body_lengt
|
||||
}
|
||||
|
||||
func pinProtectedUserKey(userId: String?) async throws -> String? {
|
||||
if let pinProtectedUserKeyError { throw pinProtectedUserKeyError }
|
||||
let userId = try unwrapUserId(userId)
|
||||
return pinProtectedUserKeyValue[userId] ?? nil
|
||||
}
|
||||
@ -563,8 +569,9 @@ class MockStateService: StateService { // swiftlint:disable:this type_body_lengt
|
||||
}
|
||||
|
||||
func setLastActiveTime(_ date: Date?, userId: String?) async throws {
|
||||
if let setLastActiveTimeError { throw setLastActiveTimeError }
|
||||
let userId = try unwrapUserId(userId)
|
||||
lastActiveTime[userId] = timeProvider.presentTime
|
||||
lastActiveTime[userId] = date
|
||||
}
|
||||
|
||||
func setLastSyncTime(_ date: Date?, userId: String?) async throws {
|
||||
@ -711,6 +718,7 @@ class MockStateService: StateService { // swiftlint:disable:this type_body_lengt
|
||||
}
|
||||
|
||||
func setVaultTimeout(value: SessionTimeoutValue, userId: String?) async throws {
|
||||
if let setVaultTimeoutError { throw setVaultTimeoutError }
|
||||
let userId = try unwrapUserId(userId)
|
||||
vaultTimeout[userId] = value
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import AuthenticatorBridgeKitMocks
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import BitwardenSdk
|
||||
@ -49,6 +51,7 @@ extension ServiceContainer {
|
||||
reviewPromptService: ReviewPromptService = MockReviewPromptService(),
|
||||
sendRepository: SendRepository = MockSendRepository(),
|
||||
settingsRepository: SettingsRepository = MockSettingsRepository(),
|
||||
sharedTimeoutService: SharedTimeoutService = MockSharedTimeoutService(),
|
||||
stateService: StateService = MockStateService(),
|
||||
syncService: SyncService = MockSyncService(),
|
||||
systemDevice: SystemDevice = MockSystemDevice(),
|
||||
@ -109,6 +112,7 @@ extension ServiceContainer {
|
||||
reviewPromptService: reviewPromptService,
|
||||
sendRepository: sendRepository,
|
||||
settingsRepository: settingsRepository,
|
||||
sharedTimeoutService: sharedTimeoutService,
|
||||
stateService: stateService,
|
||||
syncService: syncService,
|
||||
systemDevice: systemDevice,
|
||||
|
||||
@ -8,10 +8,13 @@ import Foundation
|
||||
class MockVaultTimeoutService: VaultTimeoutService {
|
||||
var account: Account = .fixture()
|
||||
var lastActiveTime = [String: Date]()
|
||||
var pinUnlockAvailabilityResult: Result<[String: Bool], Error> = .success([:])
|
||||
var setLastActiveTimeError: Error?
|
||||
var shouldSessionTimeout = [String: Bool]()
|
||||
var shouldSessionTimeoutError: Error?
|
||||
var timeProvider = MockTimeProvider(.currentTime)
|
||||
var sessionTimeoutAction = [String: SessionTimeoutAction]()
|
||||
var sessionTimeoutActionError: Error?
|
||||
var sessionTimeoutValueError: Error?
|
||||
var unlockVaultHadUserInteraction = false
|
||||
var vaultTimeout = [String: SessionTimeoutValue]()
|
||||
@ -45,6 +48,12 @@ class MockVaultTimeoutService: VaultTimeoutService {
|
||||
vaultTimeout[account.profile.userId] = value
|
||||
}
|
||||
|
||||
func isPinUnlockAvailable(userId: String?) async throws -> Bool {
|
||||
guard let userId else { throw StateServiceError.noActiveAccount }
|
||||
|
||||
return try pinUnlockAvailabilityResult.get()[userId] ?? false
|
||||
}
|
||||
|
||||
func hasPassedSessionTimeout(userId: String) async throws -> Bool {
|
||||
if let shouldSessionTimeoutError {
|
||||
throw shouldSessionTimeoutError
|
||||
@ -52,16 +61,17 @@ class MockVaultTimeoutService: VaultTimeoutService {
|
||||
return shouldSessionTimeout[userId] ?? false
|
||||
}
|
||||
|
||||
func unlockVault(userId: String?, hadUserInteraction: Bool) async throws {
|
||||
guard let userId else { return }
|
||||
isClientLocked[userId] = false
|
||||
unlockVaultHadUserInteraction = hadUserInteraction
|
||||
}
|
||||
|
||||
func remove(userId: String?) async {
|
||||
removedIds.append(userId)
|
||||
}
|
||||
|
||||
func sessionTimeoutAction(userId: String?) async throws -> SessionTimeoutAction {
|
||||
if let sessionTimeoutActionError {
|
||||
throw sessionTimeoutActionError
|
||||
}
|
||||
return sessionTimeoutAction[userId ?? account.profile.userId] ?? .lock
|
||||
}
|
||||
|
||||
func sessionTimeoutValue(userId: String?) async throws -> SessionTimeoutValue {
|
||||
if let sessionTimeoutValueError {
|
||||
throw sessionTimeoutValueError
|
||||
@ -69,6 +79,12 @@ class MockVaultTimeoutService: VaultTimeoutService {
|
||||
return vaultTimeout[userId ?? account.profile.userId] ?? .fifteenMinutes
|
||||
}
|
||||
|
||||
func unlockVault(userId: String?, hadUserInteraction: Bool) async throws {
|
||||
guard let userId else { return }
|
||||
isClientLocked[userId] = false
|
||||
unlockVaultHadUserInteraction = hadUserInteraction
|
||||
}
|
||||
|
||||
func vaultLockStatusPublisher() async -> AnyPublisher<VaultLockStatus?, Never> {
|
||||
vaultLockStatusSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
import Combine
|
||||
@ -32,6 +33,12 @@ protocol VaultTimeoutService: AnyObject {
|
||||
///
|
||||
func isLocked(userId: String) -> Bool
|
||||
|
||||
/// Whether pin unlock is available for a userId.
|
||||
/// - Parameter userId: The userId of the account.
|
||||
/// - Returns: Whether pin unlock is available.
|
||||
///
|
||||
func isPinUnlockAvailable(userId: String?) async throws -> Bool
|
||||
|
||||
/// Locks the user's vault
|
||||
///
|
||||
/// - Parameter userId: The userId of the account to lock.
|
||||
@ -45,6 +52,19 @@ protocol VaultTimeoutService: AnyObject {
|
||||
///
|
||||
func remove(userId: String?) async
|
||||
|
||||
/// Gets the `SessionTimeoutAction` for a user.
|
||||
///
|
||||
/// - Parameter userId: The userId of the account. Defaults to the active user if nil.
|
||||
///
|
||||
func sessionTimeoutAction(userId: String?) async throws -> SessionTimeoutAction
|
||||
|
||||
/// Gets the `SessionTimeoutValue` for a user.
|
||||
///
|
||||
/// - Parameter userId: The userId of the account.
|
||||
/// Defaults to the active user if nil.
|
||||
///
|
||||
func sessionTimeoutValue(userId: String?) async throws -> SessionTimeoutValue
|
||||
|
||||
/// Sets the last active time within the app.
|
||||
///
|
||||
/// - Parameter userId: The user ID associated with the last active time within the app.
|
||||
@ -68,13 +88,6 @@ protocol VaultTimeoutService: AnyObject {
|
||||
/// or the never lock key was used.
|
||||
func unlockVault(userId: String?, hadUserInteraction: Bool) async throws
|
||||
|
||||
/// Gets the `SessionTimeoutValue` for a user.
|
||||
///
|
||||
/// - Parameter userId: The userId of the account.
|
||||
/// Defaults to the active user if nil.
|
||||
///
|
||||
func sessionTimeoutValue(userId: String?) async throws -> SessionTimeoutValue
|
||||
|
||||
/// A publisher containing the active user ID and whether their vault is locked.
|
||||
///
|
||||
/// - Returns: A publisher for the active user ID whether their vault is locked.
|
||||
@ -87,12 +100,18 @@ protocol VaultTimeoutService: AnyObject {
|
||||
class DefaultVaultTimeoutService: VaultTimeoutService {
|
||||
// MARK: Private properties
|
||||
|
||||
/// The service to use system biometrics for vault unlock.
|
||||
private let biometricsRepository: BiometricsRepository
|
||||
|
||||
/// The service that handles common client functionality such as encryption and decryption.
|
||||
private let clientService: ClientService
|
||||
|
||||
/// The service used by the application to report non-fatal errors.
|
||||
private let errorReporter: ErrorReporter
|
||||
|
||||
/// A service that manages account timeout between apps.
|
||||
private let sharedTimeoutService: SharedTimeoutService
|
||||
|
||||
/// The state service used by this Default Service.
|
||||
private let stateService: StateService
|
||||
|
||||
@ -107,19 +126,25 @@ class DefaultVaultTimeoutService: VaultTimeoutService {
|
||||
/// Creates a new `DefaultVaultTimeoutService`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - biometricsRepository: The service to use system biometrics for vault unlock.
|
||||
/// - clientService: The service that handles common client functionality such as encryption and decryption.
|
||||
/// - errorReporter: The service used by the application to report non-fatal errors.
|
||||
/// - sharedTimeoutService: The service that manages account timeout between apps.
|
||||
/// - stateService: The StateService used by DefaultVaultTimeoutService.
|
||||
/// - timeProvider: Provides the current time.
|
||||
///
|
||||
init(
|
||||
biometricsRepository: BiometricsRepository,
|
||||
clientService: ClientService,
|
||||
errorReporter: ErrorReporter,
|
||||
sharedTimeoutService: SharedTimeoutService,
|
||||
stateService: StateService,
|
||||
timeProvider: TimeProvider
|
||||
) {
|
||||
self.biometricsRepository = biometricsRepository
|
||||
self.clientService = clientService
|
||||
self.errorReporter = errorReporter
|
||||
self.sharedTimeoutService = sharedTimeoutService
|
||||
self.stateService = stateService
|
||||
self.timeProvider = timeProvider
|
||||
}
|
||||
@ -149,6 +174,10 @@ class DefaultVaultTimeoutService: VaultTimeoutService {
|
||||
return isLocked
|
||||
}
|
||||
|
||||
func isPinUnlockAvailable(userId: String?) async throws -> Bool {
|
||||
try await stateService.pinProtectedUserKey(userId: userId) != nil
|
||||
}
|
||||
|
||||
func lockVault(userId: String?) async {
|
||||
do {
|
||||
let userId = try await stateService.getAccountIdOrActiveId(userId: userId)
|
||||
@ -171,12 +200,43 @@ class DefaultVaultTimeoutService: VaultTimeoutService {
|
||||
}
|
||||
}
|
||||
|
||||
func sessionTimeoutAction(userId: String?) async throws -> SessionTimeoutAction {
|
||||
let hasMasterPassword = try await stateService.getUserHasMasterPassword(userId: userId)
|
||||
let timeoutAction = try await stateService.getTimeoutAction(userId: userId)
|
||||
guard hasMasterPassword else {
|
||||
let isBiometricsEnabled = try await biometricsRepository.getBiometricUnlockStatus().isEnabled
|
||||
let isPinEnabled = try await isPinUnlockAvailable(userId: userId)
|
||||
if isPinEnabled || isBiometricsEnabled {
|
||||
return timeoutAction
|
||||
} else {
|
||||
// If the user doesn't have a master password and hasn't enabled a pin or
|
||||
// biometrics, their timeout action needs to be logout.
|
||||
return .logout
|
||||
}
|
||||
}
|
||||
return timeoutAction
|
||||
}
|
||||
|
||||
func setLastActiveTime(userId: String) async throws {
|
||||
try await stateService.setLastActiveTime(timeProvider.presentTime, userId: userId)
|
||||
let now = timeProvider.presentTime
|
||||
try await stateService.setLastActiveTime(now, userId: userId)
|
||||
let vaultTimeout = try await sessionTimeoutValue(userId: userId)
|
||||
try await updateSharedTimeout(
|
||||
lastActiveTime: now,
|
||||
timeoutValue: vaultTimeout,
|
||||
userId: userId
|
||||
)
|
||||
}
|
||||
|
||||
func setVaultTimeout(value: SessionTimeoutValue, userId: String?) async throws {
|
||||
try await stateService.setVaultTimeout(value: value, userId: userId)
|
||||
guard let userId else { return }
|
||||
let lastActiveTime = try await stateService.getLastActiveTime(userId: userId)
|
||||
try await updateSharedTimeout(
|
||||
lastActiveTime: lastActiveTime,
|
||||
timeoutValue: value,
|
||||
userId: userId
|
||||
)
|
||||
}
|
||||
|
||||
func unlockVault(userId: String?, hadUserInteraction: Bool) async throws {
|
||||
@ -202,4 +262,34 @@ class DefaultVaultTimeoutService: VaultTimeoutService {
|
||||
.removeDuplicates()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
/// Updates the shared timeout value in the SharedTimeoutService, so that BWA can log users out
|
||||
/// on timeout. In the event that the user should not be automatically logged out after a time,
|
||||
/// it will clear the timeout value.
|
||||
private func updateSharedTimeout(
|
||||
lastActiveTime: Date?,
|
||||
timeoutValue: SessionTimeoutValue,
|
||||
userId: String
|
||||
) async throws {
|
||||
let vaultTimeout = try await sessionTimeoutValue(userId: userId)
|
||||
switch vaultTimeout {
|
||||
case .never,
|
||||
.onAppRestart:
|
||||
// For timeouts of `.never` or `.onAppRestart`, timeouts cannot be calculated.
|
||||
// Therefore we can't have one saved.
|
||||
try await sharedTimeoutService.clearTimeout(forUserId: userId)
|
||||
default:
|
||||
let timeoutAction = try await sessionTimeoutAction(userId: userId)
|
||||
switch timeoutAction {
|
||||
case .lock:
|
||||
try await sharedTimeoutService.clearTimeout(forUserId: userId)
|
||||
case .logout:
|
||||
try await sharedTimeoutService.updateTimeout(
|
||||
forUserId: userId,
|
||||
lastActiveDate: lastActiveTime,
|
||||
timeoutLength: timeoutValue
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import AuthenticatorBridgeKitMocks
|
||||
import BitwardenKitMocks
|
||||
import BitwardenSdk
|
||||
import Combine
|
||||
@ -7,12 +9,14 @@ import XCTest
|
||||
@testable import BitwardenShared
|
||||
|
||||
@MainActor
|
||||
final class VaultTimeoutServiceTests: BitwardenTestCase {
|
||||
final class VaultTimeoutServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body_length
|
||||
// MARK: Properties
|
||||
|
||||
var biometricsRepository: MockBiometricsRepository!
|
||||
var cancellables: Set<AnyCancellable>!
|
||||
var clientService: MockClientService!
|
||||
var errorReporter: MockErrorReporter!
|
||||
var sharedTimeoutService: MockSharedTimeoutService!
|
||||
var stateService: MockStateService!
|
||||
var subject: DefaultVaultTimeoutService!
|
||||
var timeProvider: MockTimeProvider!
|
||||
@ -22,9 +26,11 @@ final class VaultTimeoutServiceTests: BitwardenTestCase {
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
biometricsRepository = MockBiometricsRepository()
|
||||
cancellables = []
|
||||
clientService = MockClientService()
|
||||
errorReporter = MockErrorReporter()
|
||||
sharedTimeoutService = MockSharedTimeoutService()
|
||||
stateService = MockStateService()
|
||||
timeProvider = MockTimeProvider(
|
||||
.mockTime(
|
||||
@ -32,8 +38,10 @@ final class VaultTimeoutServiceTests: BitwardenTestCase {
|
||||
)
|
||||
)
|
||||
subject = DefaultVaultTimeoutService(
|
||||
biometricsRepository: biometricsRepository,
|
||||
clientService: clientService,
|
||||
errorReporter: errorReporter,
|
||||
sharedTimeoutService: sharedTimeoutService,
|
||||
stateService: stateService,
|
||||
timeProvider: timeProvider
|
||||
)
|
||||
@ -42,6 +50,7 @@ final class VaultTimeoutServiceTests: BitwardenTestCase {
|
||||
override func tearDown() async throws {
|
||||
try await super.tearDown()
|
||||
|
||||
biometricsRepository = nil
|
||||
cancellables = nil
|
||||
clientService = nil
|
||||
errorReporter = nil
|
||||
@ -150,6 +159,32 @@ final class VaultTimeoutServiceTests: BitwardenTestCase {
|
||||
XCTAssertFalse(shouldTimeout)
|
||||
}
|
||||
|
||||
/// `isPinUnlockAvailable` returns the pin unlock availability for the active user.
|
||||
func test_isPinUnlockAvailable_noValue() async throws {
|
||||
stateService.activeAccount = .fixture()
|
||||
let value = try await subject.isPinUnlockAvailable(userId: "1")
|
||||
XCTAssertFalse(value)
|
||||
}
|
||||
|
||||
/// `isPinUnlockAvailable` returns the pin unlock availability for the active user.
|
||||
func test_isPinUnlockAvailable_value() async throws {
|
||||
let active = Account.fixture()
|
||||
stateService.activeAccount = active
|
||||
stateService.pinProtectedUserKeyValue = [
|
||||
active.profile.userId: "123",
|
||||
]
|
||||
let value = try await subject.isPinUnlockAvailable(userId: "1")
|
||||
XCTAssertTrue(value)
|
||||
}
|
||||
|
||||
/// `isPinUnlockAvailable` throws errors.
|
||||
func test_isPinUnlockAvailable_error() async throws {
|
||||
stateService.pinProtectedUserKeyError = BitwardenTestError.example
|
||||
await assertAsyncThrows(error: BitwardenTestError.example) {
|
||||
_ = try await subject.isPinUnlockAvailable(userId: "1")
|
||||
}
|
||||
}
|
||||
|
||||
/// `lockVault(userId:)` logs an error if one occurs.
|
||||
func test_lock_error() async {
|
||||
await subject.lockVault(userId: nil)
|
||||
@ -241,16 +276,130 @@ final class VaultTimeoutServiceTests: BitwardenTestCase {
|
||||
XCTAssertNotNil(clientService.userClientArray[userId])
|
||||
}
|
||||
|
||||
/// `sessionTimeoutAction()` returns the session timeout action for a user.
|
||||
func test_sessionTimeoutAction() async throws {
|
||||
stateService.activeAccount = .fixture(profile: .fixture(userId: "1"))
|
||||
stateService.accounts = [.fixture(profile: .fixture(userId: "2"))]
|
||||
stateService.timeoutAction["1"] = .lock
|
||||
stateService.timeoutAction["2"] = .logout
|
||||
|
||||
var timeoutAction = try await subject.sessionTimeoutAction(userId: "1")
|
||||
XCTAssertEqual(timeoutAction, .lock)
|
||||
|
||||
timeoutAction = try await subject.sessionTimeoutAction(userId: "2")
|
||||
XCTAssertEqual(timeoutAction, .logout)
|
||||
}
|
||||
|
||||
/// `sessionTimeoutAction()` defaults to logout if the user doesn't have a master password and
|
||||
/// hasn't enabled pin or biometrics unlock.
|
||||
func test_sessionTimeoutAction_noMasterPassword() async throws {
|
||||
stateService.activeAccount = .fixture(profile: .fixture(userId: "1"))
|
||||
stateService.timeoutAction["1"] = .lock
|
||||
stateService.userHasMasterPassword["1"] = false
|
||||
|
||||
let timeoutAction = try await subject.sessionTimeoutAction(userId: "1")
|
||||
XCTAssertEqual(timeoutAction, .logout)
|
||||
}
|
||||
|
||||
/// `sessionTimeoutAction()` allows lock or logout if the user doesn't have a master password
|
||||
/// and has biometrics unlock enabled.
|
||||
func test_sessionTimeoutAction_noMasterPassword_biometricsEnabled() async throws {
|
||||
stateService.activeAccount = .fixture(profile: .fixture(userId: "1"))
|
||||
stateService.timeoutAction["1"] = .lock
|
||||
stateService.userHasMasterPassword["1"] = false
|
||||
biometricsRepository.biometricUnlockStatus = .success(
|
||||
.available(.faceID, enabled: true)
|
||||
)
|
||||
|
||||
var timeoutAction = try await subject.sessionTimeoutAction(userId: "1")
|
||||
XCTAssertEqual(timeoutAction, .lock)
|
||||
|
||||
stateService.timeoutAction["1"] = .logout
|
||||
timeoutAction = try await subject.sessionTimeoutAction(userId: "1")
|
||||
XCTAssertEqual(timeoutAction, .logout)
|
||||
}
|
||||
|
||||
/// `sessionTimeoutAction()` allows lock or logout if the user doesn't have a master password
|
||||
/// and has pin unlock enabled.
|
||||
func test_sessionTimeoutAction_noMasterPassword_pinEnabled() async throws {
|
||||
stateService.activeAccount = .fixture(profile: .fixture(userId: "1"))
|
||||
stateService.pinProtectedUserKeyValue["1"] = "KEY"
|
||||
stateService.timeoutAction["1"] = .lock
|
||||
stateService.userHasMasterPassword["1"] = false
|
||||
|
||||
var timeoutAction = try await subject.sessionTimeoutAction(userId: "1")
|
||||
XCTAssertEqual(timeoutAction, .lock)
|
||||
|
||||
stateService.timeoutAction["1"] = .logout
|
||||
timeoutAction = try await subject.sessionTimeoutAction(userId: "1")
|
||||
XCTAssertEqual(timeoutAction, .logout)
|
||||
}
|
||||
|
||||
/// `sessionTimeoutAction()` throws errors.
|
||||
func test_sessionTimeoutAction_error() async throws {
|
||||
stateService.userHasMasterPasswordError = BitwardenTestError.example
|
||||
await assertAsyncThrows(error: BitwardenTestError.example) {
|
||||
_ = try await subject.sessionTimeoutAction(userId: "1")
|
||||
}
|
||||
}
|
||||
|
||||
/// `.setLastActiveTime(userId:)` sets the user's last active time.
|
||||
func test_setLastActiveTime() async throws {
|
||||
let account = Account.fixture()
|
||||
stateService.activeAccount = account
|
||||
try await subject.setLastActiveTime(userId: account.profile.userId)
|
||||
XCTAssertEqual(
|
||||
stateService.lastActiveTime[account.profile.userId]!.timeIntervalSince1970,
|
||||
Date().timeIntervalSince1970,
|
||||
accuracy: 1.0
|
||||
stateService.lastActiveTime[account.profile.userId]!,
|
||||
timeProvider.presentTime,
|
||||
)
|
||||
XCTAssertEqual(sharedTimeoutService.clearTimeoutUserIds, ["1"])
|
||||
}
|
||||
|
||||
/// `.setLastActiveTime(userId:)` clears shared timeout on a timeout of `.never` or `.onAppRestart`
|
||||
func test_setLastActiveTime_neverOrOnAppRestart() async throws {
|
||||
let account = Account.fixture()
|
||||
stateService.activeAccount = account
|
||||
stateService.vaultTimeout[account.profile.userId] = .never
|
||||
try await subject.setLastActiveTime(userId: account.profile.userId)
|
||||
XCTAssertEqual(sharedTimeoutService.clearTimeoutUserIds, ["1"])
|
||||
|
||||
stateService.vaultTimeout[account.profile.userId] = .onAppRestart
|
||||
try await subject.setLastActiveTime(userId: account.profile.userId)
|
||||
XCTAssertEqual(sharedTimeoutService.clearTimeoutUserIds, ["1", "1"])
|
||||
}
|
||||
|
||||
/// `.setLastActiveTime(userId:)` clears shared timeout if the user's action is `.lock`
|
||||
func test_setLastActiveTime_lock() async throws {
|
||||
let account = Account.fixture()
|
||||
stateService.activeAccount = account
|
||||
stateService.vaultTimeout[account.profile.userId] = .fifteenMinutes
|
||||
stateService.timeoutAction[account.profile.userId] = .lock
|
||||
|
||||
try await subject.setLastActiveTime(userId: account.profile.userId)
|
||||
XCTAssertEqual(sharedTimeoutService.clearTimeoutUserIds, ["1"])
|
||||
}
|
||||
|
||||
/// `.setLastActiveTime(userId:)` updates shared timeout if the user's action is `.logout`
|
||||
func test_setLastActiveTime_logout() async throws {
|
||||
let account = Account.fixture()
|
||||
stateService.activeAccount = account
|
||||
stateService.vaultTimeout[account.profile.userId] = .oneMinute
|
||||
stateService.timeoutAction[account.profile.userId] = .logout
|
||||
|
||||
try await subject.setLastActiveTime(userId: account.profile.userId)
|
||||
XCTAssertEqual(sharedTimeoutService.clearTimeoutUserIds, [])
|
||||
XCTAssertEqual(sharedTimeoutService.updateTimeoutUserId, "1")
|
||||
XCTAssertEqual(sharedTimeoutService.updateTimeoutLastActiveDate, timeProvider.presentTime)
|
||||
XCTAssertEqual(sharedTimeoutService.updateTimeoutTimeoutLength, .oneMinute)
|
||||
}
|
||||
|
||||
/// `.setLastActiveTime(userId:)` throws errors.
|
||||
func test_setLastActive_time_error() async throws {
|
||||
stateService.setLastActiveTimeError = BitwardenTestError.example
|
||||
|
||||
await assertAsyncThrows(error: BitwardenTestError.example) {
|
||||
try await subject.setLastActiveTime(userId: "1")
|
||||
}
|
||||
}
|
||||
|
||||
/// `.setVaultTimeout(value:userId:)` sets the user's vault timeout value.
|
||||
@ -259,6 +408,7 @@ final class VaultTimeoutServiceTests: BitwardenTestCase {
|
||||
stateService.activeAccount = account
|
||||
try await subject.setVaultTimeout(value: .custom(120), userId: account.profile.userId)
|
||||
XCTAssertEqual(stateService.vaultTimeout[account.profile.userId], .custom(120))
|
||||
XCTAssertEqual(sharedTimeoutService.clearTimeoutUserIds, ["1"])
|
||||
}
|
||||
|
||||
/// `.setVaultTimeout(value:userId:)` sets the user's vault timeout value to on app restart.
|
||||
@ -267,6 +417,7 @@ final class VaultTimeoutServiceTests: BitwardenTestCase {
|
||||
stateService.activeAccount = account
|
||||
try await subject.setVaultTimeout(value: .onAppRestart, userId: account.profile.userId)
|
||||
XCTAssertEqual(stateService.vaultTimeout[account.profile.userId], .onAppRestart)
|
||||
XCTAssertEqual(sharedTimeoutService.clearTimeoutUserIds, ["1"])
|
||||
}
|
||||
|
||||
/// `.setVaultTimeout(value:userId:)` sets the user's vault timeout value to never.
|
||||
@ -275,6 +426,40 @@ final class VaultTimeoutServiceTests: BitwardenTestCase {
|
||||
stateService.activeAccount = account
|
||||
try await subject.setVaultTimeout(value: .never, userId: account.profile.userId)
|
||||
XCTAssertEqual(stateService.vaultTimeout[account.profile.userId], .never)
|
||||
XCTAssertEqual(sharedTimeoutService.clearTimeoutUserIds, ["1"])
|
||||
}
|
||||
|
||||
/// `.setVaultTimeout(value:userId:)` clears shared timeout if the user's action is `.lock`
|
||||
func test_setVaultTimeout_lock() async throws {
|
||||
let account = Account.fixture()
|
||||
stateService.activeAccount = account
|
||||
stateService.timeoutAction[account.profile.userId] = .lock
|
||||
|
||||
try await subject.setVaultTimeout(value: .oneMinute, userId: account.profile.userId)
|
||||
XCTAssertEqual(sharedTimeoutService.clearTimeoutUserIds, ["1"])
|
||||
}
|
||||
|
||||
/// `.setVaultTimeout(value:userId:)` updates shared timeout if the user's action is `.logout`
|
||||
func test_setVaultTimeout_logout() async throws {
|
||||
let account = Account.fixture()
|
||||
stateService.activeAccount = account
|
||||
stateService.lastActiveTime[account.profile.userId] = timeProvider.presentTime
|
||||
stateService.timeoutAction[account.profile.userId] = .logout
|
||||
|
||||
try await subject.setVaultTimeout(value: .oneMinute, userId: account.profile.userId)
|
||||
XCTAssertEqual(sharedTimeoutService.clearTimeoutUserIds, [])
|
||||
XCTAssertEqual(sharedTimeoutService.updateTimeoutUserId, "1")
|
||||
XCTAssertEqual(sharedTimeoutService.updateTimeoutLastActiveDate, timeProvider.presentTime)
|
||||
XCTAssertEqual(sharedTimeoutService.updateTimeoutTimeoutLength, .oneMinute)
|
||||
}
|
||||
|
||||
/// `.setVaultTimeout(value:userId:)` throws errors.
|
||||
func test_setVaultTimeout_error() async throws {
|
||||
stateService.setVaultTimeoutError = BitwardenTestError.example
|
||||
|
||||
await assertAsyncThrows(error: BitwardenTestError.example) {
|
||||
try await subject.setVaultTimeout(value: .fiveMinutes, userId: "1")
|
||||
}
|
||||
}
|
||||
|
||||
/// `unlockVault(userId: nil)` should unlock the active account.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user