Skip to content

Commit

Permalink
PIA-1932: Migrate delete account API to native
Browse files Browse the repository at this point in the history
  • Loading branch information
kp-laura-sempere committed Jun 25, 2024
1 parent 48ea3f2 commit a0321c5
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public class AccountFactory {
accountDetailsUseCase: makeAccountDetailsUseCase(),
updateAccountUseCase: makeUpdateAccountUseCase(),
paymentUseCase: makePaymentUseCase(),
subscriptionsUseCase: makeSubscriptionsUseCase()
subscriptionsUseCase: makeSubscriptionsUseCase(),
deleteAccountUseCase: makeDeleteAccountUseCase()
)

}
Expand Down Expand Up @@ -114,4 +115,8 @@ private extension AccountFactory {
PaymentInformationDataConverter()
}

static func makeDeleteAccountUseCase() -> DeleteAccountUseCaseType {
DeleteAccountUseCase(networkClient: NetworkRequestFactory.maketNetworkRequestClient(), refreshAuthTokenChecker: makeRefreshAuthTokensChecker(), apiTokenProvider: makeAPITokenProvider(), vpnTokenProvider: makeVpnTokenProvider())
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@


import Foundation
import NWHttpConnection

struct DeleteAccountRequestConfiguration: NetworkRequestConfigurationType {

let networkRequestModule: NetworkRequestModule = .account
let path: RequestAPI.Path = .deleteAccount
let httpMethod: NWHttpConnection.NWConnectionHTTPMethod = .delete
let contentType: NetworkRequestContentType = .json
let inlcudeAuthHeaders: Bool = true
let urlQueryParameters: [String : String]? = nil
let responseDataType: NWDataResponseType = .jsonData

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

let timeout: TimeInterval = 10
let requestQueue: DispatchQueue? = DispatchQueue(label: "deleteAccount_request.queue")
}



15 changes: 9 additions & 6 deletions Sources/PIALibrary/Account/DefaultAccountProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ open class DefaultAccountProvider: AccountProvider, ConfigurationAccess, Databas
private let updateAccountUseCase: UpdateAccountUseCaseType
private let paymentUseCase: PaymentUseCaseType
private let subscriptionsUseCase: SubscriptionsUseCaseType
private let deleteAccountUseCase: DeleteAccountUseCaseType


init(webServices: WebServices? = nil, logoutUseCase: LogoutUseCaseType, loginUseCase: LoginUseCaseType, signupUseCase: SignupUseCaseType, apiTokenProvider: APITokenProviderType, vpnTokenProvider: VpnTokenProviderType, accountDetailsUseCase: AccountDetailsUseCaseType, updateAccountUseCase: UpdateAccountUseCaseType, paymentUseCase: PaymentUseCaseType, subscriptionsUseCase: SubscriptionsUseCaseType) {
init(webServices: WebServices? = nil, logoutUseCase: LogoutUseCaseType, loginUseCase: LoginUseCaseType, signupUseCase: SignupUseCaseType, apiTokenProvider: APITokenProviderType, vpnTokenProvider: VpnTokenProviderType, accountDetailsUseCase: AccountDetailsUseCaseType, updateAccountUseCase: UpdateAccountUseCaseType, paymentUseCase: PaymentUseCaseType, subscriptionsUseCase: SubscriptionsUseCaseType, deleteAccountUseCase: DeleteAccountUseCaseType) {
self.logoutUseCase = logoutUseCase
self.loginUseCase = loginUseCase
self.signupUseCase = signupUseCase
Expand All @@ -52,6 +53,7 @@ open class DefaultAccountProvider: AccountProvider, ConfigurationAccess, Databas
self.updateAccountUseCase = updateAccountUseCase
self.paymentUseCase = paymentUseCase
self.subscriptionsUseCase = subscriptionsUseCase
self.deleteAccountUseCase = deleteAccountUseCase
if let webServices = webServices {
customWebServices = webServices
} else {
Expand Down Expand Up @@ -424,13 +426,14 @@ open class DefaultAccountProvider: AccountProvider, ConfigurationAccess, Databas
guard isLoggedIn else {
preconditionFailure()
}
webServices.deleteAccount { (result, error) in
guard let result = result, result != false else {
callback?(error)
return

deleteAccountUseCase() { error in
DispatchQueue.main.async {
callback?(error?.asClientError())
}
callback?(nil)

}

}

public func featureFlags(_ callback: SuccessLibraryCallback?) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@

import Foundation


protocol DeleteAccountUseCaseType {
typealias Completion = ((NetworkRequestError?) -> Void)
func callAsFunction(completion: @escaping Completion)
}


class DeleteAccountUseCase: DeleteAccountUseCaseType {

let networkClient: NetworkRequestClientType
let refreshAuthTokenChecker: RefreshAuthTokensCheckerType
let apiTokenProvider: APITokenProviderType
let vpnTokenProvider: VpnTokenProviderType

init(networkClient: NetworkRequestClientType, refreshAuthTokenChecker: RefreshAuthTokensCheckerType, apiTokenProvider: APITokenProviderType, vpnTokenProvider: VpnTokenProviderType) {
self.networkClient = networkClient
self.refreshAuthTokenChecker = refreshAuthTokenChecker
self.apiTokenProvider = apiTokenProvider
self.vpnTokenProvider = vpnTokenProvider
}


func callAsFunction(completion: @escaping Completion) {
refreshAuthTokenChecker.refreshIfNeeded { [weak self] error in
guard let self else { return }
if let error {
completion(error)
} else {
self.executeRequest(with: completion)
}
}
}
}


extension DeleteAccountUseCase {
func executeRequest(with completion: @escaping Completion) {
let configuration = DeleteAccountRequestConfiguration()
networkClient.executeRequest(with: configuration) { [weak self] error, response in

guard let self else { return }

if let error {
completion(error)
} else {
self.apiTokenProvider.clearAPIToken()
self.vpnTokenProvider.clearVpnToken()
completion(nil)
}


}
}
}
10 changes: 0 additions & 10 deletions Sources/PIALibrary/WebServices/PIAWebServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,16 +213,6 @@ class PIAWebServices: WebServices, ConfigurationAccess {
}
}

func deleteAccount(_ callback: LibraryCallback<Bool>?) {
self.accountAPI.deleteAccount(callback: { errors in
if !errors.isEmpty {
callback?(false, ClientError.invalidParameter)
} else {
callback?(true, nil)
}
})
}

func handleDIPTokenExpiration(dipToken: String, _ callback: SuccessLibraryCallback?) {
self.accountAPI.renewDedicatedIP(ipToken: dipToken) { (errors) in
if !errors.isEmpty {
Expand Down
6 changes: 0 additions & 6 deletions Sources/PIALibrary/WebServices/WebServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,6 @@ protocol WebServices: class {
func handleDIPTokenExpiration(dipToken: String, _ callback: SuccessLibraryCallback?)

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

/**
Deletes the user accout on PIA servers.
- Parameter callback: Returns an `Bool` if the API returns a success.
*/
func deleteAccount(_ callback: LibraryCallback<Bool>?)

#if os(iOS) || os(tvOS)
func signup(with request: Signup, _ callback: LibraryCallback<Credentials>?)
Expand Down
149 changes: 149 additions & 0 deletions Tests/PIALibraryTests/Accounts/DeleteAccountUseCaseTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import XCTest
@testable import PIALibrary

class DeleteAccountUseCaseTests: XCTestCase {
class Fixture {
let networkClientMock = NetworkRequestClientMock()
let refreshAuthTokenCheckerMock = RefreshAuthTokensCheckerMock()
let apiTokenProviderMock = APITokenProviderMock()
let vpnTokenProviderMock = VpnTokenProviderMock()

func stubNetworkRequestSuccessfulResponse() {
networkClientMock.executeRequestResponse = NetworkRequestResponseMock(statusCode: 200)
networkClientMock.executeRequestError = nil
}

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

func stubRefreshAuthTokensWithSuccess() {
refreshAuthTokenCheckerMock.refreshIfNeededError = nil
}

func stubRefreshAuthTokensFailsWithError(_ error: NetworkRequestError) {
refreshAuthTokenCheckerMock.refreshIfNeededError = error
}
}

var fixture: Fixture!
var sut: DeleteAccountUseCase!

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

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

private func instantiateSut() {
sut = DeleteAccountUseCase(networkClient: fixture.networkClientMock, refreshAuthTokenChecker: fixture.refreshAuthTokenCheckerMock, apiTokenProvider: fixture.apiTokenProviderMock, vpnTokenProvider: fixture.vpnTokenProviderMock)
}

func test_delete_account_when_network_request_succeeds() {
// GIVEN that the network request succeeds
fixture.stubNetworkRequestSuccessfulResponse()
// AND refreshing the auth tokens also succeeds
fixture.stubRefreshAuthTokensWithSuccess()

instantiateSut()

let expectation = expectation(description: "Delete account request is executed")
var capturedError: NetworkRequestError?
// WHEN exectuting the delete account request
sut() { error in
capturedError = error
expectation.fulfill()
}

wait(for: [expectation], timeout: 3)

// THEN the refresh auth tokens if needed request is called
XCTAssertEqual(fixture.refreshAuthTokenCheckerMock.refreshIfNeededCalledAttempt, 1)

let executedRequest = fixture.networkClientMock.executeRequestWithConfiguation!
// AND the delete account request is executed
XCTAssertEqual(fixture.networkClientMock.executeRequestCalledAttempt, 1)
XCTAssertEqual(executedRequest.path, RequestAPI.Path.deleteAccount)
XCTAssertEqual(executedRequest.httpMethod, .delete)

// AND no error is retured
XCTAssertNil(capturedError)

// AND the auth tokens ARE removed
XCTAssertEqual(fixture.apiTokenProviderMock.clearAPITokenCalledAttempt, 1)
XCTAssertEqual(fixture.vpnTokenProviderMock.clearVpnTokenCalledAttempt, 1)

}

func test_delete_account_when_network_request_fails() {
// GIVEN that the network request fails
fixture.stubNetworkRequestResponseWithError(.allConnectionAttemptsFailed(statusCode: 401))
// AND refreshing the auth tokens succeeds
fixture.stubRefreshAuthTokensWithSuccess()

instantiateSut()

let expectation = expectation(description: "Delete account request is executed")
var capturedError: NetworkRequestError?
// WHEN exectuting the delete account request
sut() { error in
capturedError = error
expectation.fulfill()
}

wait(for: [expectation], timeout: 3)

// THEN the refresh auth tokens if needed request is called
XCTAssertEqual(fixture.refreshAuthTokenCheckerMock.refreshIfNeededCalledAttempt, 1)

let executedRequest = fixture.networkClientMock.executeRequestWithConfiguation!
// AND the delete account request is executed
XCTAssertEqual(fixture.networkClientMock.executeRequestCalledAttempt, 1)
XCTAssertEqual(executedRequest.path, RequestAPI.Path.deleteAccount)
XCTAssertEqual(executedRequest.httpMethod, .delete)

// AND an error IS retured
XCTAssertNotNil(capturedError)

// AND the auth tokens are NOT removed
XCTAssertEqual(fixture.apiTokenProviderMock.clearAPITokenCalledAttempt, 0)
XCTAssertEqual(fixture.vpnTokenProviderMock.clearVpnTokenCalledAttempt, 0)

}

func test_delete_account_when_refreshAuthTokens_request_fails() {

// GIVEN that refreshing the auth tokens fails
fixture.stubRefreshAuthTokensFailsWithError(.allConnectionAttemptsFailed(statusCode: 401))

instantiateSut()

let expectation = expectation(description: "Delete account request is executed")
var capturedError: NetworkRequestError?
// WHEN exectuting the delete account request
sut() { error in
capturedError = error
expectation.fulfill()
}

wait(for: [expectation], timeout: 3)

// THEN the refresh auth tokens if needed request is called
XCTAssertEqual(fixture.refreshAuthTokenCheckerMock.refreshIfNeededCalledAttempt, 1)

// AND the delete account request is NOT executed
XCTAssertEqual(fixture.networkClientMock.executeRequestCalledAttempt, 0)

// AND an error IS retured
XCTAssertNotNil(capturedError)

// AND the auth tokens are NOT removed
XCTAssertEqual(fixture.apiTokenProviderMock.clearAPITokenCalledAttempt, 0)
XCTAssertEqual(fixture.vpnTokenProviderMock.clearVpnTokenCalledAttempt, 0)

}

}

0 comments on commit a0321c5

Please sign in to comment.