mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-10 00:42:29 -06:00
1485 lines
66 KiB
Swift
1485 lines
66 KiB
Swift
import AuthenticationServices
|
|
import AuthenticatorBridgeKit
|
|
import BitwardenKit
|
|
import BitwardenKitMocks
|
|
import BitwardenResources
|
|
import Foundation
|
|
import TestHelpers
|
|
import XCTest
|
|
|
|
@testable import BitwardenShared
|
|
|
|
// swiftlint:disable file_length
|
|
|
|
class AppProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_body_length
|
|
// MARK: Properties
|
|
|
|
var appIntentMediator: MockAppIntentMediator!
|
|
var appModule: MockAppModule!
|
|
var authRepository: MockAuthRepository!
|
|
var authenticatorSyncService: MockAuthenticatorSyncService!
|
|
var autofillCredentialService: MockAutofillCredentialService!
|
|
var clientService: MockClientService!
|
|
var configService: MockConfigService!
|
|
var coordinator: MockCoordinator<AppRoute, AppEvent>!
|
|
var environmentService: MockEnvironmentService!
|
|
var errorReporter: MockErrorReporter!
|
|
var fido2UserInterfaceHelper: MockFido2UserInterfaceHelper!
|
|
var eventService: MockEventService!
|
|
var migrationService: MockMigrationService!
|
|
var notificationCenterService: MockNotificationCenterService!
|
|
var notificationService: MockNotificationService!
|
|
var pendingAppIntentActionMediator: MockPendingAppIntentActionMediator!
|
|
var policyService: MockPolicyService!
|
|
var router: MockRouter<AuthEvent, AuthRoute>!
|
|
var stateService: MockStateService!
|
|
var subject: AppProcessor!
|
|
var syncService: MockSyncService!
|
|
var timeProvider: MockTimeProvider!
|
|
var vaultRepository: MockVaultRepository!
|
|
var vaultTimeoutService: MockVaultTimeoutService!
|
|
|
|
var didEnterBackgroundCalled = 0
|
|
var willEnterForegroundCalled = 0
|
|
|
|
// MARK: Setup & Teardown
|
|
|
|
override func setUp() { // swiftlint:disable:this function_body_length
|
|
super.setUp()
|
|
|
|
router = MockRouter(routeForEvent: { _ in .landing })
|
|
appIntentMediator = MockAppIntentMediator()
|
|
appModule = MockAppModule()
|
|
authRepository = MockAuthRepository()
|
|
authenticatorSyncService = MockAuthenticatorSyncService()
|
|
autofillCredentialService = MockAutofillCredentialService()
|
|
clientService = MockClientService()
|
|
configService = MockConfigService()
|
|
coordinator = MockCoordinator()
|
|
appModule.authRouter = router
|
|
appModule.appCoordinator = coordinator
|
|
environmentService = MockEnvironmentService()
|
|
errorReporter = MockErrorReporter()
|
|
fido2UserInterfaceHelper = MockFido2UserInterfaceHelper()
|
|
eventService = MockEventService()
|
|
migrationService = MockMigrationService()
|
|
notificationCenterService = MockNotificationCenterService()
|
|
notificationService = MockNotificationService()
|
|
pendingAppIntentActionMediator = MockPendingAppIntentActionMediator()
|
|
policyService = MockPolicyService()
|
|
stateService = MockStateService()
|
|
syncService = MockSyncService()
|
|
timeProvider = MockTimeProvider(.currentTime)
|
|
vaultRepository = MockVaultRepository()
|
|
vaultTimeoutService = MockVaultTimeoutService()
|
|
|
|
subject = AppProcessor(
|
|
appIntentMediator: appIntentMediator,
|
|
appModule: appModule,
|
|
debugDidEnterBackground: { [weak self] in self?.didEnterBackgroundCalled += 1 },
|
|
debugWillEnterForeground: { [weak self] in self?.willEnterForegroundCalled += 1 },
|
|
services: ServiceContainer.withMocks(
|
|
authRepository: authRepository,
|
|
authenticatorSyncService: authenticatorSyncService,
|
|
autofillCredentialService: autofillCredentialService,
|
|
clientService: clientService,
|
|
configService: configService,
|
|
environmentService: environmentService,
|
|
errorReporter: errorReporter,
|
|
eventService: eventService,
|
|
fido2UserInterfaceHelper: fido2UserInterfaceHelper,
|
|
migrationService: migrationService,
|
|
notificationService: notificationService,
|
|
pendingAppIntentActionMediator: pendingAppIntentActionMediator,
|
|
policyService: policyService,
|
|
notificationCenterService: notificationCenterService,
|
|
stateService: stateService,
|
|
syncService: syncService,
|
|
vaultRepository: vaultRepository,
|
|
vaultTimeoutService: vaultTimeoutService,
|
|
),
|
|
)
|
|
subject.coordinator = coordinator.asAnyCoordinator()
|
|
}
|
|
|
|
override func tearDown() {
|
|
super.tearDown()
|
|
|
|
appModule = nil
|
|
authRepository = nil
|
|
autofillCredentialService = nil
|
|
clientService = nil
|
|
configService = nil
|
|
coordinator = nil
|
|
environmentService = nil
|
|
errorReporter = nil
|
|
fido2UserInterfaceHelper = nil
|
|
eventService = nil
|
|
migrationService = nil
|
|
notificationCenterService = nil
|
|
notificationService = nil
|
|
pendingAppIntentActionMediator = nil
|
|
policyService = nil
|
|
router = nil
|
|
stateService = nil
|
|
subject = nil
|
|
syncService = nil
|
|
timeProvider = nil
|
|
vaultRepository = nil
|
|
vaultTimeoutService = nil
|
|
}
|
|
|
|
// MARK: Tests
|
|
|
|
/// `init()` subscribes to app background events and logs an error if one occurs when
|
|
/// setting the last active time.
|
|
@MainActor
|
|
func test_appBackgrounded_error() {
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.setLastActiveTimeError = BitwardenTestError.example
|
|
|
|
notificationCenterService.didEnterBackgroundSubject.send()
|
|
|
|
waitFor(!errorReporter.errors.isEmpty)
|
|
XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [.example])
|
|
}
|
|
|
|
/// The user's last active time is updated when the app is backgrounded.
|
|
@MainActor
|
|
func test_appBackgrounded_setLastActiveTime() {
|
|
let account: Account = .fixture()
|
|
stateService.activeAccount = account
|
|
|
|
vaultTimeoutService.lastActiveTime[account.profile.userId] = .distantPast
|
|
|
|
notificationCenterService.didEnterBackgroundSubject.send()
|
|
waitFor(vaultTimeoutService.lastActiveTime[account.profile.userId] != .distantPast)
|
|
|
|
let updated = vaultTimeoutService.lastActiveTime[account.profile.userId]
|
|
|
|
XCTAssertEqual(timeProvider.presentTime.timeIntervalSince1970, updated!.timeIntervalSince1970, accuracy: 1.0)
|
|
}
|
|
|
|
/// `showDebugMenu` will send the correct route to the coordinator.
|
|
@MainActor
|
|
func test_showDebugMenu() {
|
|
subject.showDebugMenu()
|
|
XCTAssertEqual(coordinator.routes.last, .debugMenu)
|
|
}
|
|
|
|
/// `didRegister(withToken:)` passes the token to the notification service.
|
|
@MainActor
|
|
func test_didRegister() throws {
|
|
let tokenData = try XCTUnwrap("tokensForFree".data(using: .utf8))
|
|
|
|
let task = Task {
|
|
subject.didRegister(withToken: tokenData)
|
|
}
|
|
|
|
waitFor(notificationService.registrationTokenData == tokenData)
|
|
task.cancel()
|
|
}
|
|
|
|
/// `failedToRegister(_:)` records the error.
|
|
@MainActor
|
|
func test_failedToRegister() {
|
|
subject.failedToRegister(BitwardenTestError.example)
|
|
XCTAssertEqual(errorReporter.errors.last as? BitwardenTestError, .example)
|
|
}
|
|
|
|
/// `init()` subscribes to will enter foreground events and handles an active user timeout.
|
|
@MainActor
|
|
func test_init_appForeground_activeUserTimeout() {
|
|
let account1 = Account.fixture(profile: .fixture(userId: "1"))
|
|
let account2 = Account.fixture(profile: .fixture(userId: "2"))
|
|
stateService.activeAccount = account1
|
|
stateService.accounts = [account1, account2]
|
|
|
|
vaultTimeoutService.shouldSessionTimeout["1"] = true
|
|
notificationCenterService.willEnterForegroundSubject.send()
|
|
// Wait for the checkSessionTimeouts method to be called
|
|
waitFor(authRepository.checkSessionTimeoutCalled)
|
|
|
|
// Simulate calling the handleActiveUser closure
|
|
if let handleActiveUserClosure = authRepository.handleActiveUserClosure {
|
|
Task {
|
|
await handleActiveUserClosure("1")
|
|
}
|
|
}
|
|
waitFor(!coordinator.events.isEmpty)
|
|
XCTAssertEqual(coordinator.events, [.didTimeout(userId: "1")])
|
|
}
|
|
|
|
/// `init()` subscribes to will enter foreground events ands completes the user's autofill setup
|
|
/// process if autofill is enabled and they previously choose to set it up later.
|
|
@MainActor
|
|
func test_init_appForeground_completeAutofillAccountSetup() async throws {
|
|
// The processor checks for autofill completion when entering the foreground. Wait for the
|
|
// initial check to finish when the test starts before continuing.
|
|
try await waitForAsync { self.willEnterForegroundCalled == 1 }
|
|
|
|
autofillCredentialService.isAutofillCredentialsEnabled = true
|
|
stateService.activeAccount = .fixture()
|
|
stateService.accounts = [.fixture()]
|
|
stateService.accountSetupAutofill["1"] = .setUpLater
|
|
|
|
notificationCenterService.willEnterForegroundSubject.send()
|
|
try await waitForAsync { self.willEnterForegroundCalled == 2 }
|
|
|
|
XCTAssertEqual(stateService.accountSetupAutofill, ["1": .complete])
|
|
}
|
|
|
|
/// `init()` subscribes to will enter foreground events and handles accountBecameActive if the
|
|
/// never timeout account is unlocked in extension and there is no pending `AppIntent` actions.
|
|
@MainActor
|
|
func test_init_appForeground_checkAccountBecomeActive() async throws {
|
|
// The processor checks for switched accounts when entering the foreground. Wait for the
|
|
// initial check to finish when the test starts before continuing.
|
|
try await waitForAsync { self.willEnterForegroundCalled == 1 }
|
|
let account: Account = .fixture(profile: .fixture(userId: "2"))
|
|
let userId = account.profile.userId
|
|
stateService.activeAccount = account
|
|
authRepository.activeAccount = account
|
|
stateService.didAccountSwitchInExtensionResult = .success(true)
|
|
authRepository.vaultTimeout = [userId: .never]
|
|
authRepository.isLockedResult = .success(true)
|
|
stateService.manuallyLockedAccounts = [userId: false]
|
|
|
|
notificationCenterService.willEnterForegroundSubject.send()
|
|
try await waitForAsync { self.willEnterForegroundCalled == 2 }
|
|
|
|
XCTAssertEqual(
|
|
coordinator.events.last,
|
|
AppEvent.accountBecameActive(
|
|
account,
|
|
attemptAutomaticBiometricUnlock: true,
|
|
didSwitchAccountAutomatically: false,
|
|
),
|
|
)
|
|
}
|
|
|
|
/// `init()` subscribes to will enter foreground events and handles accountBecameActive if the
|
|
/// never timeout account is unlocked in extension and there is an empty collection of pending `AppIntent` actions.
|
|
@MainActor
|
|
func test_init_appForeground_checkAccountBecomeActivePendingAppIntentActionsEmpty() async throws {
|
|
// The processor checks for switched accounts when entering the foreground. Wait for the
|
|
// initial check to finish when the test starts before continuing.
|
|
try await waitForAsync { self.willEnterForegroundCalled == 1 }
|
|
let account: Account = .fixture(profile: .fixture(userId: "2"))
|
|
let userId = account.profile.userId
|
|
stateService.activeAccount = account
|
|
authRepository.activeAccount = account
|
|
stateService.didAccountSwitchInExtensionResult = .success(true)
|
|
authRepository.vaultTimeout = [userId: .never]
|
|
authRepository.isLockedResult = .success(true)
|
|
stateService.manuallyLockedAccounts = [userId: false]
|
|
stateService.pendingAppIntentActions = []
|
|
|
|
notificationCenterService.willEnterForegroundSubject.send()
|
|
try await waitForAsync { self.willEnterForegroundCalled == 2 }
|
|
|
|
XCTAssertEqual(
|
|
coordinator.events.last,
|
|
AppEvent.accountBecameActive(
|
|
account,
|
|
attemptAutomaticBiometricUnlock: true,
|
|
didSwitchAccountAutomatically: false,
|
|
),
|
|
)
|
|
}
|
|
|
|
/// `init()` subscribes to will enter foreground events and doesn't handle accountBecameActive if the
|
|
/// never timeout account is unlocked in extension but there's a pending `.lockAll` `AppIntent`.
|
|
@MainActor
|
|
func test_init_appForeground_checkAccountBecomeActiveEventDoesntHappenWhenPendingLockAllAppIntent() async throws {
|
|
// The processor checks for switched accounts when entering the foreground. Wait for the
|
|
// initial check to finish when the test starts before continuing.
|
|
try await waitForAsync { self.willEnterForegroundCalled == 1 }
|
|
let account: Account = .fixture(profile: .fixture(userId: "2"))
|
|
let userId = account.profile.userId
|
|
stateService.activeAccount = account
|
|
authRepository.activeAccount = account
|
|
stateService.didAccountSwitchInExtensionResult = .success(true)
|
|
authRepository.vaultTimeout = [userId: .never]
|
|
authRepository.isLockedResult = .success(true)
|
|
stateService.manuallyLockedAccounts = [userId: false]
|
|
stateService.pendingAppIntentActions = [.lockAll]
|
|
|
|
notificationCenterService.willEnterForegroundSubject.send()
|
|
try await waitForAsync { self.willEnterForegroundCalled == 2 }
|
|
|
|
XCTAssertNotEqual(
|
|
coordinator.events.last,
|
|
AppEvent.accountBecameActive(
|
|
account,
|
|
attemptAutomaticBiometricUnlock: true,
|
|
didSwitchAccountAutomatically: false,
|
|
),
|
|
)
|
|
}
|
|
|
|
/// `init()` subscribes to will enter foreground events and logs an error if one occurs while
|
|
/// checking if the active account was changed in an extension.
|
|
@MainActor
|
|
func test_init_appForeground_checkIfExtensionSwitchedAccounts_error() async throws {
|
|
// The processor checks for switched accounts when entering the foreground. Wait for the
|
|
// initial check to finish when the test starts before continuing.
|
|
try await waitForAsync { self.willEnterForegroundCalled == 1 }
|
|
|
|
stateService.didAccountSwitchInExtensionResult = .failure(BitwardenTestError.example)
|
|
|
|
notificationCenterService.willEnterForegroundSubject.send()
|
|
try await waitForAsync { self.willEnterForegroundCalled == 2 }
|
|
|
|
XCTAssertTrue(coordinator.events.isEmpty)
|
|
XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [.example])
|
|
}
|
|
|
|
/// `init()` subscribes to will enter foreground events and doesn't make any navigation changes
|
|
/// if the active account wasn't changed in the extension.
|
|
@MainActor
|
|
func test_init_appForeground_checkIfExtensionSwitchedAccounts_accountNotSwitched() async throws {
|
|
// The processor checks for switched accounts when entering the foreground. Wait for the
|
|
// initial check to finish when the test starts before continuing.
|
|
try await waitForAsync { self.willEnterForegroundCalled == 1 }
|
|
|
|
stateService.didAccountSwitchInExtensionResult = .success(false)
|
|
|
|
notificationCenterService.willEnterForegroundSubject.send()
|
|
try await waitForAsync { self.willEnterForegroundCalled == 2 }
|
|
|
|
XCTAssertTrue(coordinator.events.isEmpty)
|
|
}
|
|
|
|
/// `init()` subscribes to will enter foreground events and handles switching accounts if the
|
|
/// active account was changed in the extension.
|
|
@MainActor
|
|
func test_init_appForeground_checkIfExtensionSwitchedAccounts_accountSwitched() async throws {
|
|
// The processor checks for switched accounts when entering the foreground. Wait for the
|
|
// initial check to finish when the test starts before continuing.
|
|
try await waitForAsync { self.willEnterForegroundCalled == 1 }
|
|
|
|
stateService.activeAccount = .fixture(profile: .fixture(userId: "2"))
|
|
stateService.didAccountSwitchInExtensionResult = .success(true)
|
|
|
|
notificationCenterService.willEnterForegroundSubject.send()
|
|
try await waitForAsync { self.willEnterForegroundCalled == 2 }
|
|
|
|
XCTAssertEqual(coordinator.events, [.switchAccounts(userId: "2", isAutomatic: false)])
|
|
}
|
|
|
|
/// `init()` subscribes to will enter foreground events and doesn't check for an account switch
|
|
/// when running in the extension.
|
|
@MainActor
|
|
func test_init_appForeground_checkIfExtensionSwitchedAccounts_inExtension() async throws {
|
|
let delegate = MockAppExtensionDelegate()
|
|
delegate.isInAppExtension = true
|
|
let notificationCenterService = MockNotificationCenterService()
|
|
let stateService = MockStateService()
|
|
|
|
var willEnterForegroundCalled = 0
|
|
_ = AppProcessor(
|
|
appExtensionDelegate: delegate,
|
|
appModule: appModule,
|
|
debugWillEnterForeground: { willEnterForegroundCalled += 1 },
|
|
services: ServiceContainer.withMocks(
|
|
notificationCenterService: notificationCenterService,
|
|
stateService: stateService,
|
|
),
|
|
)
|
|
try await waitForAsync { willEnterForegroundCalled == 1 }
|
|
|
|
stateService.didAccountSwitchInExtensionResult = .success(true)
|
|
|
|
notificationCenterService.willEnterForegroundSubject.send()
|
|
try await waitForAsync { willEnterForegroundCalled == 2 }
|
|
|
|
XCTAssertTrue(coordinator.events.isEmpty)
|
|
}
|
|
|
|
/// `init()` subscribes to will enter foreground events and restarts the app is there's no
|
|
/// active account.
|
|
@MainActor
|
|
func test_init_appForeground_checkIfExtensionSwitchedAccounts_noActiveAccount() async throws {
|
|
// The processor checks for switched accounts when entering the foreground. Wait for the
|
|
// initial check to finish when the test starts before continuing.
|
|
try await waitForAsync { self.willEnterForegroundCalled == 1 }
|
|
|
|
stateService.didAccountSwitchInExtensionResult = .success(true)
|
|
|
|
notificationCenterService.willEnterForegroundSubject.send()
|
|
try await waitForAsync { self.willEnterForegroundCalled == 2 }
|
|
|
|
XCTAssertEqual(coordinator.events, [.didStart])
|
|
}
|
|
|
|
/// `init()` sets the `AppProcessor` as the delegate of any necessary services.
|
|
func test_init_setDelegates() {
|
|
XCTAssertIdentical(notificationService.delegate, subject)
|
|
XCTAssertIdentical(syncService.delegate, subject)
|
|
}
|
|
|
|
/// `handleAppLinks(URL)` navigates the user based on the input URL.
|
|
@MainActor
|
|
func test_init_handleAppLinks() {
|
|
// swiftlint:disable:next line_length
|
|
let url = URL(string: "https://bitwarden.com/redirect-connector.html#finish-signup?email=example@email.com&token=verificationtoken&fromEmail=true")
|
|
subject.handleAppLinks(incomingURL: url!)
|
|
|
|
XCTAssertEqual(coordinator.routes.last, .auth(.completeRegistrationFromAppLink(
|
|
emailVerificationToken: "verificationtoken",
|
|
userEmail: "example@email.com",
|
|
fromEmail: true,
|
|
)))
|
|
}
|
|
|
|
/// `handleAppLinks(URL)` navigates the user based on the input URL with wrong fromEmail value.
|
|
@MainActor
|
|
func test_init_handleAppLinks_fromEmail_notBool() {
|
|
// swiftlint:disable:next line_length
|
|
let url = URL(string: "https://bitwarden.eu/redirect-connector.html#finish-signup?email=example@email.com&token=verificationtoken&fromEmail=potato")
|
|
subject.handleAppLinks(incomingURL: url!)
|
|
|
|
XCTAssertEqual(coordinator.routes.last, .auth(.completeRegistrationFromAppLink(
|
|
emailVerificationToken: "verificationtoken",
|
|
userEmail: "example@email.com",
|
|
fromEmail: true,
|
|
)))
|
|
}
|
|
|
|
/// `handleAppLinks(URL)` checks error report for `.appLinksInvalidURL`.
|
|
@MainActor
|
|
func test_init_handleAppLinks_invalidURL() {
|
|
// swiftlint:disable:next line_length
|
|
let noPathUrl = URL(string: "https://bitwarden.com/redirect-connector.html#email=example@email.com&token=verificationtoken")
|
|
subject.handleAppLinks(incomingURL: noPathUrl!)
|
|
XCTAssertEqual(errorReporter.errors.last as? AppProcessorError, .appLinksInvalidURL)
|
|
XCTAssertEqual(errorReporter.errors.count, 1)
|
|
errorReporter.errors.removeAll()
|
|
|
|
let noParamsUrl = URL(string: "https://bitwarden.com/redirect-connector.html#finish-signup/")
|
|
subject.handleAppLinks(incomingURL: noParamsUrl!)
|
|
XCTAssertEqual(errorReporter.errors.last as? AppProcessorError, .appLinksInvalidURL)
|
|
XCTAssertEqual(errorReporter.errors.count, 1)
|
|
errorReporter.errors.removeAll()
|
|
|
|
let invalidHostUrl = URL(string: "/finish-signup?email=example@email.com")
|
|
subject.handleAppLinks(incomingURL: invalidHostUrl!)
|
|
XCTAssertEqual(errorReporter.errors.last as? AppProcessorError, .appLinksInvalidURL)
|
|
XCTAssertEqual(errorReporter.errors.count, 1)
|
|
}
|
|
|
|
/// `handleAppLinks(URL)` checks error report for `.appLinksInvalidPath`.
|
|
@MainActor
|
|
func test_init_handleAppLinks_invalidPath() {
|
|
// swiftlint:disable:next line_length
|
|
let url = URL(string: "https://bitwarden.com/redirect-connector.html#not-valid?email=example@email.com&token=verificationtoken&fromEmail=true")
|
|
subject.handleAppLinks(incomingURL: url!)
|
|
XCTAssertEqual(errorReporter.errors.last as? AppProcessorError, .appLinksInvalidPath)
|
|
}
|
|
|
|
/// `handleAppLinks(URL)` checks error report for `.appLinksInvalidParametersForPath`.
|
|
@MainActor
|
|
func test_init_handleAppLinks_invalidParametersForPath() {
|
|
// swiftlint:disable:next line_length
|
|
var url = URL(string: "https://bitwarden.com/redirect-connector.html#finish-signup?token=verificationtoken&fromEmail=true")
|
|
subject.handleAppLinks(incomingURL: url!)
|
|
XCTAssertEqual(errorReporter.errors.last as? AppProcessorError, .appLinksInvalidParametersForPath)
|
|
XCTAssertEqual(errorReporter.errors.count, 1)
|
|
errorReporter.errors.removeAll()
|
|
|
|
// swiftlint:disable:next line_length
|
|
url = URL(string: "https://bitwarden.com/redirect-connector.html#finish-signup?email=example@email.com&fromEmail=true")
|
|
subject.handleAppLinks(incomingURL: url!)
|
|
XCTAssertEqual(errorReporter.errors.last as? AppProcessorError, .appLinksInvalidParametersForPath)
|
|
XCTAssertEqual(errorReporter.errors.count, 1)
|
|
errorReporter.errors.removeAll()
|
|
|
|
// swiftlint:disable:next line_length
|
|
url = URL(string: "https://bitwarden.com/redirect-connector.html#finish-signup?email=example@email.com&token=verificationtoken")
|
|
subject.handleAppLinks(incomingURL: url!)
|
|
XCTAssertEqual(errorReporter.errors.last as? AppProcessorError, .appLinksInvalidParametersForPath)
|
|
XCTAssertEqual(errorReporter.errors.count, 1)
|
|
errorReporter.errors.removeAll()
|
|
}
|
|
|
|
/// `init()` subscribes to will pending App Intent actions publisher and handles an active user timeout.
|
|
@MainActor
|
|
func test_init_pendingAppIntentActionsTask() {
|
|
// Wait and reset for the first publisher default values which are `nil`.
|
|
waitFor(pendingAppIntentActionMediator.executePendingAppIntentActionsCalled)
|
|
pendingAppIntentActionMediator.executePendingAppIntentActionsCalled = false
|
|
|
|
stateService.pendingAppIntentActionsSubject.send([.lockAll])
|
|
waitFor(pendingAppIntentActionMediator.executePendingAppIntentActionsCalled)
|
|
}
|
|
|
|
/// `init()` starts the upload-event timer and attempts to upload events.
|
|
@MainActor
|
|
func test_init_uploadEvents() {
|
|
XCTAssertNotNil(subject.sendEventTimer)
|
|
XCTAssertEqual(subject.sendEventTimer?.isValid, true)
|
|
subject.sendEventTimer?.fire() // Necessary because it's a 5-minute timer
|
|
waitFor(eventService.uploadCalled)
|
|
XCTAssertTrue(eventService.uploadCalled)
|
|
}
|
|
|
|
/// `getter:isAutofillingFromList` returns `false` when delegate is not a Fido2 one.
|
|
@MainActor
|
|
func test_isAutofillingFromList_falseNoFido2Delegate() async throws {
|
|
XCTAssertFalse(subject.isAutofillingFromList)
|
|
}
|
|
|
|
/// `messageReceived(_:notificationDismissed:notificationTapped)` passes the data to the notification service.
|
|
func test_messageReceived() async {
|
|
let message: [AnyHashable: Any] = ["knock knock": "who's there?"]
|
|
|
|
await subject.messageReceived(message)
|
|
|
|
XCTAssertEqual(notificationService.messageReceivedMessage?.keys.first, "knock knock")
|
|
}
|
|
|
|
/// `onNeedsUserInteraction()` doesn't throw when `appExtensionDelegate` is not a Fido2 one.
|
|
func test_onNeedsUserInteraction_flowWithUserInteraction() async {
|
|
await assertAsyncDoesNotThrow {
|
|
try await subject.onNeedsUserInteraction()
|
|
}
|
|
}
|
|
|
|
/// `onPendingAppIntentActionSuccess(_:data:)` handles event `.accountBecameActive` when
|
|
/// pending app intent action is `.lockAll` and `data` is an account.
|
|
@MainActor
|
|
func test_onPendingAppIntentActionSuccess_lockAll() async {
|
|
let account = Account.fixture()
|
|
await subject.onPendingAppIntentActionSuccess(.lockAll, data: account)
|
|
XCTAssertEqual(
|
|
coordinator.events,
|
|
[
|
|
.accountBecameActive(
|
|
account,
|
|
attemptAutomaticBiometricUnlock: true,
|
|
didSwitchAccountAutomatically: false,
|
|
),
|
|
],
|
|
)
|
|
}
|
|
|
|
/// `onPendingAppIntentActionSuccess(_:data:)` doesn't handle event `.accountBecameActive` when
|
|
/// pending app intent action is `.lockAll` and `data` is `nil`.
|
|
@MainActor
|
|
func test_onPendingAppIntentActionSuccess_lockAllNoData() async {
|
|
await subject.onPendingAppIntentActionSuccess(.lockAll, data: nil)
|
|
XCTAssertTrue(coordinator.events.isEmpty)
|
|
}
|
|
|
|
/// `onPendingAppIntentActionSuccess(_:data:)` doesn't handle event `.accountBecameActive` when
|
|
/// pending app intent action is `.lockAll` and `data` is not an `Account`.
|
|
@MainActor
|
|
func test_onPendingAppIntentActionSuccess_lockAllDataNoAccount() async {
|
|
await subject.onPendingAppIntentActionSuccess(.lockAll, data: "noAccount")
|
|
XCTAssertTrue(coordinator.events.isEmpty)
|
|
}
|
|
|
|
/// `onPendingAppIntentActionSuccess(_:data:)` handles event `.didLogOutAll` when
|
|
/// pending app intent action is `.logOutAll`.
|
|
@MainActor
|
|
func test_onPendingAppIntentActionSuccess_logOutAll() async {
|
|
await subject.onPendingAppIntentActionSuccess(.logOutAll, data: nil)
|
|
XCTAssertEqual(
|
|
coordinator.events,
|
|
[.didLogout(userId: nil, userInitiated: true)],
|
|
)
|
|
}
|
|
|
|
/// `onPendingAppIntentActionSuccess(_:data:)` sets `setAuthCompletionRoute` as the generator when
|
|
/// pending app intent action is `.openGenerator` and the vault is locked.
|
|
@MainActor
|
|
func test_onPendingAppIntentActionSuccess_openGeneratorVaultLocked() async throws {
|
|
await subject.onPendingAppIntentActionSuccess(.openGenerator, data: nil)
|
|
XCTAssertEqual(coordinator.events, [.setAuthCompletionRoute(.tab(.generator(.generator())))])
|
|
}
|
|
|
|
/// `onPendingAppIntentActionSuccess(_:data:)` handles navigation to the generator screen when
|
|
/// pending app intent action is `.openGenerator` and the vault is unlocked.
|
|
@MainActor
|
|
func test_onPendingAppIntentActionSuccess_openGeneratorVaultUnlocked() async throws {
|
|
let account = Account.fixture()
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked[account.profile.userId] = false
|
|
|
|
await subject.onPendingAppIntentActionSuccess(.openGenerator, data: nil)
|
|
XCTAssertEqual(coordinator.routes.last, .tab(.generator(.generator())))
|
|
}
|
|
|
|
/// `onPendingAppIntentActionSuccess(_:data:)` handles receiving a pending AppIntent action for `.openGenerator`
|
|
/// and setting an auth completion route on the coordinator if the the user's vault is unlocked
|
|
/// but will be timing out as the app is foregrounded.
|
|
@MainActor
|
|
func test_onPendingAppIntentActionSuccess_openGeneratorVaultUnlockedTimeout() async throws {
|
|
let account = Account.fixture()
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked[account.profile.userId] = false
|
|
vaultTimeoutService.shouldSessionTimeout[account.profile.userId] = true
|
|
|
|
await subject.onPendingAppIntentActionSuccess(.openGenerator, data: nil)
|
|
XCTAssertEqual(coordinator.events, [.setAuthCompletionRoute(.tab(.generator(.generator())))])
|
|
}
|
|
|
|
/// `onPendingAppIntentActionSuccess(_:data:)` handles receiving a pending AppIntent action for `.openGenerator`
|
|
/// and setting an auth completion route on the coordinator if the the user's vault is unlocked
|
|
/// but checking timing out as throws an error.
|
|
@MainActor
|
|
func test_onPendingAppIntentActionSuccess_openGeneratorVaultUnlockedTimeoutError() async throws {
|
|
let account = Account.fixture()
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked[account.profile.userId] = false
|
|
vaultTimeoutService.shouldSessionTimeoutError = BitwardenTestError.example
|
|
|
|
await subject.onPendingAppIntentActionSuccess(.openGenerator, data: nil)
|
|
XCTAssertEqual(coordinator.events, [.setAuthCompletionRoute(.tab(.generator(.generator())))])
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving a bitwarden deep link and setting an auth completion route on the
|
|
/// coordinator to handle routing to the account security screen when the vault is unlocked.
|
|
@MainActor
|
|
func test_openUrl_bitwardenAccountSecurity_vaultLocked() async throws {
|
|
await subject.openUrl(.bitwardenAccountSecurity)
|
|
XCTAssertEqual(coordinator.events, [.setAuthCompletionRoute(.tab(.settings(.accountSecurity)))])
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving a bitwarden deep link and routing to the account security screen.
|
|
@MainActor
|
|
func test_openUrl_bitwardenAccountSecurity_vaultUnlocked() async throws {
|
|
let account = Account.fixture()
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked[account.profile.userId] = false
|
|
|
|
await subject.openUrl(.bitwardenAccountSecurity)
|
|
XCTAssertEqual(coordinator.routes.last, .tab(.settings(.accountSecurity)))
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving a bitwarden deep link and setting an auth completion route on the
|
|
/// coordinator if the the user's vault is unlocked but will be timing out as the app is
|
|
/// foregrounded.
|
|
@MainActor
|
|
func test_openUrl_bitwardenAccountSecurity_vaultUnlockedTimeout() async throws {
|
|
let account = Account.fixture()
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked[account.profile.userId] = false
|
|
vaultTimeoutService.shouldSessionTimeout[account.profile.userId] = true
|
|
|
|
await subject.openUrl(.bitwardenAccountSecurity)
|
|
XCTAssertEqual(coordinator.events, [.setAuthCompletionRoute(.tab(.settings(.accountSecurity)))])
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving a bitwarden deep link and setting an auth completion route on the
|
|
/// coordinator if the the user's vault is unlocked but will be timing out as the app is
|
|
/// foregrounded.
|
|
@MainActor
|
|
func test_openUrl_bitwardenAccountSecurity_vaultUnlockedTimeoutError() async throws {
|
|
let account = Account.fixture()
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked[account.profile.userId] = false
|
|
vaultTimeoutService.shouldSessionTimeoutError = BitwardenTestError.example
|
|
|
|
await subject.openUrl(.bitwardenAccountSecurity)
|
|
XCTAssertEqual(coordinator.events, [.setAuthCompletionRoute(.tab(.settings(.accountSecurity)))])
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving a bitwarden Authenticator new item deep link with the vault unlocked and an
|
|
/// invalid item is found. It shows a generic error alert and does not produce a route.
|
|
@MainActor
|
|
func test_openUrl_bitwardenAuthenticatorNewItem_invalidItem() async throws {
|
|
let account = Account.fixture()
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked[account.profile.userId] = false
|
|
authenticatorSyncService.tempItem = AuthenticatorBridgeItemDataView(
|
|
accountDomain: nil,
|
|
accountEmail: nil,
|
|
favorite: false,
|
|
id: "",
|
|
name: "",
|
|
totpKey: nil,
|
|
username: nil,
|
|
)
|
|
|
|
await subject.openUrl(.bitwardenAuthenticatorNewItem)
|
|
XCTAssertEqual(coordinator.alertShown.first,
|
|
.defaultAlert(title: Localizations.somethingWentWrong,
|
|
message: Localizations.unableToMoveTheSelectedItemPleaseTryAgain))
|
|
XCTAssertEqual(coordinator.routes, [])
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving a bitwarden Authenticator new item deep link when no temporary item is
|
|
/// found in the shared store. It shows a generic error alert and does not produce a route.
|
|
@MainActor
|
|
func test_openUrl_bitwardenAuthenticatorNewItem_noItemFound() async throws {
|
|
let account = Account.fixture()
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked[account.profile.userId] = false
|
|
authenticatorSyncService.tempItem = nil
|
|
|
|
await subject.openUrl(.bitwardenAuthenticatorNewItem)
|
|
XCTAssertEqual(coordinator.alertShown.first,
|
|
.defaultAlert(title: Localizations.somethingWentWrong,
|
|
message: Localizations.unableToMoveTheSelectedItemPleaseTryAgain))
|
|
XCTAssertEqual(coordinator.routes, [])
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving a bitwarden Authenticator new item deep link with the vault unlocked and the
|
|
/// item is found, but the item has no TOTP key. It shows a generic error alert and does not produce a route.
|
|
@MainActor
|
|
func test_openUrl_bitwardenAuthenticatorNewItem_noTotpKey() async throws {
|
|
let account = Account.fixture()
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked[account.profile.userId] = false
|
|
authenticatorSyncService.tempItem = AuthenticatorBridgeItemDataView(
|
|
accountDomain: nil,
|
|
accountEmail: nil,
|
|
favorite: false,
|
|
id: "",
|
|
name: "",
|
|
totpKey: nil,
|
|
username: nil,
|
|
)
|
|
|
|
await subject.openUrl(.bitwardenAuthenticatorNewItem)
|
|
XCTAssertEqual(coordinator.alertShown.first,
|
|
.defaultAlert(title: Localizations.somethingWentWrong,
|
|
message: Localizations.unableToMoveTheSelectedItemPleaseTryAgain))
|
|
XCTAssertEqual(coordinator.routes, [])
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving a bitwarden Authenticator new item deep link with the vault unlocked and the
|
|
/// item is found.
|
|
@MainActor
|
|
func test_openUrl_bitwardenAuthenticatorNewItem_success() async throws {
|
|
let account = Account.fixture()
|
|
let otpKey: String = .otpAuthUriKeyComplete
|
|
let model = TOTPKeyModel(authenticatorKey: otpKey)
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked[account.profile.userId] = false
|
|
authenticatorSyncService.tempItem = AuthenticatorBridgeItemDataView(
|
|
accountDomain: nil,
|
|
accountEmail: nil,
|
|
favorite: false,
|
|
id: "",
|
|
name: "",
|
|
totpKey: otpKey,
|
|
username: nil,
|
|
)
|
|
|
|
await subject.openUrl(.bitwardenAuthenticatorNewItem)
|
|
XCTAssertEqual(coordinator.routes.last, .tab(.vault(.vaultItemSelection(model))))
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving a bitwarden link with an invalid path and
|
|
/// silently returns with a no-op.
|
|
@MainActor
|
|
func test_openUrl_bitwardenInvalidPath_failSilently() async throws {
|
|
await subject.openUrl(.bitwardenInvalidPath)
|
|
|
|
XCTAssertEqual(coordinator.alertShown, [])
|
|
XCTAssertEqual(coordinator.routes, [])
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving a bitwarden link with nothing but the scheme (i.e. `bitwarden://`) and
|
|
/// silently returns with a no-op.
|
|
@MainActor
|
|
func test_openUrl_bitwardenSchemeOnly_failSilently() async throws {
|
|
await subject.openUrl(.bitwardenSchemeOnly)
|
|
|
|
XCTAssertEqual(coordinator.alertShown, [])
|
|
XCTAssertEqual(coordinator.routes, [])
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving an OTP deep link and setting an auth completion route on the
|
|
/// coordinator to handle routing to the vault item selection screen when the vault is unlocked.
|
|
@MainActor
|
|
func test_openUrl_otpKey_vaultLocked() async throws {
|
|
let otpKey: String = .otpAuthUriKeyComplete
|
|
|
|
try await subject.openUrl(XCTUnwrap(URL(string: otpKey)))
|
|
|
|
let model = TOTPKeyModel(authenticatorKey: otpKey)
|
|
XCTAssertEqual(coordinator.events, [.setAuthCompletionRoute(.tab(.vault(.vaultItemSelection(model))))])
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving an OTP deep link and routing to the vault item selection screen.
|
|
@MainActor
|
|
func test_openUrl_otpKey_vaultUnlocked() async throws {
|
|
let account = Account.fixture()
|
|
let otpKey: String = .otpAuthUriKeyComplete
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked[account.profile.userId] = false
|
|
|
|
try await subject.openUrl(XCTUnwrap(URL(string: otpKey)))
|
|
|
|
let model = TOTPKeyModel(authenticatorKey: otpKey)
|
|
XCTAssertEqual(coordinator.routes.last, .tab(.vault(.vaultItemSelection(model))))
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving an OTP deep link and setting an auth completion route on the
|
|
/// coordinator if the the user's vault is unlocked but will be timing out as the app is
|
|
/// foregrounded.
|
|
@MainActor
|
|
func test_openUrl_otpKey_vaultUnlockedTimeout() async throws {
|
|
let account = Account.fixture()
|
|
let otpKey: String = .otpAuthUriKeyComplete
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked[account.profile.userId] = false
|
|
vaultTimeoutService.shouldSessionTimeout[account.profile.userId] = true
|
|
|
|
try await subject.openUrl(XCTUnwrap(URL(string: otpKey)))
|
|
|
|
let model = TOTPKeyModel(authenticatorKey: otpKey)
|
|
XCTAssertEqual(coordinator.events, [.setAuthCompletionRoute(.tab(.vault(.vaultItemSelection(model))))])
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving an OTP deep link and setting an auth completion route on the
|
|
/// coordinator if the the user's vault is unlocked but will be timing out as the app is
|
|
/// foregrounded.
|
|
@MainActor
|
|
func test_openUrl_otpKey_vaultUnlockedTimeoutError() async throws {
|
|
let account = Account.fixture()
|
|
let otpKey: String = .otpAuthUriKeyComplete
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked[account.profile.userId] = false
|
|
vaultTimeoutService.shouldSessionTimeoutError = BitwardenTestError.example
|
|
|
|
try await subject.openUrl(XCTUnwrap(URL(string: otpKey)))
|
|
|
|
let model = TOTPKeyModel(authenticatorKey: otpKey)
|
|
XCTAssertEqual(coordinator.events, [.setAuthCompletionRoute(.tab(.vault(.vaultItemSelection(model))))])
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving an non OTP deep link and silently returns with a no-op.
|
|
@MainActor
|
|
func test_openUrl_nonOtpKey_failSilently() async throws {
|
|
try await subject.openUrl(XCTUnwrap(URL(string: "bitwarden://")))
|
|
|
|
XCTAssertEqual(coordinator.alertShown, [])
|
|
XCTAssertEqual(coordinator.routes, [])
|
|
}
|
|
|
|
/// `openUrl(_:)` handles receiving an OTP deep link if the URL isn't an OTP key.
|
|
@MainActor
|
|
func test_openUrl_otpKey_invalid() async throws {
|
|
let otpKey: String = .otpAuthUriKeyNoSecret
|
|
try await subject.openUrl(XCTUnwrap(URL(string: otpKey)))
|
|
|
|
XCTAssertEqual(coordinator.alertShown, [.defaultAlert(title: Localizations.anErrorHasOccurred)])
|
|
XCTAssertEqual(coordinator.routes, [])
|
|
}
|
|
|
|
/// `provideCredential(for:)` returns the credential with the specified identifier.
|
|
func test_provideCredential() async throws {
|
|
let credential = ASPasswordCredential(user: "user@bitwarden.com", password: "password123")
|
|
autofillCredentialService.provideCredentialPasswordCredential = credential
|
|
|
|
let providedCredential = try await subject.provideCredential(for: "1")
|
|
XCTAssertEqual(providedCredential.user, "user@bitwarden.com")
|
|
XCTAssertEqual(providedCredential.password, "password123")
|
|
}
|
|
|
|
/// `provideCredential(for:)` throws an error if one occurs.
|
|
func test_provideCredential_error() async throws {
|
|
autofillCredentialService.provideCredentialError = ASExtensionError(.userInteractionRequired)
|
|
|
|
await assertAsyncThrows(error: ASExtensionError(.userInteractionRequired)) {
|
|
_ = try await subject.provideCredential(for: "1")
|
|
}
|
|
}
|
|
|
|
/// `provideOTPCredential(for:repromptPasswordValidated:)` returns the credential with the specified identifier.
|
|
@available(iOS 18.0, *)
|
|
func test_provideOTPCredential() async throws {
|
|
let credential = ASOneTimeCodeCredential(code: "123")
|
|
autofillCredentialService.provideOTPCredentialResult = .success(credential)
|
|
|
|
let providedCredential = try await subject.provideOTPCredential(for: "1")
|
|
XCTAssertEqual(providedCredential.code, "123")
|
|
}
|
|
|
|
/// `provideOTPCredential(for:repromptPasswordValidated:)` throws an error if one occurs.
|
|
@available(iOS 18.0, *)
|
|
func test_provideOTPCredential_error() async throws {
|
|
autofillCredentialService.provideOTPCredentialResult = .failure(ASExtensionError(.userInteractionRequired))
|
|
|
|
await assertAsyncThrows(error: ASExtensionError(.userInteractionRequired)) {
|
|
_ = try await subject.provideOTPCredential(for: "1")
|
|
}
|
|
}
|
|
|
|
/// `repromptForCredentialIfNecessary(for:)` reprompts the user for their master password if
|
|
/// reprompt is enabled for the cipher.
|
|
@MainActor
|
|
func test_repromptForCredentialIfNecessary() throws {
|
|
vaultRepository.repromptRequiredForCipherResult = .success(true)
|
|
|
|
var masterPasswordValidated: Bool?
|
|
let expectation = expectation(description: #function)
|
|
Task {
|
|
try await subject.repromptForCredentialIfNecessary(for: "1") { validated in
|
|
masterPasswordValidated = validated
|
|
expectation.fulfill()
|
|
}
|
|
}
|
|
waitFor(!coordinator.alertShown.isEmpty)
|
|
|
|
XCTAssertEqual(coordinator.alertShown.count, 1)
|
|
let alert = try XCTUnwrap(coordinator.alertShown.last)
|
|
XCTAssertEqual(alert, .masterPasswordPrompt { _ in })
|
|
var textField = try XCTUnwrap(alert.alertTextFields.first)
|
|
textField = AlertTextField(id: "password", text: "password")
|
|
let submitAction = try XCTUnwrap(alert.alertActions.first(where: { $0.title == Localizations.submit }))
|
|
Task {
|
|
await submitAction.handler?(submitAction, [textField])
|
|
}
|
|
|
|
waitForExpectations(timeout: 1)
|
|
|
|
XCTAssertEqual(masterPasswordValidated, true)
|
|
}
|
|
|
|
/// `repromptForCredentialIfNecessary(for:)` logs the error if one occurs.
|
|
@MainActor
|
|
func test_repromptForCredentialIfNecessary_error() throws {
|
|
authRepository.validatePasswordResult = .failure(BitwardenTestError.example)
|
|
vaultRepository.repromptRequiredForCipherResult = .success(true)
|
|
|
|
var masterPasswordValidated: Bool?
|
|
let expectation = expectation(description: #function)
|
|
Task {
|
|
try await subject.repromptForCredentialIfNecessary(for: "1") { validated in
|
|
masterPasswordValidated = validated
|
|
expectation.fulfill()
|
|
}
|
|
}
|
|
waitFor(!coordinator.alertShown.isEmpty)
|
|
|
|
XCTAssertEqual(coordinator.alertShown.count, 1)
|
|
let alert = try XCTUnwrap(coordinator.alertShown.last)
|
|
XCTAssertEqual(alert, .masterPasswordPrompt { _ in })
|
|
Task {
|
|
try await alert.tapAction(title: Localizations.submit)
|
|
}
|
|
|
|
waitForExpectations(timeout: 1)
|
|
|
|
XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [.example])
|
|
XCTAssertEqual(masterPasswordValidated, false)
|
|
}
|
|
|
|
/// `repromptForCredentialIfNecessary(for:)` displays an alert if the user enters an invalid
|
|
/// password into the master password reprompt alert.
|
|
@MainActor
|
|
func test_repromptForCredentialIfNecessary_invalidPassword() throws {
|
|
authRepository.validatePasswordResult = .success(false)
|
|
vaultRepository.repromptRequiredForCipherResult = .success(true)
|
|
|
|
var masterPasswordValidated: Bool?
|
|
let expectation = expectation(description: #function)
|
|
Task {
|
|
try await subject.repromptForCredentialIfNecessary(for: "1") { validated in
|
|
masterPasswordValidated = validated
|
|
expectation.fulfill()
|
|
}
|
|
}
|
|
waitFor(!coordinator.alertShown.isEmpty)
|
|
|
|
XCTAssertEqual(coordinator.alertShown.count, 1)
|
|
var alert = try XCTUnwrap(coordinator.alertShown.last)
|
|
Task {
|
|
try await alert.tapAction(title: Localizations.submit)
|
|
}
|
|
|
|
waitFor(coordinator.alertShown.count == 2)
|
|
alert = try XCTUnwrap(coordinator.alertShown.last)
|
|
XCTAssertEqual(alert, .defaultAlert(title: Localizations.invalidMasterPassword))
|
|
Task {
|
|
try await alert.tapAction(title: Localizations.ok)
|
|
}
|
|
coordinator.alertOnDismissed?()
|
|
|
|
waitForExpectations(timeout: 1)
|
|
|
|
XCTAssertEqual(masterPasswordValidated, false)
|
|
}
|
|
|
|
/// `repromptForCredentialIfNecessary(for:)` calls the completion handler if reprompt isn't
|
|
/// required for the cipher.
|
|
func test_repromptForCredentialIfNecessary_repromptNotRequired() async throws {
|
|
vaultRepository.repromptRequiredForCipherResult = .success(false)
|
|
|
|
var masterPasswordValidated: Bool?
|
|
try await subject.repromptForCredentialIfNecessary(for: "1") { validated in
|
|
masterPasswordValidated = validated
|
|
}
|
|
|
|
XCTAssertEqual(masterPasswordValidated, false)
|
|
}
|
|
|
|
/// `routeToLanding(_:)` navigates to show the landing view.
|
|
@MainActor
|
|
func test_routeToLanding() async {
|
|
await subject.routeToLanding()
|
|
XCTAssertEqual(coordinator.routes.last, .auth(.landing))
|
|
}
|
|
|
|
/// `showLoginRequest(_:)` navigates to show the login request view.
|
|
@MainActor
|
|
func test_showLoginRequest() {
|
|
subject.showLoginRequest(.fixture())
|
|
XCTAssertEqual(coordinator.routes.last, .loginRequest(.fixture()))
|
|
}
|
|
|
|
/// `start(navigator:)` builds the AppCoordinator and navigates to the initial route if provided.
|
|
@MainActor
|
|
func test_start_initialRoute() async {
|
|
let rootNavigator = MockRootNavigator()
|
|
|
|
await subject.start(
|
|
appContext: .mainApp,
|
|
initialRoute: .extensionSetup(.extensionActivation(type: .appExtension)),
|
|
navigator: rootNavigator,
|
|
window: nil,
|
|
)
|
|
|
|
waitFor(!coordinator.routes.isEmpty)
|
|
|
|
XCTAssertTrue(appModule.appCoordinator.isStarted)
|
|
XCTAssertEqual(
|
|
appModule.appCoordinator.routes,
|
|
[.extensionSetup(.extensionActivation(type: .appExtension))],
|
|
)
|
|
XCTAssertEqual(migrationService.didPerformMigrations, true)
|
|
}
|
|
|
|
/// `start(navigator:)` builds the AppCoordinator and navigates to the `.didStart` route.
|
|
@MainActor
|
|
func test_start_authRoute() async {
|
|
let rootNavigator = MockRootNavigator()
|
|
|
|
await subject.start(appContext: .mainApp, navigator: rootNavigator, window: nil)
|
|
|
|
waitFor(!coordinator.events.isEmpty)
|
|
|
|
XCTAssertTrue(appModule.appCoordinator.isStarted)
|
|
XCTAssertEqual(appModule.appCoordinator.events, [.didStart])
|
|
XCTAssertEqual(migrationService.didPerformMigrations, true)
|
|
}
|
|
|
|
/// `start(navigator:)` doesn't complete the accounts autofill setup when running in an app extension.
|
|
@MainActor
|
|
func test_start_completeAutofillAccountSetupIfEnabled_appExtension() async throws {
|
|
let delegate = MockAppExtensionDelegate()
|
|
delegate.isInAppExtension = true
|
|
var willEnterForegroundCalled = 0
|
|
let subject = AppProcessor(
|
|
appExtensionDelegate: delegate,
|
|
appModule: appModule,
|
|
debugWillEnterForeground: { willEnterForegroundCalled += 1 },
|
|
services: ServiceContainer.withMocks(
|
|
autofillCredentialService: autofillCredentialService,
|
|
configService: configService,
|
|
stateService: stateService,
|
|
),
|
|
)
|
|
try await waitForAsync { willEnterForegroundCalled == 1 }
|
|
|
|
autofillCredentialService.isAutofillCredentialsEnabled = true
|
|
stateService.activeAccount = .fixture()
|
|
stateService.accounts = [.fixture()]
|
|
stateService.accountSetupAutofill["1"] = .setUpLater
|
|
|
|
let rootNavigator = MockRootNavigator()
|
|
await subject.start(appContext: .mainApp, navigator: rootNavigator, window: nil)
|
|
|
|
XCTAssertEqual(stateService.accountSetupAutofill, ["1": .setUpLater])
|
|
}
|
|
|
|
/// `start(navigator:)` doesn't complete the accounts autofill setup if autofill is disabled.
|
|
@MainActor
|
|
func test_start_completeAutofillAccountSetupIfEnabled_autofillDisabled() async {
|
|
autofillCredentialService.isAutofillCredentialsEnabled = false
|
|
stateService.activeAccount = .fixture()
|
|
stateService.accounts = [.fixture()]
|
|
stateService.accountSetupAutofill["1"] = .setUpLater
|
|
|
|
let rootNavigator = MockRootNavigator()
|
|
await subject.start(appContext: .mainApp, navigator: rootNavigator, window: nil)
|
|
|
|
XCTAssertEqual(stateService.accountSetupAutofill, ["1": .setUpLater])
|
|
}
|
|
|
|
/// `start(navigator:)` logs an error if one occurs while updating the account's autofill setup.
|
|
@MainActor
|
|
func test_start_completeAutofillAccountSetupIfEnabled_error() async throws {
|
|
// The processor checks for autofill completion when entering the foreground. Wait for the
|
|
// initial check to finish when the test starts before continuing.
|
|
try await waitForAsync { self.willEnterForegroundCalled == 1 }
|
|
|
|
autofillCredentialService.isAutofillCredentialsEnabled = true
|
|
stateService.accounts = [.fixture()]
|
|
stateService.accountSetupAutofill["1"] = .setUpLater
|
|
stateService.accountSetupAutofillError = BitwardenTestError.example
|
|
|
|
let rootNavigator = MockRootNavigator()
|
|
await subject.start(appContext: .mainApp, navigator: rootNavigator, window: nil)
|
|
|
|
XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [.example])
|
|
XCTAssertEqual(stateService.accountSetupAutofill, ["1": .setUpLater])
|
|
}
|
|
|
|
/// `start(navigator:)` doesn't log an error if there's no accounts.
|
|
@MainActor
|
|
func test_start_completeAutofillAccountSetupIfEnabled_noAccounts() async throws {
|
|
autofillCredentialService.isAutofillCredentialsEnabled = true
|
|
|
|
let rootNavigator = MockRootNavigator()
|
|
await subject.start(appContext: .mainApp, navigator: rootNavigator, window: nil)
|
|
|
|
XCTAssertTrue(errorReporter.errors.isEmpty)
|
|
XCTAssertTrue(stateService.accountSetupAutofill.isEmpty)
|
|
}
|
|
|
|
/// `start(navigator:)` doesn't update the user's autofill setup progress if they have no
|
|
/// current progress recorded.
|
|
@MainActor
|
|
func test_start_completeAutofillAccountSetupIfEnabled_noProgress() async {
|
|
autofillCredentialService.isAutofillCredentialsEnabled = true
|
|
stateService.activeAccount = .fixture()
|
|
stateService.accounts = [.fixture()]
|
|
|
|
let rootNavigator = MockRootNavigator()
|
|
await subject.start(appContext: .mainApp, navigator: rootNavigator, window: nil)
|
|
|
|
XCTAssertTrue(stateService.accountSetupAutofill.isEmpty)
|
|
}
|
|
|
|
/// `start(navigator:)` completes the user's autofill setup progress if autofill is enabled and
|
|
/// they previously choose to set it up later.
|
|
@MainActor
|
|
func test_start_completeAutofillAccountSetupIfEnabled_success() async throws {
|
|
// The processor checks for autofill completion when entering the foreground. Wait for the
|
|
// initial check to finish when the test starts before continuing.
|
|
try await waitForAsync { self.willEnterForegroundCalled == 1 }
|
|
|
|
autofillCredentialService.isAutofillCredentialsEnabled = true
|
|
stateService.activeAccount = .fixture()
|
|
stateService.accounts = [.fixture()]
|
|
stateService.accountSetupAutofill["1"] = .setUpLater
|
|
|
|
let rootNavigator = MockRootNavigator()
|
|
await subject.start(appContext: .mainApp, navigator: rootNavigator, window: nil)
|
|
|
|
XCTAssertEqual(stateService.accountSetupAutofill, ["1": .complete])
|
|
}
|
|
|
|
/// `switchAccountsForLoginRequest(to:showAlert:)` has the coordinator switch to the specified
|
|
/// account without showing a confirmation alert.
|
|
@MainActor
|
|
func test_switchAccountsForLoginRequest() async {
|
|
await subject.switchAccountsForLoginRequest(to: .fixture(), showAlert: false)
|
|
|
|
XCTAssertEqual(coordinator.events, [.switchAccounts(userId: "1", isAutomatic: false)])
|
|
}
|
|
|
|
/// `switchAccountsForLoginRequest(to:showAlert:)` shows an alert to confirm the user wants to
|
|
/// switch to the specified account and then has the coordinator switch accounts.
|
|
@MainActor
|
|
func test_switchAccountsForLoginRequest_showAlert() async throws {
|
|
let account = Account.fixture()
|
|
await subject.switchAccountsForLoginRequest(to: account, showAlert: true)
|
|
|
|
let alert = try XCTUnwrap(coordinator.alertShown.last)
|
|
XCTAssertEqual(
|
|
alert,
|
|
.confirmation(
|
|
title: Localizations.logInRequested,
|
|
message: Localizations.loginAttemptFromXDoYouWantToSwitchToThisAccount(account.profile.email),
|
|
confirmationHandler: {},
|
|
),
|
|
)
|
|
|
|
try await alert.tapAction(title: Localizations.cancel)
|
|
XCTAssertTrue(coordinator.events.isEmpty)
|
|
|
|
try await alert.tapAction(title: Localizations.yes)
|
|
XCTAssertEqual(coordinator.events, [.switchAccounts(userId: account.profile.userId, isAutomatic: false)])
|
|
}
|
|
|
|
/// `unlockVaultWithNeverlockKey()` unlocks it calling the auth repository.
|
|
func test_unlockVaultWithNeverlockKey() async throws {
|
|
try await subject.unlockVaultWithNeverlockKey()
|
|
|
|
XCTAssertTrue(authRepository.unlockVaultWithNeverlockKeyCalled)
|
|
}
|
|
|
|
/// `unlockVaultWithNeverlockKey()` throws because auth repository call throws.
|
|
func test_unlockVaultWithNeverlockKey_throws() async throws {
|
|
authRepository.unlockVaultWithNeverlockResult = .failure(BitwardenTestError.example)
|
|
await assertAsyncThrows(error: BitwardenTestError.example) {
|
|
try await subject.unlockVaultWithNeverlockKey()
|
|
}
|
|
}
|
|
|
|
// MARK: SyncServiceDelegate
|
|
|
|
/// `onFetchSyncSucceeded(userId:)` clear the unlock user pins when it has performed sync after login
|
|
/// for the first time and `.removeUnlockWithPin` policy is enabled.
|
|
func test_onFetchSyncSucceeded_clearPins() async throws {
|
|
await stateService.addAccount(.fixture())
|
|
stateService.pinProtectedUserKeyValue["1"] = "pin"
|
|
stateService.encryptedPinByUserId["1"] = "encPin"
|
|
stateService.accountVolatileData["1"] = AccountVolatileData(pinProtectedUserKey: "pin")
|
|
stateService.hasPerformedSyncAfterLogin["1"] = false
|
|
policyService.policyAppliesToUserResult[.removeUnlockWithPin] = true
|
|
|
|
await subject.onFetchSyncSucceeded(userId: "1")
|
|
|
|
XCTAssertNil(stateService.pinProtectedUserKeyValue["1"])
|
|
XCTAssertNil(stateService.encryptedPinByUserId["1"])
|
|
XCTAssertNil(stateService.accountVolatileData["1"])
|
|
}
|
|
|
|
/// `onFetchSyncSucceeded(userId:)` doesn't clear the unlock user pins when it has performed sync after login
|
|
/// for the first time and `.removeUnlockWithPin` policy is disabled.
|
|
func test_onFetchSyncSucceeded_doesNotClearPinsWhenRemoveUnlockWithPinPolicyDisabled() async throws {
|
|
await stateService.addAccount(.fixture())
|
|
stateService.pinProtectedUserKeyValue["1"] = "pin"
|
|
stateService.encryptedPinByUserId["1"] = "encPin"
|
|
stateService.accountVolatileData["1"] = AccountVolatileData(pinProtectedUserKey: "pin")
|
|
stateService.hasPerformedSyncAfterLogin["1"] = false
|
|
policyService.policyAppliesToUserResult[.removeUnlockWithPin] = false
|
|
|
|
await subject.onFetchSyncSucceeded(userId: "1")
|
|
|
|
XCTAssertNotNil(stateService.pinProtectedUserKeyValue["1"])
|
|
XCTAssertNotNil(stateService.encryptedPinByUserId["1"])
|
|
XCTAssertNotNil(stateService.accountVolatileData["1"])
|
|
XCTAssertTrue(stateService.hasPerformedSyncAfterLogin["1"] == true)
|
|
}
|
|
|
|
/// `onFetchSyncSucceeded(userId:)` doesn't clear the unlock user pins when it's not the first time it has
|
|
/// performed sync after login.
|
|
func test_onFetchSyncSucceeded_doesNotClearPinsWhenNotFirstTimeSyncAfterLogin() async throws {
|
|
await stateService.addAccount(.fixture())
|
|
stateService.pinProtectedUserKeyValue["1"] = "pin"
|
|
stateService.encryptedPinByUserId["1"] = "encPin"
|
|
stateService.accountVolatileData["1"] = AccountVolatileData(pinProtectedUserKey: "pin")
|
|
stateService.hasPerformedSyncAfterLogin["1"] = true
|
|
|
|
await subject.onFetchSyncSucceeded(userId: "1")
|
|
|
|
XCTAssertTrue(policyService.policyAppliesToUserPolicies.isEmpty)
|
|
XCTAssertNotNil(stateService.pinProtectedUserKeyValue["1"])
|
|
XCTAssertNotNil(stateService.encryptedPinByUserId["1"])
|
|
XCTAssertNotNil(stateService.accountVolatileData["1"])
|
|
}
|
|
|
|
/// `onFetchSyncSucceeded(userId:)` doesn't do anything when `getHasPerformedSyncAfterLogin(userId:)` throws.
|
|
func test_onFetchSyncSucceeded_getHasPerformedSyncAfterLoginThrows() async throws {
|
|
stateService.pinProtectedUserKeyValue["1"] = "pin"
|
|
stateService.encryptedPinByUserId["1"] = "encPin"
|
|
stateService.accountVolatileData["1"] = AccountVolatileData(pinProtectedUserKey: "pin")
|
|
stateService.getHasPerformedSyncAfterLoginError = BitwardenTestError.example
|
|
|
|
await subject.onFetchSyncSucceeded(userId: "1")
|
|
|
|
XCTAssertTrue(policyService.policyAppliesToUserPolicies.isEmpty)
|
|
XCTAssertNotNil(stateService.pinProtectedUserKeyValue["1"])
|
|
XCTAssertNotNil(stateService.encryptedPinByUserId["1"])
|
|
XCTAssertNotNil(stateService.accountVolatileData["1"])
|
|
XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [.example])
|
|
}
|
|
|
|
/// `onFetchSyncSucceeded(userId:)` doesn't do anything when
|
|
/// `setHasPerformedSyncAfterLogin(hasBeenPerformed:, userId:)` throws.
|
|
func test_onFetchSyncSucceeded_setHasPerformedSyncAfterLoginThrows() async throws {
|
|
stateService.pinProtectedUserKeyValue["1"] = "pin"
|
|
stateService.encryptedPinByUserId["1"] = "encPin"
|
|
stateService.accountVolatileData["1"] = AccountVolatileData(pinProtectedUserKey: "pin")
|
|
stateService.setHasPerformedSyncAfterLoginError = BitwardenTestError.example
|
|
|
|
await subject.onFetchSyncSucceeded(userId: "1")
|
|
|
|
XCTAssertTrue(policyService.policyAppliesToUserPolicies.isEmpty)
|
|
XCTAssertNotNil(stateService.pinProtectedUserKeyValue["1"])
|
|
XCTAssertNotNil(stateService.encryptedPinByUserId["1"])
|
|
XCTAssertNotNil(stateService.accountVolatileData["1"])
|
|
XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [.example])
|
|
}
|
|
|
|
/// `onFetchSyncSucceeded(userId:)` doesn't clear pins when
|
|
/// `clearPins(userId:)` throws.
|
|
func test_onFetchSyncSucceeded_clearPinsThrows() async throws {
|
|
stateService.pinProtectedUserKeyValue["1"] = "pin"
|
|
stateService.encryptedPinByUserId["1"] = "encPin"
|
|
stateService.accountVolatileData["1"] = AccountVolatileData(pinProtectedUserKey: "pin")
|
|
stateService.hasPerformedSyncAfterLogin["1"] = false
|
|
policyService.policyAppliesToUserResult[.removeUnlockWithPin] = true
|
|
stateService.activeAccount = nil
|
|
|
|
await subject.onFetchSyncSucceeded(userId: "1")
|
|
|
|
XCTAssertNotNil(stateService.pinProtectedUserKeyValue["1"])
|
|
XCTAssertNotNil(stateService.encryptedPinByUserId["1"])
|
|
XCTAssertNotNil(stateService.accountVolatileData["1"])
|
|
XCTAssertEqual(errorReporter.errors as? [StateServiceError], [.noActiveAccount])
|
|
}
|
|
|
|
/// `removeMasterPassword(organizationName:)` notifies the coordinator to show the remove
|
|
/// master password screen.
|
|
@MainActor
|
|
func test_removeMasterPassword() {
|
|
coordinator.isLoadingOverlayShowing = true
|
|
|
|
subject.removeMasterPassword(
|
|
organizationName: "Example Org",
|
|
organizationId: "ORG_ID",
|
|
keyConnectorUrl: "https://example.com",
|
|
)
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(
|
|
coordinator.routes,
|
|
[
|
|
.auth(.removeMasterPassword(
|
|
organizationName: "Example Org",
|
|
organizationId: "ORG_ID",
|
|
keyConnectorUrl: "https://example.com",
|
|
)),
|
|
],
|
|
)
|
|
}
|
|
|
|
/// `removeMasterPassword(organizationName:)` doesn't show the remove master password screen in
|
|
/// the extension.
|
|
@MainActor
|
|
func test_removeMasterPassword_extension() {
|
|
let delegate = MockAppExtensionDelegate()
|
|
let subject = AppProcessor(
|
|
appExtensionDelegate: delegate,
|
|
appModule: appModule,
|
|
services: ServiceContainer.withMocks(),
|
|
)
|
|
|
|
subject.removeMasterPassword(
|
|
organizationName: "Example Org",
|
|
organizationId: "ORG_ID",
|
|
keyConnectorUrl: "https://example.com",
|
|
)
|
|
|
|
XCTAssertTrue(coordinator.routes.isEmpty)
|
|
}
|
|
|
|
/// `securityStampChanged(userId:)` logs the user out and notifies the coordinator.
|
|
@MainActor
|
|
func test_securityStampChanged() async {
|
|
coordinator.isLoadingOverlayShowing = true
|
|
|
|
await subject.securityStampChanged(userId: "1")
|
|
|
|
XCTAssertTrue(authRepository.logoutCalled)
|
|
XCTAssertEqual(authRepository.logoutUserId, "1")
|
|
XCTAssertFalse(authRepository.logoutUserInitiated)
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.events, [.didLogout(userId: "1", userInitiated: false)])
|
|
}
|
|
|
|
/// `securityStampChanged(userId:)` throws logging the user out which is logged and notifies the coordinator.
|
|
@MainActor
|
|
func test_securityStampChanged_throwsLogging() async {
|
|
coordinator.isLoadingOverlayShowing = true
|
|
authRepository.logoutResult = .failure(BitwardenTestError.example)
|
|
|
|
await subject.securityStampChanged(userId: "1")
|
|
|
|
XCTAssertTrue(authRepository.logoutCalled)
|
|
XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [.example])
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.events, [.didLogout(userId: "1", userInitiated: false)])
|
|
}
|
|
|
|
/// `onRefreshTokenError(error:)` logs the user out and notifies the coordinator when a 401 is
|
|
/// received while refreshing the token.
|
|
@MainActor
|
|
func test_onRefreshTokenError_logOut401() async throws {
|
|
coordinator.isLoadingOverlayShowing = true
|
|
|
|
try await subject.onRefreshTokenError(error: ResponseValidationError(response: .failure(statusCode: 401)))
|
|
|
|
XCTAssertTrue(authRepository.logoutCalled)
|
|
XCTAssertEqual(authRepository.logoutUserId, nil)
|
|
XCTAssertFalse(authRepository.logoutUserInitiated)
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.events, [.didLogout(userId: nil, userInitiated: false)])
|
|
}
|
|
|
|
/// `onRefreshTokenError(error:)` logs the user out and notifies the coordinator a 403 is
|
|
/// received while refreshing the token.
|
|
@MainActor
|
|
func test_onRefreshTokenError_logOut403() async throws {
|
|
coordinator.isLoadingOverlayShowing = true
|
|
|
|
try await subject.onRefreshTokenError(error: ResponseValidationError(response: .failure(statusCode: 403)))
|
|
|
|
XCTAssertTrue(authRepository.logoutCalled)
|
|
XCTAssertEqual(authRepository.logoutUserId, nil)
|
|
XCTAssertFalse(authRepository.logoutUserInitiated)
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.events, [.didLogout(userId: nil, userInitiated: false)])
|
|
}
|
|
|
|
/// `onRefreshTokenError(error:)` logs the user out and notifies the coordinator when error is `.invalidGrant`.
|
|
@MainActor
|
|
func test_onRefreshTokenError_logOutInvalidGrant() async throws {
|
|
coordinator.isLoadingOverlayShowing = true
|
|
|
|
try await subject.onRefreshTokenError(error: IdentityTokenRefreshRequestError.invalidGrant)
|
|
|
|
XCTAssertTrue(authRepository.logoutCalled)
|
|
XCTAssertEqual(authRepository.logoutUserId, nil)
|
|
XCTAssertFalse(authRepository.logoutUserInitiated)
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.events, [.didLogout(userId: nil, userInitiated: false)])
|
|
}
|
|
|
|
/// `onRefreshTokenError(error:)` throws logging the user out which is logged and notifies the coordinator
|
|
/// when error is `.invalidGrant`.
|
|
@MainActor
|
|
func test_onRefreshTokenError_logOutInvalidGrantThrowsLogging() async throws {
|
|
coordinator.isLoadingOverlayShowing = true
|
|
authRepository.logoutResult = .failure(BitwardenTestError.example)
|
|
|
|
try await subject.onRefreshTokenError(error: IdentityTokenRefreshRequestError.invalidGrant)
|
|
|
|
XCTAssertTrue(authRepository.logoutCalled)
|
|
XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [.example])
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.events, [.didLogout(userId: nil, userInitiated: false)])
|
|
}
|
|
|
|
/// `onRefreshTokenError(error:)` doesn't perform log out when error is not `.invalidGrant`.
|
|
@MainActor
|
|
func test_onRefreshTokenError_notInvalidGrant() async throws {
|
|
coordinator.isLoadingOverlayShowing = true
|
|
|
|
try await subject.onRefreshTokenError(error: BitwardenTestError.example)
|
|
|
|
XCTAssertFalse(authRepository.logoutCalled)
|
|
XCTAssertTrue(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertTrue(coordinator.events.isEmpty)
|
|
}
|
|
|
|
/// `prepareEnvironmentConfig()` loads the URLs for the active account and the configuration.
|
|
@MainActor
|
|
func test_prepareEnvironmentConfig() async throws {
|
|
await subject.prepareEnvironmentConfig()
|
|
XCTAssertTrue(environmentService.didLoadURLsForActiveAccount)
|
|
XCTAssertTrue(configService.configMocker.called)
|
|
}
|
|
}
|