Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PIA-1845: Add Dedicated IP native endpoint
Browse files Browse the repository at this point in the history
kp-said-rehouni committed Jul 3, 2024
1 parent 0f0daed commit 2b8ddeb
Showing 24 changed files with 818 additions and 112 deletions.
3 changes: 3 additions & 0 deletions Sources/PIALibrary/Account/Data/ClientErrorMapper.swift
Original file line number Diff line number Diff line change
@@ -42,6 +42,9 @@ struct ClientErrorMapper {

case .unableToDecodeData:
return .malformedResponseData

case .unauthorized:
return .unauthorized
}
}

46 changes: 46 additions & 0 deletions Sources/PIALibrary/Account/Data/DedicatedIPServerMapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

import Foundation

class DedicatedIPServerMapper: DedicatedIPServerMapperType {
private let dedicatedIPTokenHandler: DedicatedIPTokenHandlerType

init(dedicatedIPTokenHandler: DedicatedIPTokenHandlerType) {
self.dedicatedIPTokenHandler = dedicatedIPTokenHandler
}

func map(dedicatedIps: [DedicatedIPInformation]) -> Result<[Server], ClientError> {
var dipRegions = [Server]()

for dipServer in dedicatedIps {
let status = DedicatedIPStatus(fromAPIStatus: dipServer.status)

switch dipServer.status {
case .active:

guard let firstServer = Client.providers.serverProvider.currentServers.first(where: {$0.regionIdentifier == dipServer.id}) else {
return .failure(ClientError.malformedResponseData)
}

guard let ip = dipServer.ip, let cn = dipServer.cn, let expirationTime = dipServer.dipExpire else {
return .failure(ClientError.malformedResponseData)
}

let dipUsername = "dedicated_ip_"+dipServer.dipToken+"_"+String.random(length: 8)
let expiringDate = Date(timeIntervalSince1970: TimeInterval(expirationTime))
let server = Server.ServerAddressIP(ip: ip, cn: cn, van: false)

let dipRegion = Server(serial: firstServer.serial, name: firstServer.name, country: firstServer.country, hostname: firstServer.hostname, openVPNAddressesForTCP: [server], openVPNAddressesForUDP: [server], wireGuardAddressesForUDP: [server], iKEv2AddressesForUDP: [server], pingAddress: firstServer.pingAddress, geo: false, meta: nil, dipExpire: expiringDate, dipToken: dipServer.dipToken, dipStatus: status, dipUsername: dipUsername, regionIdentifier: firstServer.regionIdentifier)

dipRegions.append(dipRegion)
dedicatedIPTokenHandler(dedicatedIp: dipServer, dipUsername: dipUsername)

default:

let dipRegion = Server(serial: "", name: "", country: "", hostname: "", openVPNAddressesForTCP: [], openVPNAddressesForUDP: [], wireGuardAddressesForUDP: [], iKEv2AddressesForUDP: [], pingAddress: nil, geo: false, meta: nil, dipExpire: nil, dipToken: nil, dipStatus: status, dipUsername: nil, regionIdentifier: "")
dipRegions.append(dipRegion)
}
}

return .success(dipRegions)
}
}
21 changes: 21 additions & 0 deletions Sources/PIALibrary/Account/Data/DedicatedIPTokenHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

import Foundation

class DedicatedIPTokenHandler: DedicatedIPTokenHandlerType {
private let secureStore: SecureStore

init(secureStore: SecureStore) {
self.secureStore = secureStore
}

func callAsFunction(dedicatedIp: DedicatedIPInformation, dipUsername: String) {
if dedicatedIp.isAboutToExpire {
Macros.postNotification(.PIADIPRegionExpiring, [.token : dedicatedIp.dipToken])
}

Macros.postNotification(.PIADIPCheckIP, [.token : dedicatedIp.dipToken, .ip : dedicatedIp.ip!])

secureStore.setDIPToken(dedicatedIp.dipToken)
secureStore.setPassword(dedicatedIp.ip!, forDipToken: dipUsername)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

import Foundation
import NWHttpConnection

struct GetDedicatedIPsRequestConfiguration: NetworkRequestConfigurationType {
let networkRequestModule: NetworkRequestModule = .account
let path: RequestAPI.Path = .dedicatedIp
let httpMethod: NWHttpConnection.NWConnectionHTTPMethod = .post
let contentType: NetworkRequestContentType = .json
var inlcudeAuthHeaders: Bool = true
var urlQueryParameters: [String : String]? = nil
let responseDataType: NWDataResponseType = .jsonData

var body: Data? = nil
var otherHeaders: [String : String]? = nil

let timeout: TimeInterval = 10
let requestQueue: DispatchQueue? = DispatchQueue(label: "getDedicatedIPs_request.queue")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

import Foundation
import NWHttpConnection

struct RenewDedicatedIPRequestConfiguration: NetworkRequestConfigurationType {
let networkRequestModule: NetworkRequestModule = .account
let path: RequestAPI.Path = .renewDedicatedIp
let httpMethod: NWHttpConnection.NWConnectionHTTPMethod = .post
let contentType: NetworkRequestContentType = .json
var inlcudeAuthHeaders: Bool = true
var urlQueryParameters: [String : String]? = nil
let responseDataType: NWDataResponseType = .jsonData

var body: Data? = nil
var otherHeaders: [String : String]? = nil

let timeout: TimeInterval = 10
let requestQueue: DispatchQueue? = DispatchQueue(label: "RenewDedicatedIP_request.queue")
}
Original file line number Diff line number Diff line change
@@ -6,31 +6,11 @@ class SignupInformationDataCoverter: SignupInformationDataCoverterType {
let signupInformation = SignupInformation(store: "apple_app_store",
receipt: signup.receipt.base64EncodedString(),
email: signup.email,
marketing: stringify(json: signup.marketing),
debug: stringify(json: signup.debug))
marketing: stringify(json: signup.marketing, prettyPrinted: false),
debug: stringify(json: signup.debug, prettyPrinted: false))

return signupInformation.toData()
}

private func stringify(json: [String: Any]?, prettyPrinted: Bool = false) -> String? {
guard let json else {
return nil
}

var options: JSONSerialization.WritingOptions = []
if prettyPrinted {
options = JSONSerialization.WritingOptions.prettyPrinted
}

do {
let data = try JSONSerialization.data(withJSONObject: json, options: options)
if let string = String(data: data, encoding: String.Encoding.utf8) {
return string
}
} catch {
print(error)
}

return nil
}
}

extension SignupInformationDataCoverter: JSONToStringCoverterType {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

import Foundation

struct DedicatedIPInformationResult: Codable {
let result: [DedicatedIPInformation]
}

public struct DedicatedIPInformation: Codable {
enum Status: String, Codable {
case active, expired, invalid, error
}

let id: String?
let ip: String?
let cn: String?
let groups: [String]?
let dipExpire: Double?
let dipToken: String
let status: DedicatedIPInformation.Status

enum CodingKeys: String, CodingKey {
case id = "id"
case ip = "ip"
case cn = "cn"
case groups = "groups"
case dipExpire = "dip_expire"
case dipToken = "dip_token"
case status = "status"
}

//Expiring in 5 days or less
var isAboutToExpire: Bool {
guard let dipExpire, let nextDays = Calendar.current.date(byAdding: .day, value: 5, to: Date())
else {
return true
}

let expiringDate = Date(timeIntervalSince1970: TimeInterval(dipExpire))
return nextDays >= expiringDate
}

static func makeWith(data: Data) -> [DedicatedIPInformation]? {
let dto = try? JSONDecoder().decode(DedicatedIPInformationResult.self, from: data)
return dto?.result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

import Foundation

protocol DedicatedIPServerMapperType {
func map(dedicatedIps: [DedicatedIPInformation]) -> Result<[Server], ClientError>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

import Foundation

protocol DedicatedIPTokenHandlerType {
func callAsFunction(dedicatedIp: DedicatedIPInformation, dipUsername: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@

import Foundation

public protocol GetDedicatedIPsUseCaseType {
typealias Completion = ((Result<[DedicatedIPInformation], NetworkRequestError>) -> Void)
func callAsFunction(dipTokens: [String], completion: @escaping Completion)
}

class GetDedicatedIPsUseCase: GetDedicatedIPsUseCaseType {
private let networkClient: NetworkRequestClientType
private let refreshAuthTokensChecker: RefreshAuthTokensCheckerType

init(networkClient: NetworkRequestClientType, refreshAuthTokensChecker: RefreshAuthTokensCheckerType) {
self.networkClient = networkClient
self.refreshAuthTokensChecker = refreshAuthTokensChecker
}

func callAsFunction(dipTokens: [String], completion: @escaping Completion) {
refreshAuthTokensChecker.refreshIfNeeded { [weak self] error in
guard let self else { return }
if let error {
completion(.failure(error))
} else {
networkClient.executeRequest(with: makeConfiguration(dipTokens: dipTokens)) { error, dataResponse in
if let error {
self.handleErrorResponse(error, completion: completion)
} else if let dataResponse {
self.handleDataResponse(dataResponse, completion: completion)
} else {
completion(.failure(NetworkRequestError.allConnectionAttemptsFailed()))
}
}
}
}
}

private func makeConfiguration(dipTokens: [String]) -> GetDedicatedIPsRequestConfiguration {
var configuration = GetDedicatedIPsRequestConfiguration()

let bodyDataDict = ["tokens": dipTokens]

if let bodyData = try? JSONEncoder().encode(bodyDataDict) {
configuration.body = bodyData
}

return configuration
}

private func handleErrorResponse(_ error: NetworkRequestError, completion: @escaping GetDedicatedIPsUseCaseType.Completion) {
switch error {
case .allConnectionAttemptsFailed(let statusCode):
completion(.failure(statusCode == 401 ? NetworkRequestError.unauthorized : error))
return
case .connectionError(statusCode: let statusCode, message: _):
completion(.failure(statusCode == 401 ? NetworkRequestError.unauthorized : error))
return
default:
completion(.failure(error))
}
}

private func handleDataResponse(_ dataResponse: NetworkRequestResponseType, completion: @escaping GetDedicatedIPsUseCaseType.Completion) {
guard let dataResponseContent = dataResponse.data else {
completion(.failure(NetworkRequestError.noDataContent))
return
}

guard let dto = DedicatedIPInformation.makeWith(data: dataResponseContent) else {
completion(.failure(NetworkRequestError.unableToDecodeDataContent))
return
}

completion(.success(dto))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@

import Foundation

public protocol RenewDedicatedIPUseCaseType {
typealias Completion = ((Result<Void, NetworkRequestError>) -> Void)
func callAsFunction(dipToken: String, completion: @escaping Completion)
}

class RenewDedicatedIPUseCase: RenewDedicatedIPUseCaseType {
private let networkClient: NetworkRequestClientType
private let refreshAuthTokensChecker: RefreshAuthTokensCheckerType

init(networkClient: NetworkRequestClientType, refreshAuthTokensChecker: RefreshAuthTokensCheckerType) {
self.networkClient = networkClient
self.refreshAuthTokensChecker = refreshAuthTokensChecker
}

func callAsFunction(dipToken: String, completion: @escaping Completion) {
refreshAuthTokensChecker.refreshIfNeeded { [weak self] error in
guard let self else { return }
if let error {
completion(.failure(error))
} else {
networkClient.executeRequest(with: makeConfiguration(dipToken: dipToken)) { error, response in
if let error {
self.handleErrorResponse(error, completion: completion)
return
}

completion(.success(()))
}
}
}
}

private func makeConfiguration(dipToken: String) -> RenewDedicatedIPRequestConfiguration {
var configuration = RenewDedicatedIPRequestConfiguration()

let bodyDataDict = ["token": dipToken]

if let bodyData = try? JSONEncoder().encode(bodyDataDict) {
configuration.body = bodyData
}

return configuration
}

private func handleErrorResponse(_ error: NetworkRequestError, completion: @escaping RenewDedicatedIPUseCaseType.Completion) {
switch error {
case .allConnectionAttemptsFailed(let statusCode):
completion(.failure(statusCode == 401 ? NetworkRequestError.unauthorized : error))
return
case .connectionError(statusCode: let statusCode, message: let message):
completion(.failure(statusCode == 401 ? NetworkRequestError.unauthorized : error))
return
default:
completion(.failure(error))
}
}
}
2 changes: 1 addition & 1 deletion Sources/PIALibrary/Client+Providers.swift
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ extension Client {
public var accountProvider: AccountProvider = AccountFactory.makeDefaultAccountProvider()

/// Provides methods for handling the available VPN servers.
public var serverProvider: ServerProvider = DefaultServerProvider()
public var serverProvider: ServerProvider = ServerProviderFactory.makeDefaultServerProvider()

/// Provides methods for controlling the VPN connection.
public var vpnProvider: VPNProvider = DefaultVPNProvider()
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ public enum NetworkRequestError: Error, Equatable {
case unableToDecodeDataContent
case connectionCompletedWithNoResponse
case badReceipt
case unauthorized
case unknown(message: String? = nil)
case unableToDecodeData

8 changes: 8 additions & 0 deletions Sources/PIALibrary/Mock/MockDedicatedIPServerMapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

import Foundation

class MockDedicatedIPServerMapper: DedicatedIPServerMapperType {
func map(dedicatedIps: [DedicatedIPInformation]) -> Result<[Server], ClientError> {
return .success([])
}
}
6 changes: 6 additions & 0 deletions Sources/PIALibrary/Mock/MockGetDedicatedIPsUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

import Foundation

class MockGetDedicatedIPsUseCase: GetDedicatedIPsUseCaseType {
func callAsFunction(dipTokens: [String], completion: @escaping Completion) {}
}
6 changes: 6 additions & 0 deletions Sources/PIALibrary/Mock/MockRenewDedicatedIPUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

import Foundation

class MockRenewDedicatedIPUseCase: RenewDedicatedIPUseCaseType {
func callAsFunction(dipToken: String, completion: @escaping Completion) {}
}
5 changes: 4 additions & 1 deletion Sources/PIALibrary/Mock/MockServerProvider.swift
Original file line number Diff line number Diff line change
@@ -82,7 +82,10 @@ public class MockServerProvider: ServerProvider, DatabaseAccess, WebServicesCons
]

let webServices = MockWebServices()
delegate = DefaultServerProvider(webServices: webServices)
delegate = DefaultServerProvider(webServices: webServices,
renewDedicatedIP: MockRenewDedicatedIPUseCase(),
getDedicatedIPs: MockGetDedicatedIPsUseCase(),
dedicatedIPServerMapper: MockDedicatedIPServerMapper())
self.webServices = webServices

webServices.serversBundle = {
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

import Foundation

class ServerProviderFactory {
static func makeDefaultServerProvider() -> ServerProvider {
DefaultServerProvider(renewDedicatedIP: makeRenewDedicatedIPUseCase(),
getDedicatedIPs: makeGetDedicatedIPsUseCase(),
dedicatedIPServerMapper: makeDedicatedIPServerMapper())
}

static func makeGetDedicatedIPsUseCase() -> GetDedicatedIPsUseCaseType {
GetDedicatedIPsUseCase(networkClient: NetworkRequestFactory.maketNetworkRequestClient(),
refreshAuthTokensChecker: AccountFactory.makeRefreshAuthTokensChecker())
}

public static func makeDedicatedIPServerMapper() -> DedicatedIPServerMapperType {
DedicatedIPServerMapper(dedicatedIPTokenHandler: makeDedicatedIPTokenHandler())
}

static func makeDedicatedIPTokenHandler() -> DedicatedIPTokenHandlerType {
DedicatedIPTokenHandler(secureStore: Client.database.secure)
}

public static func makeRenewDedicatedIPUseCase() -> RenewDedicatedIPUseCaseType {
RenewDedicatedIPUseCase(networkClient: NetworkRequestFactory.maketNetworkRequestClient(),
refreshAuthTokensChecker: AccountFactory.makeRefreshAuthTokensChecker())
}
}
129 changes: 105 additions & 24 deletions Sources/PIALibrary/Server/DefaultServerProvider.swift
Original file line number Diff line number Diff line change
@@ -27,13 +27,20 @@ import __PIALibraryNative
open class DefaultServerProvider: ServerProvider, ConfigurationAccess, DatabaseAccess, PreferencesAccess, WebServicesAccess, WebServicesConsumer {

private let customWebServices: WebServices?
private let renewDedicatedIP: RenewDedicatedIPUseCaseType
private let getDedicatedIPs: GetDedicatedIPsUseCaseType
private let dedicatedIPServerMapper: DedicatedIPServerMapperType

init(webServices: WebServices? = nil) {
init(webServices: WebServices? = nil, renewDedicatedIP: RenewDedicatedIPUseCaseType, getDedicatedIPs: GetDedicatedIPsUseCaseType, dedicatedIPServerMapper: DedicatedIPServerMapperType) {
if let webServices = webServices {
customWebServices = webServices
} else {
customWebServices = nil
}

self.renewDedicatedIP = renewDedicatedIP
self.getDedicatedIPs = getDedicatedIPs
self.dedicatedIPServerMapper = dedicatedIPServerMapper
}

// MARK: ServerProvider
@@ -149,6 +156,23 @@ open class DefaultServerProvider: ServerProvider, ConfigurationAccess, DatabaseA
}
}

private func handleDownloadDIPsResponse(_ response: Result<[Server], ClientError>, bundle: ServersBundle, callback: (([Server]?, Error?) -> Void)?) {
switch response {
case .success(let servers):
var allServers = bundle.servers

for server in servers where !bundle.servers.contains(where: {$0.dipToken == server.dipToken}) {
allServers.append(server)
}

self.currentServers = allServers
Macros.postNotification(.PIAThemeDidChange)
callback?(currentServers, nil)
case .failure(let clientError):
callback?(currentServers, clientError)
}
}

public func download(_ callback: (([Server]?, Error?) -> Void)?) {
webServices.downloadServers { (bundle, error) in
guard let bundle = bundle else {
@@ -160,6 +184,26 @@ open class DefaultServerProvider: ServerProvider, ConfigurationAccess, DatabaseA
}

if let tokens = self.accessedDatabase.secure.dipTokens(), !tokens.isEmpty {
self.getDedicatedIPs(dipTokens: tokens) { [weak self] result in
guard let self else { return }
switch result {
case .success(let dedicatedIPServers):
let mapperResult = dedicatedIPServerMapper.map(dedicatedIps: dedicatedIPServers)
handleDownloadDIPsResponse(mapperResult, bundle: bundle, callback: callback)

case .failure(let error):
let clientError = ClientErrorMapper.map(networkRequestError: error)
if clientError == .unauthorized {
Client.providers.accountProvider.logout(nil)
Macros.postNotification(.PIAUnauthorized)
} else {
callback?(self.currentServers, clientError)
}
}
}

/*

self.webServices.activateDIPToken(tokens: tokens) { (servers, error) in

if error != nil, error as! ClientError == ClientError.unauthorized {
@@ -181,51 +225,87 @@ open class DefaultServerProvider: ServerProvider, ConfigurationAccess, DatabaseA
self.currentServers = allServers
Macros.postNotification(.PIAThemeDidChange)
callback?(self.currentServers, error)
}
}*/
} else {
self.currentServers = bundle.servers
callback?(self.currentServers, error)
}

}
}

public func activateDIPToken(_ token: String, _ callback: LibraryCallback<Server?>?) {
guard Client.providers.accountProvider.isLoggedIn else {
preconditionFailure()
}
webServices.activateDIPToken(tokens: [token]) { (servers, error) in
if let servers = servers,
let first = servers.first,
let status = first.dipStatus {
if !self.currentServers.contains(where: {$0.dipToken == first.dipToken}) && status == .active {
self.currentServers.append(contentsOf: servers)
}
callback?(first, error)
} else {
callback?(nil, error)

getDedicatedIPs(dipTokens: [token]) { [weak self] result in
guard let self else { return }
switch result {
case .success(let servers):
handleDIPServerResponse(dedicatedIPServerMapper.map(dedicatedIps: servers), callback)
case .failure(let error):
callback?(nil, ClientErrorMapper.map(networkRequestError: error))
}
}
}

private func handleDIPServerResponse(_ response: Result<[Server], ClientError>, _ callback: LibraryCallback<Server>?) {
guard case .success(let servers) = response else {
guard case .failure(let error) = response else {
callback?(nil, ClientError.unexpectedReply)
return
}

callback?(nil, error)
return
}

guard let first = servers.first, let status = first.dipStatus else {
callback?(nil, ClientError.unexpectedReply)
return
}

if !self.currentServers.contains(where: {$0.dipToken == first.dipToken}) && status == .active {
self.currentServers.append(contentsOf: servers)
}

callback?(first, nil)
}

public func activateDIPTokens(_ tokens: [String], _ callback: LibraryCallback<[Server]>?) {
guard Client.providers.accountProvider.isLoggedIn else {
preconditionFailure()
}
webServices.activateDIPToken(tokens: tokens) { (servers, error) in
if let servers = servers {
for server in servers {
if !self.currentServers.contains(where: {$0.dipToken == server.dipToken}) {
self.currentServers.append(server)
}
}
callback?(servers, error)
} else {
callback?([], error)

getDedicatedIPs(dipTokens: tokens) { [weak self] result in
guard let self else { return }
switch result {
case .success(let servers):
handleDIPServersResponse(dedicatedIPServerMapper.map(dedicatedIps: servers), callback)
case .failure(let error):
callback?([], ClientErrorMapper.map(networkRequestError: error))
}
}
}

private func handleDIPServersResponse(_ response: Result<[Server], ClientError>, _ callback: LibraryCallback<[Server]>?) {
guard case .success(let servers) = response else {
guard case .failure(let error) = response else {
callback?(nil, ClientError.unexpectedReply)
return
}

callback?(nil, error)
return
}

for server in servers where !self.currentServers.contains(where: {$0.dipToken == server.dipToken}) {
self.currentServers.append(server)
}

callback?(servers, nil)
}

public func removeDIPToken(_ dipToken: String) {
guard Client.providers.accountProvider.isLoggedIn else {
preconditionFailure()
@@ -238,7 +318,8 @@ open class DefaultServerProvider: ServerProvider, ConfigurationAccess, DatabaseA
guard Client.providers.accountProvider.isLoggedIn else {
preconditionFailure()
}
webServices.handleDIPTokenExpiration(dipToken: dipToken, nil)

renewDedicatedIP(dipToken: dipToken, completion: { _ in })
}

public func find(withIdentifier identifier: String) -> Server? {
3 changes: 1 addition & 2 deletions Sources/PIALibrary/WebServices/DedicatedIP.swift
Original file line number Diff line number Diff line change
@@ -20,7 +20,6 @@
//

import Foundation
import account

public enum DedicatedIPStatus {

@@ -29,7 +28,7 @@ public enum DedicatedIPStatus {
case invalid
case error

init(fromAPIStatus dipStatus: DedicatedIPInformationResponse.Status) {
init(fromAPIStatus dipStatus: DedicatedIPInformation.Status) {
switch dipStatus {
case .invalid:
self = .invalid
58 changes: 0 additions & 58 deletions Sources/PIALibrary/WebServices/PIAWebServices.swift
Original file line number Diff line number Diff line change
@@ -237,64 +237,6 @@ class PIAWebServices: WebServices, ConfigurationAccess {
}
}

func activateDIPToken(tokens: [String], _ callback: LibraryCallback<[Server]>?) {
self.accountAPI.dedicatedIPs(ipTokens: tokens) { (dedicatedIps, errors) in
if !errors.isEmpty {
callback?([], self.mapDIPError(errors.last))
return
}

var dipRegions = [Server]()
for dipServer in dedicatedIps {

let status = DedicatedIPStatus(fromAPIStatus: dipServer.status)

switch status {
case .active:

guard let firstServer = Client.providers.serverProvider.currentServers.first(where: {$0.regionIdentifier == dipServer.id}) else {
callback?([], ClientError.malformedResponseData)
return
}

guard let ip = dipServer.ip, let cn = dipServer.cn, let expirationTime = dipServer.dip_expire else {
callback?([], ClientError.malformedResponseData)
return
}

let dipToken = dipServer.dipToken

let expiringDate = Date(timeIntervalSince1970: TimeInterval(expirationTime))
let server = Server.ServerAddressIP(ip: ip, cn: cn, van: false)

if let nextDays = Calendar.current.date(byAdding: .day, value: 5, to: Date()), nextDays >= expiringDate {
//Expiring in 5 days or less
Macros.postNotification(.PIADIPRegionExpiring, [.token : dipToken])
}

Macros.postNotification(.PIADIPCheckIP, [.token : dipToken, .ip : ip])

let dipUsername = "dedicated_ip_"+dipServer.dipToken+"_"+String.random(length: 8)

let dipRegion = Server(serial: firstServer.serial, name: firstServer.name, country: firstServer.country, hostname: firstServer.hostname, openVPNAddressesForTCP: [server], openVPNAddressesForUDP: [server], wireGuardAddressesForUDP: [server], iKEv2AddressesForUDP: [server], pingAddress: firstServer.pingAddress, geo: false, meta: nil, dipExpire: expiringDate, dipToken: dipServer.dipToken, dipStatus: status, dipUsername: dipUsername, regionIdentifier: firstServer.regionIdentifier)

dipRegions.append(dipRegion)

Client.database.secure.setDIPToken(dipServer.dipToken)
Client.database.secure.setPassword(ip, forDipToken: dipUsername)

default:

let dipRegion = Server(serial: "", name: "", country: "", hostname: "", openVPNAddressesForTCP: [], openVPNAddressesForUDP: [], wireGuardAddressesForUDP: [], iKEv2AddressesForUDP: [], pingAddress: nil, geo: false, meta: nil, dipExpire: nil, dipToken: nil, dipStatus: status, dipUsername: nil, regionIdentifier: "")
dipRegions.append(dipRegion)

}

}
callback?(dipRegions, nil)
}
}

#if os(iOS) || os(tvOS)
func signup(with request: Signup, _ callback: ((Credentials?, Error?) -> Void)?) {
var marketingJSON = ""
2 changes: 0 additions & 2 deletions Sources/PIALibrary/WebServices/WebServices.swift
Original file line number Diff line number Diff line change
@@ -46,8 +46,6 @@ protocol WebServices: class {

func handleDIPTokenExpiration(dipToken: String, _ callback: SuccessLibraryCallback?)

func activateDIPToken(tokens: [String], _ callback: LibraryCallback<[Server]>?)

#if os(iOS) || os(tvOS)
func signup(with request: Signup, _ callback: LibraryCallback<Credentials>?)
#endif
224 changes: 224 additions & 0 deletions Tests/PIALibraryTests/Accounts/GetDedicatedIPsUseCaseTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
//
// GetDedicatedIPsUseCaseTests.swift
//
//
// Created by Said Rehouni on 20/6/24.
//

import XCTest
@testable import PIALibrary

final class GetDedicatedIPsUseCaseTests: XCTestCase {
class Fixture {
var networkClientMock = NetworkRequestClientMock()
let refreshAuthTokensCheckerMock = RefreshAuthTokensCheckerMock()

func stubRequestWithResponse(_ response: NetworkRequestResponseType) {
networkClientMock.executeRequestResponse = response
}

func stubRequestWithError(_ error: NetworkRequestError) {
networkClientMock.executeRequestError = error
}
}

var fixture: Fixture!
var sut: GetDedicatedIPsUseCase!
var capturedResult: Result<[DedicatedIPInformation], NetworkRequestError>!

override func setUp() {
fixture = Fixture()
}

override func tearDown() {
fixture = nil
sut = nil
capturedResult = nil
}

private func instantiateSut() {
sut = GetDedicatedIPsUseCase(networkClient: fixture.networkClientMock,
refreshAuthTokensChecker: fixture.refreshAuthTokensCheckerMock)
}

func test_getDedicatedIPs_completes_with_credentials_succesfully_when_response_is_valid() {
// GIVEN Network client completes with no error and a valid response
let data = """
{
"result" : [
{
"id" : "001",
"ip" : "1.1.1",
"cn" : "cn",
"groups" : [],
"dip_expire" : 13123,
"dip_token" : "asdsad",
"status" : "active"
},
{
"id" : "002",
"ip" : "1.1.2",
"cn" : "cn2",
"groups" : [],
"dip_expire" : 13123,
"dip_token" : "asdsad",
"status" : "active"
},
]
}
""".data(using: .utf8)

fixture.stubRequestWithResponse(NetworkRequestResponseStub(data: data))
instantiateSut()

let expectation = expectation(description: "Waiting for get dedicated ips to finish")

// WHEN get dedicated ips is executed
sut(dipTokens: ["asd", "ffre"]) { [weak self] result in
self?.capturedResult = result
expectation.fulfill()
}

// THEN completes with allConnectionAttemptsFailed error
wait(for: [expectation], timeout: 1.0)
guard case .success(let servers) = capturedResult else {
XCTFail("Expected success got failure")
return
}

let sortedServers = servers.sorted { ($0.id ?? "") < ($1.id ?? "") }
XCTAssertEqual(sortedServers[0].id, "001")
XCTAssertEqual(sortedServers[0].ip, "1.1.1")
XCTAssertEqual(sortedServers[0].cn, "cn")
XCTAssertEqual(sortedServers[0].groups, [])
XCTAssertEqual(sortedServers[0].dipExpire, 13123)
XCTAssertEqual(sortedServers[0].dipToken, "asdsad")
XCTAssertEqual(sortedServers[0].status, .active)

XCTAssertEqual(sortedServers[1].id, "002")
XCTAssertEqual(sortedServers[1].ip, "1.1.2")
XCTAssertEqual(sortedServers[1].cn, "cn2")
XCTAssertEqual(sortedServers[1].groups, [])
XCTAssertEqual(sortedServers[1].dipExpire, 13123)
XCTAssertEqual(sortedServers[1].dipToken, "asdsad")
XCTAssertEqual(sortedServers[1].status, .active)
}

func test_getDedicatedIPs_completes_with_a_allConnectionAttemptsFailed_error_when_there_is_no_error_and_no_response() {
// GIVEN Network client completes with no error and no response
instantiateSut()

let expectation = expectation(description: "Waiting for get dedicated ips to finish")

// WHEN get dedicated ips is executed
sut(dipTokens: []) { [weak self] result in
self?.capturedResult = result
expectation.fulfill()
}

// THEN completes with allConnectionAttemptsFailed error
wait(for: [expectation], timeout: 1.0)
guard case .failure(let error) = capturedResult else {
XCTFail("Expected failure got success")
return
}

XCTAssertEqual(error, .allConnectionAttemptsFailed())
}

func test_getDedicatedIPs_completes_with_a_noDataContent_error_when_there_is_response_with_no_data() {
// GIVEN Network client completes with a response with invalid data
fixture.stubRequestWithResponse(NetworkRequestResponseStub(data: nil))
instantiateSut()

let expectation = expectation(description: "Waiting for get dedicated ips to finish")

// WHEN get dedicated ips is executed
sut(dipTokens: []) { [weak self] result in
self?.capturedResult = result
expectation.fulfill()
}

// THEN completes with allConnectionAttemptsFailed error
wait(for: [expectation], timeout: 1.0)
guard case .failure(let error) = capturedResult else {
XCTFail("Expected failure got success")
return
}

XCTAssertEqual(error, .noDataContent)
}

func test_getDedicatedIPs_completes_with_a_unableToDecodeDataContent_error_when_there_is_response_with_invalid_data() {
// GIVEN Network client completes with a response with invalid data
let data = "{ \"status\" : \"status\", \"user\" : \"username\", \"pass\" : \"password\"}"
.data(using: .utf8)

fixture.stubRequestWithResponse(NetworkRequestResponseStub(data: data))
instantiateSut()

let expectation = expectation(description: "Waiting for get dedicated ips to finish")

// WHEN get dedicated ips is executed
sut(dipTokens: []) { [weak self] result in
self?.capturedResult = result
expectation.fulfill()
}

// THEN completes with allConnectionAttemptsFailed error
wait(for: [expectation], timeout: 1.0)
guard case .failure(let error) = capturedResult else {
XCTFail("Expected failure got success")
return
}

XCTAssertEqual(error, .unableToDecodeDataContent)
}

func test_getDedicatedIPs_completes_with_an_unauthorized_error_when_there_is_401_status_code() {
// GIVEN Network client completes with an 401 status code error
instantiateSut()
fixture.stubRequestWithError(.connectionError(statusCode: 401, message: "any message"))
let expectation = expectation(description: "Waiting for get dedicated ips to finish")

// WHEN get dedicated ips is executed
sut(dipTokens: []) { [weak self] result in
self?.capturedResult = result
expectation.fulfill()
}

// THEN completes with unauthorized error
wait(for: [expectation], timeout: 1.0)
guard case .failure(let error) = capturedResult else {
XCTFail("Expected failure got success")
return
}

XCTAssertEqual(error, .unauthorized)
}

func test_getDedicatedIPs_creates_valid_networkConfiguration() {
// GIVEN
let expectedBody = try? JSONEncoder().encode(["tokens": ["001", "002"]])
instantiateSut()

// WHEN get dedicated ips is executed
sut.callAsFunction(dipTokens: ["001", "002"]) { _ in }

// THEN
guard let capturedConfiguration = fixture.networkClientMock.executeRequestWithConfiguation as? GetDedicatedIPsRequestConfiguration else {
XCTFail("Expected GetDedicatedIPsRequestConfiguration configuration")
return
}

XCTAssertEqual(capturedConfiguration.networkRequestModule, .account)
XCTAssertEqual(capturedConfiguration.path, .dedicatedIp)
XCTAssertEqual(capturedConfiguration.httpMethod, .post)
XCTAssertTrue(capturedConfiguration.inlcudeAuthHeaders)
XCTAssertEqual(capturedConfiguration.contentType, .json)
XCTAssertNil(capturedConfiguration.urlQueryParameters)
XCTAssertEqual(capturedConfiguration.responseDataType, .jsonData)
XCTAssertEqual(capturedConfiguration.timeout, 10)
XCTAssertEqual(capturedConfiguration.body?.count, expectedBody?.count)
}
}
129 changes: 129 additions & 0 deletions Tests/PIALibraryTests/Accounts/RenewDedicatedIPUseCaseTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@

import XCTest
@testable import PIALibrary

final class RenewDedicatedIPUseCaseTests: XCTestCase {
class Fixture {
var networkClientMock = NetworkRequestClientMock()
let refreshAuthTokensCheckerMock = RefreshAuthTokensCheckerMock()

func stubRequestWithResponse(_ response: NetworkRequestResponseType) {
networkClientMock.executeRequestResponse = response
}

func stubRequestWithError(_ error: NetworkRequestError) {
networkClientMock.executeRequestError = error
}
}

var fixture: Fixture!
var sut: RenewDedicatedIPUseCase!
var capturedResult: Result<Void, NetworkRequestError>!

override func setUp() {
fixture = Fixture()
}

override func tearDown() {
fixture = nil
sut = nil
capturedResult = nil
}

private func instantiateSut() {
sut = RenewDedicatedIPUseCase(networkClient: fixture.networkClientMock,
refreshAuthTokensChecker: fixture.refreshAuthTokensCheckerMock)
}

func test_renewDedicatedIPs_completes_with_success_when_there_is_no_error() {
// GIVEN Network client completes with no error
fixture.stubRequestWithResponse(NetworkRequestResponseStub(data: nil))
instantiateSut()

let expectation = expectation(description: "Waiting for renew dedicated IP to finish")

// WHEN renewDedicatedIPs is executed
sut(dipToken: "dipToken") { [weak self] result in
self?.capturedResult = result
expectation.fulfill()
}

// THEN completes with success
wait(for: [expectation], timeout: 1.0)
guard case .success = capturedResult else {
XCTFail("Expected success, got failure")
return
}
}

func test_renewDedicatedIPs_completes_with_an_unauthorized_error_when_there_is_401_status_code() {
// GIVEN Network client completes with an 401 status code error
instantiateSut()
fixture.stubRequestWithError(.connectionError(statusCode: 401, message: "any message"))
let expectation = expectation(description: "Waiting for renew dedicated IP to finish")

// WHEN renewDedicatedIPs is executed
sut(dipToken: "dipToken") { [weak self] result in
self?.capturedResult = result
expectation.fulfill()
}

// THEN completes with unauthorized error
wait(for: [expectation], timeout: 1.0)
guard case .failure(let error) = capturedResult else {
XCTFail("Expected failure got success")
return
}

XCTAssertEqual(error, .unauthorized)
}

func test_renewDedicatedIPs_completes_with_an_error_when_network_client_completes_with_a_non_401_error() {
// GIVEN Network client completes with a non 401 error
fixture.stubRequestWithError(.allConnectionAttemptsFailed(statusCode: 404))
instantiateSut()

let expectation = expectation(description: "Waiting for renew dedicated IP to finish")

// WHEN renewDedicatedIPs is executed
sut(dipToken: "dipToken") { [weak self] result in
self?.capturedResult = result
expectation.fulfill()
}

// THEN completes with allConnectionAttemptsFailed error
wait(for: [expectation], timeout: 1.0)
guard case .failure(let error) = capturedResult else {
XCTFail("Expected failure got success")
return
}

XCTAssertEqual(error, .allConnectionAttemptsFailed(statusCode: 404))
}

func test_renewDedicatedIPs_creates_valid_networkConfiguration() {
// GIVEN
let expectedBody = try? JSONEncoder().encode(["token": "001"])
instantiateSut()

// WHEN renewDedicatedIPs is executed
sut.callAsFunction(dipToken: "001") { _ in }

// THEN
guard let capturedConfiguration = fixture.networkClientMock.executeRequestWithConfiguation as? RenewDedicatedIPRequestConfiguration else {
XCTFail("Expected RenewDedicatedIPRequestConfiguration configuration")
return
}

XCTAssertEqual(capturedConfiguration.networkRequestModule, .account)
XCTAssertEqual(capturedConfiguration.path, .renewDedicatedIp)
XCTAssertEqual(capturedConfiguration.httpMethod, .post)
XCTAssertTrue(capturedConfiguration.inlcudeAuthHeaders)
XCTAssertEqual(capturedConfiguration.contentType, .json)
XCTAssertNil(capturedConfiguration.urlQueryParameters)
XCTAssertEqual(capturedConfiguration.responseDataType, .jsonData)
XCTAssertEqual(capturedConfiguration.timeout, 10)
XCTAssertEqual(capturedConfiguration.body?.count, expectedBody?.count)
}

}

0 comments on commit 2b8ddeb

Please sign in to comment.