Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PIA-1224: Integrate VPN status monitor into connection button #66

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -14,9 +31,20 @@ class PIAConnectionButtonViewModel: ObservableObject {
@Published var state: State = .disconnected

private let vpnConnectionUseCase: VpnConnectionUseCaseType
private let vpnStatusMonitor: VPNStatusMonitorType
private var cancellables = Set<AnyCancellable>()

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
Expand Down Expand Up @@ -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
}
}

}

2 changes: 2 additions & 0 deletions PIA VPN-tvOS/Shared/StateMonitors/VPNStatusMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class VPNStatusMonitor: VPNStatusMonitorType {
self.vpnStatusProvider = vpnStatusProvider
self.notificationCenter = notificationCenter
self.status = CurrentValueSubject<VPNStatus, Never>(vpnStatusProvider.vpnStatus)

addObservers()
}

private func addObservers() {
Expand Down
6 changes: 6 additions & 0 deletions PIA VPN-tvOSTests/Common/Mocks/VpnConnectionUseCaseMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +31,7 @@ class VpnConnectionUseCaseMock: VpnConnectionUseCaseType {
func connect() {
connectCalled = true
connectCalledAttempt += 1
connectionAction?()
}

var disconnectCalled: Bool = false
Expand All @@ -35,6 +40,7 @@ class VpnConnectionUseCaseMock: VpnConnectionUseCaseType {
func disconnect() {
disconnectCalled = true
disconnectCalledAttempt += 1
disconnectionAction?()
}


Expand Down
57 changes: 31 additions & 26 deletions PIA VPN-tvOSTests/Dashboard/PIAConnectionButtonViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnyCancellable>!
var capturedState: [PIAConnectionButtonViewModel.State]!

override func setUp() {
fixture = Fixture()
sut = PIAConnectionButtonViewModel(useCase: fixture.vpnConnectionUseCaseMock)
sut = PIAConnectionButtonViewModel(useCase: fixture.vpnConnectionUseCaseMock,
vpnStatusMonitor: fixture.vpnStatusMonitor)
cancellables = Set<AnyCancellable>()
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()
Expand All @@ -60,29 +67,29 @@ 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() {
// GIVEN that the vpn state is connected
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)


Expand All @@ -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])
}

}
20 changes: 20 additions & 0 deletions PIA VPN-tvOSTests/Shared/Mocks/VPNStatusMonitorMock.swift
Original file line number Diff line number Diff line change
@@ -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<VPNStatus, Never>()

func getStatus() -> AnyPublisher<VPNStatus, Never> {
return status.eraseToAnyPublisher()
}
}
4 changes: 4 additions & 0 deletions PIA VPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -1263,6 +1264,7 @@
E52E691C2B5DC2B200471913 /* StateMonitorsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateMonitorsFactory.swift; sourceTree = "<group>"; };
E52E691E2B5DCEA500471913 /* UserAuthenticationStatusMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAuthenticationStatusMonitorMock.swift; sourceTree = "<group>"; };
E52E69212B5DCF1F00471913 /* VPNStatusProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNStatusProviderMock.swift; sourceTree = "<group>"; };
E52E69242B5E92CC00471913 /* VPNStatusMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNStatusMonitorMock.swift; sourceTree = "<group>"; };
E59E8F932AEA7A29009278F5 /* ActivityButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityButton.swift; sourceTree = "<group>"; };
E59E8F962AEA7A5A009278F5 /* AutolayoutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutolayoutViewController.swift; sourceTree = "<group>"; };
E59E8F992AEA7A7F009278F5 /* SignupInternetUnreachableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignupInternetUnreachableViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2567,6 +2569,7 @@
isa = PBXGroup;
children = (
E52E69212B5DCF1F00471913 /* VPNStatusProviderMock.swift */,
E52E69242B5E92CC00471913 /* VPNStatusMonitorMock.swift */,
);
path = Mocks;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
Loading