diff --git a/PIA VPN-tvOS/Dashboard/CompositionRoot/DashboardFactory.swift b/PIA VPN-tvOS/Dashboard/CompositionRoot/DashboardFactory.swift index db277dea..7d79e5d9 100644 --- a/PIA VPN-tvOS/Dashboard/CompositionRoot/DashboardFactory.swift +++ b/PIA VPN-tvOS/Dashboard/CompositionRoot/DashboardFactory.swift @@ -29,7 +29,8 @@ class DashboardFactory { extension DashboardFactory { private static func makePIAConnectionButtonViewModel() -> PIAConnectionButtonViewModel { - return PIAConnectionButtonViewModel(useCase: makeVpnConnectionUseCase()) + return PIAConnectionButtonViewModel(useCase: makeVpnConnectionUseCase(), + vpnStatusMonitor: StateMonitorsFactory.makeVPNStatusMonitor()) } private static func makeVpnConnectionUseCase() -> VpnConnectionUseCaseType { diff --git a/PIA VPN-tvOS/Dashboard/Presentation/PIAConnectionButtonViewModel.swift b/PIA VPN-tvOS/Dashboard/Presentation/PIAConnectionButtonViewModel.swift index 3b2d3f3c..b03bbec8 100644 --- a/PIA VPN-tvOS/Dashboard/Presentation/PIAConnectionButtonViewModel.swift +++ b/PIA VPN-tvOS/Dashboard/Presentation/PIAConnectionButtonViewModel.swift @@ -2,6 +2,23 @@ import Foundation import SwiftUI +import PIALibrary +import Combine + +extension VPNStatus { + func toConnectionButtonState() -> PIAConnectionButtonViewModel.State { + switch self { + case .connected: + return .connected + case .connecting: + return .connecting + case .disconnecting: + return .disconnecting + default: + return .disconnected + } + } +} class PIAConnectionButtonViewModel: ObservableObject { enum State { @@ -14,9 +31,20 @@ class PIAConnectionButtonViewModel: ObservableObject { @Published var state: State = .disconnected private let vpnConnectionUseCase: VpnConnectionUseCaseType + private let vpnStatusMonitor: VPNStatusMonitorType + private var cancellables = Set() - init(useCase: VpnConnectionUseCaseType) { + init(useCase: VpnConnectionUseCaseType, vpnStatusMonitor: VPNStatusMonitorType) { self.vpnConnectionUseCase = useCase + self.vpnStatusMonitor = vpnStatusMonitor + + addObservers() + } + + private func addObservers() { + vpnStatusMonitor.getStatus().sink { [weak self] vpnStatus in + self?.state = vpnStatus.toConnectionButtonState() + }.store(in: &cancellables) } // Inner ring color and outer ring color @@ -55,28 +83,11 @@ extension PIAConnectionButtonViewModel { } private func connect() { - // TODO: Take the state from the real VpnManager state monitor - state = .connecting - vpnConnectionUseCase.connect() - - // TODO: Take the state from the real VpnManager state monitor - DispatchQueue.main.asyncAfter(deadline: .now()+0.2) { [weak self] in - self?.state = .connected - } } private func disconnect() { - // TODO: Take the state from the real VpnManager state monitor - state = .disconnecting - vpnConnectionUseCase.disconnect() - - // TODO: Take the state from the real VpnManager state monitor - DispatchQueue.main.asyncAfter(deadline: .now()+0.2) { [weak self] in - self?.state = .disconnected - } } - } diff --git a/PIA VPN-tvOS/Shared/StateMonitors/VPNStatusMonitor.swift b/PIA VPN-tvOS/Shared/StateMonitors/VPNStatusMonitor.swift index ee69aef5..64eab365 100644 --- a/PIA VPN-tvOS/Shared/StateMonitors/VPNStatusMonitor.swift +++ b/PIA VPN-tvOS/Shared/StateMonitors/VPNStatusMonitor.swift @@ -23,6 +23,8 @@ class VPNStatusMonitor: VPNStatusMonitorType { self.vpnStatusProvider = vpnStatusProvider self.notificationCenter = notificationCenter self.status = CurrentValueSubject(vpnStatusProvider.vpnStatus) + + addObservers() } private func addObservers() { diff --git a/PIA VPN-tvOSTests/Common/Mocks/VpnConnectionUseCaseMock.swift b/PIA VPN-tvOSTests/Common/Mocks/VpnConnectionUseCaseMock.swift index c36b2811..bc467874 100644 --- a/PIA VPN-tvOSTests/Common/Mocks/VpnConnectionUseCaseMock.swift +++ b/PIA VPN-tvOSTests/Common/Mocks/VpnConnectionUseCaseMock.swift @@ -15,10 +15,14 @@ class VpnConnectionUseCaseMock: VpnConnectionUseCaseType { var connectCalledToServerAttempt: Int = 0 var connectToServerCalledWithArgument: ServerType? + var connectionAction: (() -> Void)? + var disconnectionAction: (() -> Void)? + func connect(to server: ServerType) { connectToServerCalled = true connectCalledToServerAttempt += 1 connectToServerCalledWithArgument = server + connectionAction?() } var connectCalled: Bool = false @@ -27,6 +31,7 @@ class VpnConnectionUseCaseMock: VpnConnectionUseCaseType { func connect() { connectCalled = true connectCalledAttempt += 1 + connectionAction?() } var disconnectCalled: Bool = false @@ -35,6 +40,7 @@ class VpnConnectionUseCaseMock: VpnConnectionUseCaseType { func disconnect() { disconnectCalled = true disconnectCalledAttempt += 1 + disconnectionAction?() } diff --git a/PIA VPN-tvOSTests/Dashboard/PIAConnectionButtonViewModelTests.swift b/PIA VPN-tvOSTests/Dashboard/PIAConnectionButtonViewModelTests.swift index 3d05d2c0..d5998faf 100644 --- a/PIA VPN-tvOSTests/Dashboard/PIAConnectionButtonViewModelTests.swift +++ b/PIA VPN-tvOSTests/Dashboard/PIAConnectionButtonViewModelTests.swift @@ -14,43 +14,50 @@ final class PIAConnectionButtonViewModelTests: XCTestCase { final class Fixture { let vpnConnectionUseCaseMock = VpnConnectionUseCaseMock() + let vpnStatusMonitor = VPNStatusMonitorMock() } var fixture: Fixture! var sut: PIAConnectionButtonViewModel! var cancellables: Set! + var capturedState: [PIAConnectionButtonViewModel.State]! override func setUp() { fixture = Fixture() - sut = PIAConnectionButtonViewModel(useCase: fixture.vpnConnectionUseCaseMock) + sut = PIAConnectionButtonViewModel(useCase: fixture.vpnConnectionUseCaseMock, + vpnStatusMonitor: fixture.vpnStatusMonitor) cancellables = Set() + capturedState = [PIAConnectionButtonViewModel.State]() } override func tearDown() { fixture = nil sut = nil cancellables = nil + capturedState = nil } func test_toggleConnection_when_disconnected() { // GIVEN that the vpn state is disconnected XCTAssertTrue(sut.state == .disconnected) - let connectingExpectation = expectation(description: "vpn is connecting") + fixture.vpnConnectionUseCaseMock.connectionAction = { [weak self] in + self?.fixture.vpnStatusMonitor.status.send(.connecting) + self?.fixture.vpnStatusMonitor.status.send(.connected) + } - let connectedExpectation = expectation(description: "vpn is connected") + fixture.vpnConnectionUseCaseMock.disconnectionAction = { [weak self] in + self?.fixture.vpnStatusMonitor.status.send(.disconnecting) + self?.fixture.vpnStatusMonitor.status.send(.disconnected) + } + + capturedState = [PIAConnectionButtonViewModel.State]() sut.$state.sink { _ in } receiveValue: { state in - switch state { - case .connecting: - connectingExpectation.fulfill() - case .connected: - connectedExpectation.fulfill() - default: break - } + self.capturedState.append(state) }.store(in: &cancellables) - + // WHEN calling the toggle connection method sut.toggleConnection() @@ -60,9 +67,7 @@ final class PIAConnectionButtonViewModelTests: XCTestCase { XCTAssertTrue(fixture.vpnConnectionUseCaseMock.connectCalledAttempt == 1) // AND the Vpn state becomes 'connecting' and 'connected' - wait(for: [connectingExpectation, connectedExpectation], timeout: 1) - XCTAssertEqual(sut.state, .connected) - + XCTAssertEqual(capturedState, [.disconnected, .connecting, .connected]) } func test_toggleConnection_when_connected() { @@ -70,19 +75,21 @@ final class PIAConnectionButtonViewModelTests: XCTestCase { sut.state = .connected XCTAssertTrue(sut.state == .connected) - let disconnectingExpectation = expectation(description: "vpn is disconnecting") + fixture.vpnConnectionUseCaseMock.connectionAction = { [weak self] in + self?.fixture.vpnStatusMonitor.status.send(.connecting) + self?.fixture.vpnStatusMonitor.status.send(.connected) + } - let disconnectedExpectation = expectation(description: "vpn is disconnected") + fixture.vpnConnectionUseCaseMock.disconnectionAction = { [weak self] in + self?.fixture.vpnStatusMonitor.status.send(.disconnecting) + self?.fixture.vpnStatusMonitor.status.send(.disconnected) + } + + capturedState = [PIAConnectionButtonViewModel.State]() sut.$state.sink { _ in } receiveValue: { state in - switch state { - case .disconnecting: - disconnectingExpectation.fulfill() - case .disconnected: - disconnectedExpectation.fulfill() - default: break - } + self.capturedState.append(state) }.store(in: &cancellables) @@ -94,9 +101,7 @@ final class PIAConnectionButtonViewModelTests: XCTestCase { XCTAssertTrue(fixture.vpnConnectionUseCaseMock.disconnectCalledAttempt == 1) // AND the Vpn state becomes 'disconnecting' and 'disconnected' - wait(for: [disconnectingExpectation, disconnectedExpectation], timeout: 1) - XCTAssertEqual(sut.state, .disconnected) - + XCTAssertEqual(capturedState, [.connected, .disconnecting, .disconnected]) } } diff --git a/PIA VPN-tvOSTests/Shared/Mocks/VPNStatusMonitorMock.swift b/PIA VPN-tvOSTests/Shared/Mocks/VPNStatusMonitorMock.swift new file mode 100644 index 00000000..0503f6d0 --- /dev/null +++ b/PIA VPN-tvOSTests/Shared/Mocks/VPNStatusMonitorMock.swift @@ -0,0 +1,20 @@ +// +// VPNStatusMonitorMock.swift +// PIA VPN-tvOSTests +// +// Created by Said Rehouni on 22/1/24. +// Copyright © 2024 Private Internet Access Inc. All rights reserved. +// + +import Foundation +import Combine +import PIALibrary +@testable import PIA_VPN_tvOS + +class VPNStatusMonitorMock: VPNStatusMonitorType { + var status = PassthroughSubject() + + func getStatus() -> AnyPublisher { + return status.eraseToAnyPublisher() + } +} diff --git a/PIA VPN.xcodeproj/project.pbxproj b/PIA VPN.xcodeproj/project.pbxproj index 1dd36622..7d35aaef 100644 --- a/PIA VPN.xcodeproj/project.pbxproj +++ b/PIA VPN.xcodeproj/project.pbxproj @@ -571,6 +571,7 @@ E52E691D2B5DC2B200471913 /* StateMonitorsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52E691C2B5DC2B200471913 /* StateMonitorsFactory.swift */; }; E52E691F2B5DCEA500471913 /* UserAuthenticationStatusMonitorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52E691E2B5DCEA500471913 /* UserAuthenticationStatusMonitorMock.swift */; }; E52E69222B5DCF1F00471913 /* VPNStatusProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52E69212B5DCF1F00471913 /* VPNStatusProviderMock.swift */; }; + E52E69252B5E92CC00471913 /* VPNStatusMonitorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52E69242B5E92CC00471913 /* VPNStatusMonitorMock.swift */; }; E59E8F942AEA7A29009278F5 /* ActivityButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E59E8F932AEA7A29009278F5 /* ActivityButton.swift */; }; E59E8F952AEA7A29009278F5 /* ActivityButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E59E8F932AEA7A29009278F5 /* ActivityButton.swift */; }; E59E8F972AEA7A5A009278F5 /* AutolayoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E59E8F962AEA7A5A009278F5 /* AutolayoutViewController.swift */; }; @@ -1263,6 +1264,7 @@ E52E691C2B5DC2B200471913 /* StateMonitorsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateMonitorsFactory.swift; sourceTree = ""; }; E52E691E2B5DCEA500471913 /* UserAuthenticationStatusMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAuthenticationStatusMonitorMock.swift; sourceTree = ""; }; E52E69212B5DCF1F00471913 /* VPNStatusProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNStatusProviderMock.swift; sourceTree = ""; }; + E52E69242B5E92CC00471913 /* VPNStatusMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNStatusMonitorMock.swift; sourceTree = ""; }; E59E8F932AEA7A29009278F5 /* ActivityButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityButton.swift; sourceTree = ""; }; E59E8F962AEA7A5A009278F5 /* AutolayoutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutolayoutViewController.swift; sourceTree = ""; }; E59E8F992AEA7A7F009278F5 /* SignupInternetUnreachableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignupInternetUnreachableViewController.swift; sourceTree = ""; }; @@ -2567,6 +2569,7 @@ isa = PBXGroup; children = ( E52E69212B5DCF1F00471913 /* VPNStatusProviderMock.swift */, + E52E69242B5E92CC00471913 /* VPNStatusMonitorMock.swift */, ); path = Mocks; sourceTree = ""; @@ -4069,6 +4072,7 @@ 698C3B492B2B33650012D527 /* VpnConnectionUseCaseMock.swift in Sources */, 69CA26B22B59668700E78894 /* RegionsListUseCaseMock.swift in Sources */, E5C507CC2B1FACE000200A6A /* LoginWithCredentialsUseCaseTests.swift in Sources */, + E52E69252B5E92CC00471913 /* VPNStatusMonitorMock.swift in Sources */, 696E8F0D2B31A8760080BB31 /* NotificationCenterMock.swift in Sources */, E5C507A82B153E6B00200A6A /* LoginViewModelTests.swift in Sources */, E5AB286C2B2796E700744E5F /* LoginProviderMock.swift in Sources */,