diff --git a/PIA VPN-tvOS/Login/CompositionRoot/LoginFactory.swift b/PIA VPN-tvOS/Login/CompositionRoot/LoginFactory.swift index c8bd8fa8..f7497b8d 100644 --- a/PIA VPN-tvOS/Login/CompositionRoot/LoginFactory.swift +++ b/PIA VPN-tvOS/Login/CompositionRoot/LoginFactory.swift @@ -19,9 +19,7 @@ class LoginFactory { checkLoginAvailability: CheckLoginAvailability(), validateLoginCredentials: ValidateCredentialsFormat(), errorHandler: makeLoginViewModelErrorHandler(), - appRouter: AppRouter.shared, - successDestination: OnboardingDestinations.installVPNProfile) - + onSuccessAction: .navigate(router: AppRouter.shared, destination: OnboardingDestinations.installVPNProfile)) } private static func makeLoginWithCredentialsUseCase() -> LoginWithCredentialsUseCaseType { diff --git a/PIA VPN-tvOS/Login/Presentation/LoginViewModel.swift b/PIA VPN-tvOS/Login/Presentation/LoginViewModel.swift index d6397d8e..7dbd1bb6 100644 --- a/PIA VPN-tvOS/Login/Presentation/LoginViewModel.swift +++ b/PIA VPN-tvOS/Login/Presentation/LoginViewModel.swift @@ -13,21 +13,18 @@ class LoginViewModel: ObservableObject { private let checkLoginAvailability: CheckLoginAvailabilityType private let validateLoginCredentials: ValidateCredentialsFormatType private let errorHandler: LoginViewModelErrorHandlerType - private let appRouter: AppRouterType - - private let successDestination: any Destinations + private let onSuccessAction: AppRouter.Actions @Published var isAccountExpired = false @Published var shouldShowErrorMessage = false @Published var loginStatus: LoginStatus = .none - init(loginWithCredentialsUseCase: LoginWithCredentialsUseCaseType, checkLoginAvailability: CheckLoginAvailabilityType, validateLoginCredentials: ValidateCredentialsFormatType, errorHandler: LoginViewModelErrorHandlerType, appRouter: AppRouterType, successDestination: any Destinations) { + init(loginWithCredentialsUseCase: LoginWithCredentialsUseCaseType, checkLoginAvailability: CheckLoginAvailabilityType, validateLoginCredentials: ValidateCredentialsFormatType, errorHandler: LoginViewModelErrorHandlerType, onSuccessAction: AppRouter.Actions) { self.loginWithCredentialsUseCase = loginWithCredentialsUseCase self.checkLoginAvailability = checkLoginAvailability self.validateLoginCredentials = validateLoginCredentials self.errorHandler = errorHandler - self.appRouter = appRouter - self.successDestination = successDestination + self.onSuccessAction = onSuccessAction } func login(username: String, password: String) { @@ -54,7 +51,7 @@ class LoginViewModel: ObservableObject { case .success(let userAccount): Task { @MainActor in self.loginStatus = .succeeded(userAccount: userAccount) - self.appRouter.navigate(to: self.successDestination) + self.onSuccessAction() } case .failure(let error): diff --git a/PIA VPN-tvOS/Navigation/AppRouter.swift b/PIA VPN-tvOS/Navigation/AppRouter.swift index 9b063572..0b06d850 100644 --- a/PIA VPN-tvOS/Navigation/AppRouter.swift +++ b/PIA VPN-tvOS/Navigation/AppRouter.swift @@ -11,6 +11,7 @@ protocol AppRouterType { func goBackToRoot() } + // AppRouter enables any component to navigate the user to any screen defined within Destinations class AppRouter: ObservableObject, AppRouterType { @@ -38,19 +39,17 @@ class AppRouter: ObservableObject, AppRouterType { func goBackToRoot() { path.removeLast(path.count) } - } extension AppRouter { enum Actions: Equatable { - case pop(router: AppRouterType) case goBackToRoot(router: AppRouterType) case navigate(router: AppRouterType, destination: any Destinations) - func execute() { + func callAsFunction() { switch self { case .pop(let router): router.pop() @@ -60,7 +59,7 @@ extension AppRouter { router.navigate(to: destination) } } - + static func == (lhs: AppRouter.Actions, rhs: AppRouter.Actions) -> Bool { switch (lhs, rhs) { case (.pop, .pop): @@ -74,5 +73,4 @@ extension AppRouter { } } - } diff --git a/PIA VPN-tvOS/Navigation/Destinations.swift b/PIA VPN-tvOS/Navigation/Destinations.swift index 608410d8..0a5b5c6f 100644 --- a/PIA VPN-tvOS/Navigation/Destinations.swift +++ b/PIA VPN-tvOS/Navigation/Destinations.swift @@ -10,7 +10,6 @@ import Foundation typealias Destinations = Hashable - enum DashboardDestinations: Destinations { case home } diff --git a/PIA VPN-tvOS/Navigation/Routes.swift b/PIA VPN-tvOS/Navigation/Routes.swift index b4890ed8..f5f78707 100644 --- a/PIA VPN-tvOS/Navigation/Routes.swift +++ b/PIA VPN-tvOS/Navigation/Routes.swift @@ -15,7 +15,6 @@ enum AuthenticationDestinations: Destinations { enum OnboardingDestinations: Destinations { case installVPNProfile - case dashboard } public extension View { @@ -33,8 +32,6 @@ public extension View { switch destination { case .installVPNProfile: VPNConfigurationInstallingFactory.makeVPNConfigurationInstallingView() - case .dashboard: - UserActivatedContainerFactory.makeUSerActivatedContainerView() } } } diff --git a/PIA VPN-tvOS/RegionsSelection/Presentation/RegionsContainerViewModel.swift b/PIA VPN-tvOS/RegionsSelection/Presentation/RegionsContainerViewModel.swift index ca6e0bf9..3894ce0a 100644 --- a/PIA VPN-tvOS/RegionsSelection/Presentation/RegionsContainerViewModel.swift +++ b/PIA VPN-tvOS/RegionsSelection/Presentation/RegionsContainerViewModel.swift @@ -43,7 +43,7 @@ class RegionsContainerViewModel: ObservableObject { func navigate(to route: RegionSelectionSideMenuItems) { if route == .search { - onSearchSelectedAction.execute() + onSearchSelectedAction() } else { selectedSideMenuItem = route } diff --git a/PIA VPN-tvOS/RegionsSelection/Presentation/RegionsListViewModel.swift b/PIA VPN-tvOS/RegionsSelection/Presentation/RegionsListViewModel.swift index 7d61bc3b..2a754737 100644 --- a/PIA VPN-tvOS/RegionsSelection/Presentation/RegionsListViewModel.swift +++ b/PIA VPN-tvOS/RegionsSelection/Presentation/RegionsListViewModel.swift @@ -46,6 +46,6 @@ class RegionsListViewModel: ObservableObject { func didSelectRegionServer(_ server: ServerType) { useCase.select(server: server) - onServerSelectedRouterAction.execute() + onServerSelectedRouterAction() } } diff --git a/PIA VPN-tvOS/RootContainer/CompositionRoot/RootContainerFactory.swift b/PIA VPN-tvOS/RootContainer/CompositionRoot/RootContainerFactory.swift index dc56712e..c2ad1bdf 100644 --- a/PIA VPN-tvOS/RootContainer/CompositionRoot/RootContainerFactory.swift +++ b/PIA VPN-tvOS/RootContainer/CompositionRoot/RootContainerFactory.swift @@ -11,9 +11,10 @@ class RootContainerFactory { guard let defaultAccountProvider = Client.providers.accountProvider as? DefaultAccountProvider else { fatalError("Incorrect account provider type") } - return RootContainerViewModel(accountProvider: defaultAccountProvider, + return RootContainerViewModel(accountProvider: defaultAccountProvider, vpnConfigurationAvailability: VPNConfigurationAvailability(), bootstrap: BootstraperFactory.makeBootstrapper(), - userAuthenticationStatusMonitor: StateMonitorsFactory.makeUserAuthenticationStatusMonitor()) + userAuthenticationStatusMonitor: StateMonitorsFactory.makeUserAuthenticationStatusMonitor(), + appRouter: AppRouterFactory.makeAppRouter()) } } diff --git a/PIA VPN-tvOS/RootContainer/Presentation/RootContainerViewModel.swift b/PIA VPN-tvOS/RootContainer/Presentation/RootContainerViewModel.swift index 06a17590..55803528 100644 --- a/PIA VPN-tvOS/RootContainer/Presentation/RootContainerViewModel.swift +++ b/PIA VPN-tvOS/RootContainer/Presentation/RootContainerViewModel.swift @@ -20,30 +20,29 @@ class RootContainerViewModel: ObservableObject { private let vpnConfigurationAvailability: VPNConfigurationAvailabilityType private let bootstrap: BootstraperType private let userAuthenticationStatusMonitor: UserAuthenticationStatusMonitorType + private let appRouter: AppRouterType private var cancellables = Set() - init(accountProvider: AccountProviderType, notificationCenter: NotificationCenterType = NotificationCenter.default, vpnConfigurationAvailability: VPNConfigurationAvailabilityType, bootstrap: BootstraperType, userAuthenticationStatusMonitor: UserAuthenticationStatusMonitorType) { + init(accountProvider: AccountProviderType, notificationCenter: NotificationCenterType = NotificationCenter.default, vpnConfigurationAvailability: VPNConfigurationAvailabilityType, bootstrap: BootstraperType, userAuthenticationStatusMonitor: UserAuthenticationStatusMonitorType, appRouter: AppRouterType) { self.accountProvider = accountProvider self.notificationCenter = notificationCenter self.vpnConfigurationAvailability = vpnConfigurationAvailability self.bootstrap = bootstrap self.userAuthenticationStatusMonitor = userAuthenticationStatusMonitor - updateState() + self.appRouter = appRouter + subscribeToAccountUpdates() + setup() } - deinit { - notificationCenter.removeObserver(self) - } - - func phaseDidBecomeActive() { + private func setup() { bootstrap() isBootstrapped = true updateState() } - private func updateState() { + @objc private func updateState() { guard isBootstrapped else { return } @@ -57,12 +56,16 @@ class RootContainerViewModel: ObservableObject { // logged in, vpn profile not installed case (true, false): state = .activatedNotOnboarded + appRouter.navigate(to: OnboardingDestinations.installVPNProfile) // not logged in, any case (false, _): state = .notActivated } } - + + deinit { + notificationCenter.removeObserver(self) + } } // Combine subscriptions @@ -72,5 +75,10 @@ extension RootContainerViewModel { userAuthenticationStatusMonitor.getStatus().sink { status in self.updateState() }.store(in: &cancellables) + + notificationCenter.addObserver(self, + selector: #selector(updateState), + name: .DidInstallVPNProfile, + object: nil) } } diff --git a/PIA VPN-tvOS/RootContainer/UI/RootContainerView.swift b/PIA VPN-tvOS/RootContainer/UI/RootContainerView.swift index 64946d5f..8dcccf3c 100644 --- a/PIA VPN-tvOS/RootContainer/UI/RootContainerView.swift +++ b/PIA VPN-tvOS/RootContainer/UI/RootContainerView.swift @@ -25,16 +25,12 @@ struct RootContainerView: View { LoginFactory.makeLoginView() .withAuthenticationRoutes() .withOnboardingRoutes() - case .activatedNotOnboarded: - VPNConfigurationInstallingFactory.makeVPNConfigurationInstallingView() - .withOnboardingRoutes() - case .activated: + case .activatedNotOnboarded, .activated: UserActivatedContainerFactory.makeUSerActivatedContainerView() } }.onChange(of: scenePhase) { _, newPhase in if newPhase == .active { NSLog(">>> Active") - viewModel.phaseDidBecomeActive() } else if newPhase == .inactive { NSLog(">>> Inactive") } else if newPhase == .background { diff --git a/PIA VPN-tvOS/Shared/Utils/Foundation+Protocols.swift b/PIA VPN-tvOS/Shared/Utils/Foundation+Protocols.swift index 4eb18968..bf91769e 100644 --- a/PIA VPN-tvOS/Shared/Utils/Foundation+Protocols.swift +++ b/PIA VPN-tvOS/Shared/Utils/Foundation+Protocols.swift @@ -18,3 +18,7 @@ protocol NotificationCenterType { } extension NotificationCenter: NotificationCenterType {} + +extension Notification.Name { + public static let DidInstallVPNProfile = Notification.Name("DidInstallVPNProfile") +} diff --git a/PIA VPN-tvOSTests/VPNConfigurationInstalling/Mocks/InstallVPNConfigurationUseCaseMock.swift b/PIA VPN-tvOS/VPNConfigurationInstalling/CompositionRoot/InstallVPNConfigurationUseCaseMock.swift similarity index 100% rename from PIA VPN-tvOSTests/VPNConfigurationInstalling/Mocks/InstallVPNConfigurationUseCaseMock.swift rename to PIA VPN-tvOS/VPNConfigurationInstalling/CompositionRoot/InstallVPNConfigurationUseCaseMock.swift diff --git a/PIA VPN-tvOS/VPNConfigurationInstalling/CompositionRoot/VPNConfigurationInstallingFactory.swift b/PIA VPN-tvOS/VPNConfigurationInstalling/CompositionRoot/VPNConfigurationInstallingFactory.swift index 317aa35c..48b79716 100644 --- a/PIA VPN-tvOS/VPNConfigurationInstalling/CompositionRoot/VPNConfigurationInstallingFactory.swift +++ b/PIA VPN-tvOS/VPNConfigurationInstalling/CompositionRoot/VPNConfigurationInstallingFactory.swift @@ -25,9 +25,10 @@ class VPNConfigurationInstallingFactory { private static func makeVPNConfigurationInstallingViewModel() -> VPNConfigurationInstallingViewModel { VPNConfigurationInstallingViewModel(installVPNConfiguration: makeInstallVPNConfigurationUseCase(), - errorMapper: VPNConfigurationInstallingErrorMapper(), - appRouter: AppRouter.shared, - successDestination: OnboardingDestinations.dashboard) + errorMapper: VPNConfigurationInstallingErrorMapper()) { + AppRouter.Actions.goBackToRoot(router: AppRouter.shared)() + NotificationCenter.default.post(name: .DidInstallVPNProfile, object: nil) + } } private static func makeInstallVPNConfigurationUseCase() -> InstallVPNConfigurationUseCaseType { diff --git a/PIA VPN-tvOS/VPNConfigurationInstalling/Data/InstallVpnConfigurationProvider.swift b/PIA VPN-tvOS/VPNConfigurationInstalling/Data/InstallVpnConfigurationProvider.swift index da4eba15..7ae1a298 100644 --- a/PIA VPN-tvOS/VPNConfigurationInstalling/Data/InstallVpnConfigurationProvider.swift +++ b/PIA VPN-tvOS/VPNConfigurationInstalling/Data/InstallVpnConfigurationProvider.swift @@ -27,9 +27,8 @@ class InstallVpnConfigurationProvider: InstallVPNConfigurationUseCaseType { return } - continuation.resume() vpnConfigurationAvailability.set(value: true) - + continuation.resume() } } } diff --git a/PIA VPN-tvOS/VPNConfigurationInstalling/Presentation/VPNConfigurationInstallingViewModel.swift b/PIA VPN-tvOS/VPNConfigurationInstalling/Presentation/VPNConfigurationInstallingViewModel.swift index 340e588b..0edb3d3c 100644 --- a/PIA VPN-tvOS/VPNConfigurationInstalling/Presentation/VPNConfigurationInstallingViewModel.swift +++ b/PIA VPN-tvOS/VPNConfigurationInstalling/Presentation/VPNConfigurationInstallingViewModel.swift @@ -11,19 +11,16 @@ import Foundation class VPNConfigurationInstallingViewModel: ObservableObject { private let installVPNConfiguration: InstallVPNConfigurationUseCaseType private let errorMapper: VPNConfigurationInstallingErrorMapper - private let appRouter: AppRouterType + private let onSuccessAction: () -> Void @Published var shouldShowErrorMessage = false @Published var installingStatus: VPNConfigurationInstallingStatus = .none var errorMessage: String? - private let successDestination: any Destinations - - init(installVPNConfiguration: InstallVPNConfigurationUseCaseType, errorMapper: VPNConfigurationInstallingErrorMapper, appRouter: AppRouterType, successDestination: any Destinations) { + init(installVPNConfiguration: InstallVPNConfigurationUseCaseType, errorMapper: VPNConfigurationInstallingErrorMapper, onSuccessAction: @escaping () -> Void) { self.installVPNConfiguration = installVPNConfiguration self.errorMapper = errorMapper - self.appRouter = appRouter - self.successDestination = successDestination + self.onSuccessAction = onSuccessAction } func install() { @@ -38,7 +35,7 @@ class VPNConfigurationInstallingViewModel: ObservableObject { try await installVPNConfiguration() Task { @MainActor in installingStatus = .succeeded - appRouter.navigate(to: successDestination) + onSuccessAction() } } catch { errorMessage = errorMapper.map(error: error) diff --git a/PIA VPN-tvOSTests/Common/Mocks/AppRouterSpy.swift b/PIA VPN-tvOSTests/Common/Mocks/AppRouterSpy.swift index 194006b3..1b5f7478 100644 --- a/PIA VPN-tvOSTests/Common/Mocks/AppRouterSpy.swift +++ b/PIA VPN-tvOSTests/Common/Mocks/AppRouterSpy.swift @@ -10,6 +10,7 @@ import Foundation @testable import PIA_VPN_tvOS class AppRouterSpy: AppRouterType { + enum Request: Equatable { static func == (lhs: AppRouterSpy.Request, rhs: AppRouterSpy.Request) -> Bool { switch (lhs, rhs) { diff --git a/PIA VPN-tvOSTests/Dashboard/DashboardViewModelTests.swift b/PIA VPN-tvOSTests/Dashboard/DashboardViewModelTests.swift index ee5a7684..c649e0d4 100644 --- a/PIA VPN-tvOSTests/Dashboard/DashboardViewModelTests.swift +++ b/PIA VPN-tvOSTests/Dashboard/DashboardViewModelTests.swift @@ -14,7 +14,7 @@ import SwiftUI class DashboardViewModelTests: XCTestCase { class Fixture { let accountProviderMock = AccountProviderTypeMock() - let appRouter = AppRouter.shared + let appRouter = AppRouter(with: NavigationPath()) } var fixture: Fixture! diff --git a/PIA VPN-tvOSTests/Login/LoginIntegrationTests.swift b/PIA VPN-tvOSTests/Login/LoginIntegrationTests.swift index 7834527a..69a3feb0 100644 --- a/PIA VPN-tvOSTests/Login/LoginIntegrationTests.swift +++ b/PIA VPN-tvOSTests/Login/LoginIntegrationTests.swift @@ -32,8 +32,7 @@ final class LoginIntegrationTests: XCTestCase { checkLoginAvailability: CheckLoginAvailability(), validateLoginCredentials: ValidateCredentialsFormat(), errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()), - appRouter: appRouter, - successDestination: OnboardingDestinations.installVPNProfile) + onSuccessAction: .navigate(router: appRouter, destination: OnboardingDestinations.installVPNProfile)) var cancellables = Set() let expectation = expectation(description: "Waiting for didLoginSuccessfully property to be updated") @@ -104,9 +103,8 @@ final class LoginIntegrationTests: XCTestCase { let sut = LoginViewModel(loginWithCredentialsUseCase: loginWithCredentialsUseCase, checkLoginAvailability: CheckLoginAvailability(), validateLoginCredentials: ValidateCredentialsFormat(), - errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()), - appRouter: appRouter, - successDestination: OnboardingDestinations.installVPNProfile) + errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()), + onSuccessAction: .navigate(router: appRouter, destination: OnboardingDestinations.installVPNProfile)) var cancellables = Set() let expectation = expectation(description: "Waiting for isAccountExpired property to be updated") diff --git a/PIA VPN-tvOSTests/Login/LoginViewModelTests.swift b/PIA VPN-tvOSTests/Login/LoginViewModelTests.swift index 64f9021b..7038ac8e 100644 --- a/PIA VPN-tvOSTests/Login/LoginViewModelTests.swift +++ b/PIA VPN-tvOSTests/Login/LoginViewModelTests.swift @@ -27,8 +27,7 @@ final class LoginViewModelTests: XCTestCase { checkLoginAvailability: checkLoginAvailabilityMock, validateLoginCredentials: ValidateCredentialsFormat(), errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()), - appRouter: appRouterSpy, - successDestination: OnboardingDestinations.installVPNProfile) + onSuccessAction: .navigate(router: appRouterSpy, destination: OnboardingDestinations.installVPNProfile)) var cancellables = Set() let expectation = expectation(description: "Waiting for shouldShowErrorMessage property to be updated") @@ -72,8 +71,7 @@ final class LoginViewModelTests: XCTestCase { checkLoginAvailability: checkLoginAvailabilityMock, validateLoginCredentials: ValidateCredentialsFormat(), errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()), - appRouter: appRouterSpy, - successDestination: OnboardingDestinations.installVPNProfile) + onSuccessAction: .navigate(router: appRouterSpy, destination: OnboardingDestinations.installVPNProfile)) var cancellables = Set() let expectation = expectation(description: "Waiting for shouldShowErrorMessage property to be updated") @@ -117,8 +115,7 @@ final class LoginViewModelTests: XCTestCase { checkLoginAvailability: checkLoginAvailabilityMock, validateLoginCredentials: ValidateCredentialsFormat(), errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()), - appRouter: appRouterSpy, - successDestination: OnboardingDestinations.installVPNProfile) + onSuccessAction: .navigate(router: appRouterSpy, destination: OnboardingDestinations.installVPNProfile)) var cancellables = Set() let expectation = expectation(description: "Waiting for shouldShowErrorMessage property to be updated") @@ -163,8 +160,7 @@ final class LoginViewModelTests: XCTestCase { checkLoginAvailability: checkLoginAvailabilityMock, validateLoginCredentials: ValidateCredentialsFormat(), errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()), - appRouter: appRouterSpy, - successDestination: OnboardingDestinations.installVPNProfile) + onSuccessAction: .navigate(router: appRouterSpy, destination: OnboardingDestinations.installVPNProfile)) var cancellables = Set() let expectation = expectation(description: "Waiting for didLoginSuccessfully property to be updated") @@ -205,8 +201,7 @@ final class LoginViewModelTests: XCTestCase { checkLoginAvailability: checkLoginAvailabilityMock, validateLoginCredentials: ValidateCredentialsFormat(), errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()), - appRouter: appRouterSpy, - successDestination: OnboardingDestinations.installVPNProfile) + onSuccessAction: .navigate(router: appRouterSpy, destination: OnboardingDestinations.installVPNProfile)) var cancellables = Set() let expectation = expectation(description: "Waiting for isAccountExpired property to be updated") @@ -249,8 +244,7 @@ final class LoginViewModelTests: XCTestCase { checkLoginAvailability: checkLoginAvailabilityMock, validateLoginCredentials: ValidateCredentialsFormat(), errorHandler: LoginViewModelErrorHandler(errorMapper: LoginPresentableErrorMapper()), - appRouter: appRouterSpy, - successDestination: OnboardingDestinations.installVPNProfile) + onSuccessAction: .navigate(router: appRouterSpy, destination: OnboardingDestinations.installVPNProfile)) var cancellables = Set() let expectation = expectation(description: "Waiting for shouldShowErrorMessage property to be updated") diff --git a/PIA VPN-tvOSTests/RootContainer/RootContainerViewModelTests.swift b/PIA VPN-tvOSTests/RootContainer/RootContainerViewModelTests.swift index f17ce4aa..0decf978 100644 --- a/PIA VPN-tvOSTests/RootContainer/RootContainerViewModelTests.swift +++ b/PIA VPN-tvOSTests/RootContainer/RootContainerViewModelTests.swift @@ -9,6 +9,7 @@ import XCTest import Combine @testable import PIA_VPN_tvOS +import SwiftUI final class RootContainerViewModelTests: XCTestCase { @@ -16,12 +17,12 @@ final class RootContainerViewModelTests: XCTestCase { let accountProvierMock = AccountProviderTypeMock() let notificationCenterMock = NotificationCenterMock() var vpnConfigurationAvailabilityMock = VPNConfigurationAvailabilityMock(value: false) + let appRouterSpy = AppRouterSpy() let bootstrapMock = BootstraperMock() func makeUserAuthenticationStatusMonitorMock(status: UserAuthenticationStatus) -> UserAuthenticationStatusMonitorMock { return UserAuthenticationStatusMonitorMock(status: status) } - } var fixture: Fixture! @@ -38,11 +39,12 @@ final class RootContainerViewModelTests: XCTestCase { } private func initializeSut(bootStrapped: Bool = true) { - sut = RootContainerViewModel(accountProvider: fixture.accountProvierMock, + sut = RootContainerViewModel(accountProvider: fixture.accountProvierMock, notificationCenter: fixture.notificationCenterMock, vpnConfigurationAvailability: fixture.vpnConfigurationAvailabilityMock, bootstrap: fixture.bootstrapMock, - userAuthenticationStatusMonitor: fixture.makeUserAuthenticationStatusMonitorMock(status: .loggedOut)) + userAuthenticationStatusMonitor: fixture.makeUserAuthenticationStatusMonitorMock(status: .loggedOut), + appRouter: fixture.appRouterSpy) sut.isBootstrapped = bootStrapped } @@ -50,13 +52,14 @@ final class RootContainerViewModelTests: XCTestCase { // GIVEN that the user is not logged in fixture.accountProvierMock.isLoggedIn = false - initializeSut() - // WHEN the app is launched - sut.phaseDidBecomeActive() + initializeSut() // THEN the state becomes 'notActivated' XCTAssertEqual(sut.state, .notActivated) + + // AND no navigation requests are sent to the router + XCTAssertEqual(fixture.appRouterSpy.requests, []) } func testState_WhenUserIsAuthenticatedAndVpnProfileNotInstalled() { @@ -65,13 +68,14 @@ final class RootContainerViewModelTests: XCTestCase { // AND GIVEN that the Onboarding Vpn Profile is NOT installed stubOnboardingVpnInstallation(finished: false) - initializeSut() - // WHEN the app is launched - sut.phaseDidBecomeActive() + initializeSut() // THEN the state becomes 'activatedNotOnboarded' XCTAssertEqual(sut.state, .activatedNotOnboarded) + + // AND the router is called to navigate to the Onboarding Install VPN profile + XCTAssertEqual(fixture.appRouterSpy.requests, [AppRouterSpy.Request.navigate(OnboardingDestinations.installVPNProfile)]) } func testState_WhenUserIsAuthenticatedAndVpnProfileInstalled() { @@ -80,21 +84,20 @@ final class RootContainerViewModelTests: XCTestCase { // AND GIVEN that the Onboarding Vpn Profile is installed stubOnboardingVpnInstallation(finished: true) - initializeSut() - // WHEN the app is launched - sut.phaseDidBecomeActive() + initializeSut() // THEN the state becomes 'activated' XCTAssertEqual(sut.state, .activated) + + // AND no navigation requests are sent to the router + XCTAssertEqual(fixture.appRouterSpy.requests, []) } func testBoostrapperIsCalled_WhenAppIsLaunched() { // GIVEN sut is initialized - initializeSut() - // WHEN the app is launched - sut.phaseDidBecomeActive() + initializeSut() // THEN Boostrapper is called XCTAssertEqual(fixture.bootstrapMock.callAsFunctionTimesCalled, 1) @@ -112,10 +115,9 @@ final class RootContainerViewModelTests: XCTestCase { notificationCenter: fixture.notificationCenterMock, vpnConfigurationAvailability: fixture.vpnConfigurationAvailabilityMock, bootstrap: fixture.bootstrapMock, - userAuthenticationStatusMonitor: userAuthenticationStatusMonitor) + userAuthenticationStatusMonitor: userAuthenticationStatusMonitor, + appRouter: fixture.appRouterSpy) - // AND the app is launched - sut.phaseDidBecomeActive() XCTAssertEqual(sut.state, .notActivated) fixture.accountProvierMock.isLoggedIn = true @@ -138,10 +140,9 @@ final class RootContainerViewModelTests: XCTestCase { notificationCenter: fixture.notificationCenterMock, vpnConfigurationAvailability: fixture.vpnConfigurationAvailabilityMock, bootstrap: fixture.bootstrapMock, - userAuthenticationStatusMonitor: userAuthenticationStatusMonitor) + userAuthenticationStatusMonitor: userAuthenticationStatusMonitor, + appRouter: fixture.appRouterSpy) - // AND the app is launched - sut.phaseDidBecomeActive() XCTAssertEqual(sut.state, .activated) fixture.accountProvierMock.isLoggedIn = false diff --git a/PIA VPN-tvOSTests/VPNConfigurationInstalling/VPNConfigurationInstallingViewModelTests.swift b/PIA VPN-tvOSTests/VPNConfigurationInstalling/VPNConfigurationInstallingViewModelTests.swift index 3008a90b..52ca66c7 100644 --- a/PIA VPN-tvOSTests/VPNConfigurationInstalling/VPNConfigurationInstallingViewModelTests.swift +++ b/PIA VPN-tvOSTests/VPNConfigurationInstalling/VPNConfigurationInstallingViewModelTests.swift @@ -35,14 +35,16 @@ final class VPNConfigurationInstallingViewModelTests: XCTestCase { fixture = nil cancellables = nil } + + func instantiateSut(with installConfigError: InstallVPNConfigurationError? = nil) { + sut = VPNConfigurationInstallingViewModel( + installVPNConfiguration: fixture.makeInstallVPNConfiguration(error: installConfigError), + errorMapper: fixture.errorMapper) { AppRouter.Actions.goBackToRoot(router: self.fixture.appRouterSpy)() } + } func test_install_fails_when_installVPNConfiguration_fails() { // GIVEN - sut = VPNConfigurationInstallingViewModel( - installVPNConfiguration: fixture.makeInstallVPNConfiguration(error: .userCanceled), - errorMapper: fixture.errorMapper, - appRouter: fixture.appRouterSpy, - successDestination: OnboardingDestinations.dashboard) + instantiateSut(with: .userCanceled) let expectation = expectation(description: "Waiting for installing to finish with error message") let expectedErrorMessage = "We need this permission for the application to function." @@ -70,11 +72,7 @@ final class VPNConfigurationInstallingViewModelTests: XCTestCase { func test_install_succeeds_when_installVPNConfiguration_succeeds() { // GIVEN - sut = VPNConfigurationInstallingViewModel( - installVPNConfiguration: fixture.makeInstallVPNConfiguration(error: nil), - errorMapper: fixture.errorMapper, - appRouter: fixture.appRouterSpy, - successDestination: OnboardingDestinations.dashboard) + instantiateSut() let expectation = expectation(description: "Waiting for installing to finish successfully") var capturedInstallingStatuses = [VPNConfigurationInstallingStatus]() @@ -89,10 +87,10 @@ final class VPNConfigurationInstallingViewModelTests: XCTestCase { sut.install() // THEN - wait(for: [expectation], timeout: 1) + wait(for: [expectation], timeout: 2) XCTAssertEqual(capturedInstallingStatuses, [.isInstalling, .succeeded]) XCTAssertFalse(sut.shouldShowErrorMessage) XCTAssertNil(sut.errorMessage) - XCTAssertEqual(fixture.appRouterSpy.requests, [.navigate(OnboardingDestinations.dashboard)]) + XCTAssertEqual(fixture.appRouterSpy.requests, [.goBackToRoot]) } } diff --git a/PIA VPN.xcodeproj/project.pbxproj b/PIA VPN.xcodeproj/project.pbxproj index 1327fa9a..8c7f4f17 100644 --- a/PIA VPN.xcodeproj/project.pbxproj +++ b/PIA VPN.xcodeproj/project.pbxproj @@ -203,7 +203,6 @@ 697A5F512B514DC500661977 /* UserActivatedContainerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 697A5F502B514DC500661977 /* UserActivatedContainerFactory.swift */; }; 69816C3D2B4EBB3C00E3C86B /* Destinations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69816C3C2B4EBB3C00E3C86B /* Destinations.swift */; }; 698615A72B62EA7C00A1EA54 /* InstallVPNConfigurationUseCaseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AB28DF2B4C107F00744E5F /* InstallVPNConfigurationUseCaseMock.swift */; }; - 698615A82B62EA7F00A1EA54 /* InstallVPNConfigurationUseCaseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AB28DF2B4C107F00744E5F /* InstallVPNConfigurationUseCaseMock.swift */; }; 698C3B492B2B33650012D527 /* VpnConnectionUseCaseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698C3B482B2B33650012D527 /* VpnConnectionUseCaseMock.swift */; }; 698C3B4B2B2B34760012D527 /* PIAConnectionButtonViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698C3B4A2B2B34760012D527 /* PIAConnectionButtonViewModelTests.swift */; }; 698C3B4E2B2B3CBE0012D527 /* LoginIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AB28742B279BB600744E5F /* LoginIntegrationTests.swift */; }; @@ -2669,6 +2668,7 @@ isa = PBXGroup; children = ( E5AB28B32B35AA8C00744E5F /* VPNConfigurationInstallingFactory.swift */, + E5AB28DF2B4C107F00744E5F /* InstallVPNConfigurationUseCaseMock.swift */, ); path = CompositionRoot; sourceTree = ""; @@ -2686,7 +2686,6 @@ children = ( E5AB28DD2B4C107F00744E5F /* VPNConfigurationAvailabilityMock.swift */, E5AB28DE2B4C107F00744E5F /* VpnConfigurationProviderTypeMock.swift */, - E5AB28DF2B4C107F00744E5F /* InstallVPNConfigurationUseCaseMock.swift */, ); path = Mocks; sourceTree = ""; @@ -4087,7 +4086,6 @@ E5AB286C2B2796E700744E5F /* LoginProviderMock.swift in Sources */, E5AB28872B2911C900744E5F /* AccountProviderMock.swift in Sources */, 696E8F102B31AC690080BB31 /* RootContainerViewModelTests.swift in Sources */, - 698615A82B62EA7F00A1EA54 /* InstallVPNConfigurationUseCaseMock.swift in Sources */, E52E691F2B5DCEA500471913 /* UserAuthenticationStatusMonitorMock.swift in Sources */, E52E690F2B5D695A00471913 /* VPNStatusMonitorTests.swift in Sources */, 698C3B4E2B2B3CBE0012D527 /* LoginIntegrationTests.swift in Sources */,