diff --git a/Sources/PIALibrary/Account/CompositionRoot/AccountFactory.swift b/Sources/PIALibrary/Account/CompositionRoot/AccountFactory.swift index f0216980..30231de6 100644 --- a/Sources/PIALibrary/Account/CompositionRoot/AccountFactory.swift +++ b/Sources/PIALibrary/Account/CompositionRoot/AccountFactory.swift @@ -43,7 +43,8 @@ public class AccountFactory { accountDetailsUseCase: makeAccountDetailsUseCase(), updateAccountUseCase: makeUpdateAccountUseCase(), paymentUseCase: makePaymentUseCase(), - subscriptionsUseCase: makeSubscriptionsUseCase() + subscriptionsUseCase: makeSubscriptionsUseCase(), + deleteAccountUseCase: makeDeleteAccountUseCase() ) } @@ -114,4 +115,8 @@ private extension AccountFactory { PaymentInformationDataConverter() } + static func makeDeleteAccountUseCase() -> DeleteAccountUseCaseType { + DeleteAccountUseCase(networkClient: NetworkRequestFactory.maketNetworkRequestClient(), refreshAuthTokenChecker: makeRefreshAuthTokensChecker(), apiTokenProvider: makeAPITokenProvider(), vpnTokenProvider: makeVpnTokenProvider()) + } + } diff --git a/Sources/PIALibrary/Account/Data/Networking/DeleteAccountRequestConfiguration.swift b/Sources/PIALibrary/Account/Data/Networking/DeleteAccountRequestConfiguration.swift new file mode 100644 index 00000000..5051ead9 --- /dev/null +++ b/Sources/PIALibrary/Account/Data/Networking/DeleteAccountRequestConfiguration.swift @@ -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") +} + + + diff --git a/Sources/PIALibrary/Account/DefaultAccountProvider.swift b/Sources/PIALibrary/Account/DefaultAccountProvider.swift index 971f3bd0..ba5da3df 100644 --- a/Sources/PIALibrary/Account/DefaultAccountProvider.swift +++ b/Sources/PIALibrary/Account/DefaultAccountProvider.swift @@ -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 @@ -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 { @@ -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?) { diff --git a/Sources/PIALibrary/Account/Domain/UseCases/DeleteAccountUseCase.swift b/Sources/PIALibrary/Account/Domain/UseCases/DeleteAccountUseCase.swift new file mode 100644 index 00000000..fea79a43 --- /dev/null +++ b/Sources/PIALibrary/Account/Domain/UseCases/DeleteAccountUseCase.swift @@ -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) + } + + + } + } +} diff --git a/Sources/PIALibrary/WebServices/PIAWebServices.swift b/Sources/PIALibrary/WebServices/PIAWebServices.swift index c2919e1a..fb523f29 100644 --- a/Sources/PIALibrary/WebServices/PIAWebServices.swift +++ b/Sources/PIALibrary/WebServices/PIAWebServices.swift @@ -213,16 +213,6 @@ class PIAWebServices: WebServices, ConfigurationAccess { } } - func deleteAccount(_ callback: LibraryCallback?) { - 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 { diff --git a/Sources/PIALibrary/WebServices/WebServices.swift b/Sources/PIALibrary/WebServices/WebServices.swift index 857862ad..157de395 100644 --- a/Sources/PIALibrary/WebServices/WebServices.swift +++ b/Sources/PIALibrary/WebServices/WebServices.swift @@ -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?) #if os(iOS) || os(tvOS) func signup(with request: Signup, _ callback: LibraryCallback?) diff --git a/Tests/PIALibraryTests/Accounts/DeleteAccountUseCaseTests.swift b/Tests/PIALibraryTests/Accounts/DeleteAccountUseCaseTests.swift new file mode 100644 index 00000000..32dd54bf --- /dev/null +++ b/Tests/PIALibraryTests/Accounts/DeleteAccountUseCaseTests.swift @@ -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) + + } + +}