mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-11 03:37:34 -06:00
452 lines
17 KiB
Swift
452 lines
17 KiB
Swift
import BitwardenKit
|
|
import BitwardenKitMocks
|
|
import BitwardenResources
|
|
import SwiftUI
|
|
import XCTest
|
|
|
|
@testable import BitwardenShared
|
|
|
|
class SettingsCoordinatorTests: BitwardenTestCase { // swiftlint:disable:this type_body_length
|
|
// MARK: Properties
|
|
|
|
var configService: MockConfigService!
|
|
var delegate: MockSettingsCoordinatorDelegate!
|
|
var module: MockAppModule!
|
|
var stackNavigator: MockStackNavigator!
|
|
var subject: SettingsCoordinator!
|
|
|
|
// MARK: Setup & Teardown
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
|
|
configService = MockConfigService()
|
|
delegate = MockSettingsCoordinatorDelegate()
|
|
module = MockAppModule()
|
|
stackNavigator = MockStackNavigator()
|
|
|
|
subject = SettingsCoordinator(
|
|
delegate: delegate,
|
|
module: module,
|
|
services: ServiceContainer.withMocks(
|
|
configService: configService,
|
|
),
|
|
stackNavigator: stackNavigator,
|
|
)
|
|
}
|
|
|
|
override func tearDown() {
|
|
super.tearDown()
|
|
|
|
configService = nil
|
|
delegate = nil
|
|
module = nil
|
|
stackNavigator = nil
|
|
subject = nil
|
|
}
|
|
|
|
// MARK: Tests
|
|
|
|
/// `didCompleteLoginsImport()` notifies the delegate that the user completed importing their
|
|
/// logins and dismisses the import logins flow.
|
|
@MainActor
|
|
func test_didCompleteLoginsImport() throws {
|
|
subject.didCompleteLoginsImport()
|
|
|
|
XCTAssertTrue(delegate.didCompleteLoginsImportCalled)
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .dismissedWithCompletionHandler)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.about` pushes the about view onto the stack navigator.
|
|
@MainActor
|
|
func test_navigateTo_about() throws {
|
|
subject.navigate(to: .about)
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .pushed)
|
|
XCTAssertTrue(action.view is UIHostingController<AboutView>)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.accountSecurity` pushes the account security view onto the stack navigator.
|
|
@MainActor
|
|
func test_navigateTo_accountSecurity() throws {
|
|
subject.navigate(to: .accountSecurity)
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .pushed)
|
|
XCTAssertTrue(action.view is UIHostingController<AccountSecurityView>)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.addEditFolder` starts the add/edit folder coordinator and navigates
|
|
/// to the add/edit folder view.
|
|
@MainActor
|
|
func test_navigateTo_addEditFolder() throws {
|
|
subject.navigate(to: .addEditFolder(folder: nil))
|
|
|
|
XCTAssertTrue(module.addEditFolderCoordinator.isStarted)
|
|
XCTAssertEqual(module.addEditFolderCoordinator.routes, [.addEditFolder(folder: nil)])
|
|
}
|
|
|
|
/// `navigate(to:)` with `.alert` has the stack navigator present the alert.
|
|
@MainActor
|
|
func test_navigateTo_alert() throws {
|
|
let alert = Alert.defaultAlert(
|
|
title: Localizations.anErrorHasOccurred,
|
|
message: Localizations.genericErrorMessage,
|
|
)
|
|
subject.showAlert(alert)
|
|
|
|
XCTAssertEqual(stackNavigator.alerts, [alert])
|
|
}
|
|
|
|
/// `navigate(to:)` with `.appearance` pushes the appearance view onto the stack navigator.
|
|
@MainActor
|
|
func test_navigateTo_appearance() throws {
|
|
subject.navigate(to: .appearance)
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .pushed)
|
|
XCTAssertTrue(action.view is UIHostingController<AppearanceView>)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.appExtension` pushes the app extension view onto the stack navigator.
|
|
@MainActor
|
|
func test_navigateTo_appExtension() throws {
|
|
subject.navigate(to: .appExtension)
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .pushed)
|
|
XCTAssertTrue(action.view is UIHostingController<AppExtensionView>)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.appExtensionSetup` pushes the app extension view onto the stack navigator.
|
|
@MainActor
|
|
func test_navigateTo_appExtensionSetup() throws {
|
|
subject.navigate(to: .appExtensionSetup)
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .presented)
|
|
XCTAssertTrue(action.view is UIActivityViewController)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.autoFill` pushes the auto-fill view onto the stack navigator.
|
|
@MainActor
|
|
func test_navigateTo_autoFill() throws {
|
|
subject.navigate(to: .autoFill)
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .pushed)
|
|
XCTAssertTrue(action.view is UIHostingController<AutoFillView>)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.deleteAccount` presents the delete account view.
|
|
@MainActor
|
|
func test_navigateTo_deleteAccount() throws {
|
|
subject.navigate(to: .deleteAccount)
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .presented)
|
|
XCTAssertTrue(action.view is DeleteAccountView)
|
|
XCTAssertEqual(action.embedInNavigationController, true)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.didDeleteAccount(otherAccounts:)` calls the delegate method
|
|
/// that performs navigation post-deletion.
|
|
@MainActor
|
|
func test_navigateTo_didDeleteAccount() throws {
|
|
let task = Task {
|
|
await subject.handleEvent(.didDeleteAccount)
|
|
}
|
|
|
|
waitFor(!stackNavigator.actions.isEmpty)
|
|
task.cancel()
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .dismissedWithCompletionHandler)
|
|
XCTAssertTrue(delegate.didDeleteAccountCalled)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.dismiss` dismisses the view.
|
|
@MainActor
|
|
func test_navigate_dismiss() throws {
|
|
subject.navigate(to: .dismiss)
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .dismissed)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.exportVault` presents the export vault to file view when
|
|
/// Credential Exchange flag to export is disabled.
|
|
@MainActor
|
|
func test_navigateTo_exportVaultCXPDisabled() async throws {
|
|
configService.featureFlagsBool[.cxpExportMobile] = false
|
|
let task = Task {
|
|
subject.navigate(to: .exportVault)
|
|
}
|
|
defer { task.cancel() }
|
|
|
|
try await waitForAsync { [weak self] in
|
|
guard let self else { return true }
|
|
return stackNavigator.actions.last?.view is ExportVaultView
|
|
}
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .presented)
|
|
XCTAssertTrue(action.view is ExportVaultView)
|
|
XCTAssertEqual(action.embedInNavigationController, true)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.exportVault` presents the export settings view when
|
|
/// Credential Exchange flag to export is enabled.
|
|
@MainActor
|
|
func test_navigateTo_exportVaultCXPEnabled() async throws {
|
|
configService.featureFlagsBool[.cxpExportMobile] = true
|
|
let task = Task {
|
|
subject.navigate(to: .exportVault)
|
|
}
|
|
defer { task.cancel() }
|
|
|
|
try await waitForAsync { [weak self] in
|
|
guard let self else { return true }
|
|
return stackNavigator.actions.last != nil
|
|
}
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .pushed)
|
|
XCTAssertTrue(action.view is UIHostingController<ExportSettingsView>)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.exportVaultToFile` presents the export vault to file view.
|
|
@MainActor
|
|
func test_navigateTo_exportVaultToFile() throws {
|
|
subject.navigate(to: .exportVaultToFile)
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .presented)
|
|
XCTAssertTrue(action.view is ExportVaultView)
|
|
XCTAssertEqual(action.embedInNavigationController, true)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.exportVaultToApp` presents the export vault
|
|
/// to another app view (Credential Exchange flow) by starting its coordinator.
|
|
@MainActor
|
|
func test_navigateTo_exportVaultToApp() throws {
|
|
subject.navigate(to: .exportVaultToApp)
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .presented)
|
|
XCTAssertTrue(action.view is UINavigationController)
|
|
XCTAssertTrue(module.exportCXFCoordinator.isStarted)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.importLogins` presents the import logins flow.
|
|
@MainActor
|
|
func test_navigateTo_importLogins() throws {
|
|
subject.navigate(to: .importLogins)
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .presented)
|
|
XCTAssertTrue(action.view is UINavigationController)
|
|
XCTAssertTrue(module.importLoginsCoordinator.isStarted)
|
|
XCTAssertEqual(module.importLoginsCoordinator.routes.last, .importLogins(.settings))
|
|
}
|
|
|
|
/// `navigate(to:)` with `.flightRecorder` starts flight recorder coordinator and navigates to
|
|
/// the enable flight recorder view.
|
|
@MainActor
|
|
func test_navigateTo_flightRecorder() throws {
|
|
subject.navigate(to: .flightRecorder(.enableFlightRecorder))
|
|
|
|
XCTAssertTrue(module.flightRecorderCoordinator.isStarted)
|
|
XCTAssertEqual(module.flightRecorderCoordinator.routes.last, .enableFlightRecorder)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.lockVault` navigates the user to the login view.
|
|
@MainActor
|
|
func test_navigateTo_lockVault() async throws {
|
|
await subject.handleEvent(.authAction(.lockVault(userId: "")))
|
|
|
|
XCTAssertTrue(delegate.didLockVaultCalled)
|
|
XCTAssertFalse(delegate.hasManuallyLockedVault)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.lockVault` calls the delegate to handle locking vault
|
|
/// on manually locked.
|
|
@MainActor
|
|
func test_navigateTo_lockVaultManually() async throws {
|
|
await subject.handleEvent(.authAction(.lockVault(userId: "", isManuallyLocking: true)))
|
|
|
|
XCTAssertTrue(delegate.didLockVaultCalled)
|
|
XCTAssertTrue(delegate.hasManuallyLockedVault)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.loginRequest` pushes the login request view onto the stack navigator.
|
|
@MainActor
|
|
func test_navigateTo_loginRequest() throws {
|
|
subject.navigate(to: .loginRequest(.fixture()))
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .presented)
|
|
XCTAssertTrue(action.view is UINavigationController)
|
|
|
|
XCTAssertTrue(module.loginRequestCoordinator.isStarted)
|
|
XCTAssertEqual(module.loginRequestCoordinator.routes.last, .loginRequest(.fixture()))
|
|
}
|
|
|
|
/// `navigate(to:)` with `.logout` informs the delegate that the user logged out.
|
|
@MainActor
|
|
func test_navigateTo_logout_userInitiated() throws {
|
|
let task = Task {
|
|
await subject.handleEvent(.authAction(.logout(userId: "123", userInitiated: true)))
|
|
}
|
|
|
|
waitFor(delegate.didLogoutCalled)
|
|
task.cancel()
|
|
let userInitiated = try XCTUnwrap(delegate.wasLogoutUserInitiated)
|
|
XCTAssertTrue(userInitiated)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.logout` informs the delegate that the user logged out.
|
|
@MainActor
|
|
func test_navigateTo_logout_systemInitiated() throws {
|
|
let task = Task {
|
|
await subject.handleEvent(.authAction(.logout(userId: "123", userInitiated: false)))
|
|
}
|
|
|
|
waitFor(delegate.didLogoutCalled)
|
|
task.cancel()
|
|
let userInitiated = try XCTUnwrap(delegate.wasLogoutUserInitiated)
|
|
XCTAssertFalse(userInitiated)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.folders` pushes the folders view onto the stack navigator.
|
|
@MainActor
|
|
func test_navigateTo_folders() throws {
|
|
subject.navigate(to: .folders)
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .pushed)
|
|
XCTAssertTrue(action.view is UIHostingController<FoldersView>)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.other` pushes the other view onto the stack navigator.
|
|
@MainActor
|
|
func test_navigateTo_other() throws {
|
|
subject.navigate(to: .other)
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .pushed)
|
|
XCTAssertTrue(action.view is UIHostingController<OtherSettingsView>)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.passwordAutoFill` pushes the password auto-fill view onto the stack navigator.
|
|
@MainActor
|
|
func test_navigateTo_passwordAutoFill() throws {
|
|
subject.navigate(to: .passwordAutoFill)
|
|
|
|
XCTAssertTrue(module.passwordAutoFillCoordinator.isStarted)
|
|
XCTAssertEqual(module.passwordAutoFillCoordinator.routes, [.passwordAutofill(mode: .settings)])
|
|
XCTAssertNil(module.passwordAutoFillCoordinatorDelegate)
|
|
XCTAssertIdentical(module.passwordAutoFillCoordinatorStackNavigator, stackNavigator)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.pendingLoginRequests()` presents the pending login requests view.
|
|
@MainActor
|
|
func test_navigateTo_pendingLoginRequests() throws {
|
|
subject.navigate(to: .pendingLoginRequests)
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .presented)
|
|
XCTAssertTrue(action.view is PendingRequestsView)
|
|
XCTAssertEqual(action.embedInNavigationController, true)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.selectLanguage()` presents the select language view.
|
|
@MainActor
|
|
func test_navigateTo_selectLanguage() throws {
|
|
subject.navigate(to: .selectLanguage(currentLanguage: .default))
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .presented)
|
|
XCTAssertTrue(action.view is SelectLanguageView)
|
|
XCTAssertEqual(action.embedInNavigationController, true)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.settings` pushes the settings view onto the stack navigator.
|
|
@MainActor
|
|
func test_navigateTo_settings() throws {
|
|
subject.navigate(to: .settings(.tab))
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .replaced)
|
|
XCTAssertTrue(action.view is SettingsView)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.shareURL(_:)` presents an activity view controller to share the URL.
|
|
@MainActor
|
|
func test_navigateTo_shareURL() throws {
|
|
subject.navigate(to: .shareURL(.example))
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .presented)
|
|
XCTAssertTrue(action.view is UIActivityViewController)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.vault` pushes the vault settings view onto the stack navigator.
|
|
@MainActor
|
|
func test_navigateTo_vault() throws {
|
|
subject.navigate(to: .vault)
|
|
|
|
let action = try XCTUnwrap(stackNavigator.actions.last)
|
|
XCTAssertEqual(action.type, .pushed)
|
|
XCTAssertTrue(action.view is UIHostingController<VaultSettingsView>)
|
|
}
|
|
|
|
/// `navigate(to:)` with `.vaultUnlockSetup` pushes the vault unlock setup screen.
|
|
@MainActor
|
|
func test_navigateTo_vaultUnlockSetup() throws {
|
|
subject.navigate(to: .vaultUnlockSetup)
|
|
|
|
XCTAssertEqual(module.authCoordinator.routes, [.vaultUnlockSetup(.settings)])
|
|
}
|
|
|
|
/// `showLoadingOverlay()` and `hideLoadingOverlay()` can be used to show and hide the loading overlay.
|
|
@MainActor
|
|
func test_show_hide_loadingOverlay() throws {
|
|
stackNavigator.rootViewController = UIViewController()
|
|
try setKeyWindowRoot(viewController: XCTUnwrap(stackNavigator.rootViewController))
|
|
|
|
XCTAssertNil(window.viewWithTag(LoadingOverlayDisplayHelper.overlayViewTag))
|
|
|
|
subject.showLoadingOverlay(LoadingOverlayState(title: "Loading..."))
|
|
XCTAssertNotNil(window.viewWithTag(LoadingOverlayDisplayHelper.overlayViewTag))
|
|
|
|
subject.hideLoadingOverlay()
|
|
waitFor { window.viewWithTag(LoadingOverlayDisplayHelper.overlayViewTag) == nil }
|
|
XCTAssertNil(window.viewWithTag(LoadingOverlayDisplayHelper.overlayViewTag))
|
|
}
|
|
|
|
/// `start()` navigates to the settings view.
|
|
@MainActor
|
|
func test_start() {
|
|
subject.start()
|
|
|
|
XCTAssertTrue(stackNavigator.actions.last?.view is SettingsView)
|
|
}
|
|
|
|
/// `updateSettingsTabBadge(_:)` updates the badge value on the root view controller's tab bar item.
|
|
@MainActor
|
|
func test_updateSettingsTabBadge() {
|
|
stackNavigator.rootViewController = UIViewController()
|
|
|
|
subject.updateSettingsTabBadge("1")
|
|
XCTAssertEqual(stackNavigator.rootViewController?.tabBarItem.badgeValue, "1")
|
|
|
|
subject.updateSettingsTabBadge("2")
|
|
XCTAssertEqual(stackNavigator.rootViewController?.tabBarItem.badgeValue, "2")
|
|
|
|
subject.updateSettingsTabBadge(nil)
|
|
XCTAssertNil(stackNavigator.rootViewController?.tabBarItem.badgeValue)
|
|
}
|
|
} // swiftlint:disable:this file_length
|