Skip to content

Commit

Permalink
PIA-1224: Integrate VPN status monitor into connection button (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
kp-said-rehouni authored Jan 24, 2024
1 parent 8126d60 commit 0be9984
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 45 deletions.
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

0 comments on commit 0be9984

Please sign in to comment.