diff --git a/Sources/PIALibrary/Account/DefaultAccountProvider.swift b/Sources/PIALibrary/Account/DefaultAccountProvider.swift index b1845773..6c7ad82f 100644 --- a/Sources/PIALibrary/Account/DefaultAccountProvider.swift +++ b/Sources/PIALibrary/Account/DefaultAccountProvider.swift @@ -628,7 +628,7 @@ open class DefaultAccountProvider: AccountProvider, ConfigurationAccess, Databas return } - paymentUseCase.processPayment(with: user.credentials, request: payment) { (error) in + paymentUseCase(with: user.credentials, request: payment) { (error) in log.debug("Payment processed with error: \(error)") diff --git a/Sources/PIALibrary/Account/Domain/UseCases/PaymentUseCase.swift b/Sources/PIALibrary/Account/Domain/UseCases/PaymentUseCase.swift index 0f5fdb57..ef37b6a7 100644 --- a/Sources/PIALibrary/Account/Domain/UseCases/PaymentUseCase.swift +++ b/Sources/PIALibrary/Account/Domain/UseCases/PaymentUseCase.swift @@ -6,7 +6,7 @@ private let log = SwiftyBeaver.self protocol PaymentUseCaseType { typealias Completion = ((NetworkRequestError?) -> Void) - func processPayment(with credentials: Credentials, request: Payment, completion: @escaping Completion) + func callAsFunction(with credentials: Credentials, request: Payment, completion: @escaping Completion) } @@ -19,7 +19,7 @@ class PaymentUseCase: PaymentUseCaseType { self.paymentInformationDataConverter = paymentInformationDataConverter } - func processPayment(with credentials: Credentials, request: Payment, completion: @escaping Completion) { + func callAsFunction(with credentials: Credentials, request: Payment, completion: @escaping Completion) { var configuration = PaymentRequestConfiguration() @@ -59,7 +59,7 @@ private extension PaymentUseCase { } private func handleErrorResponse(_ error: NetworkRequestError, completion: @escaping Completion) { - if case .connectionError(statusCode: let statusCode, message: let message) = error, statusCode == 400 { + if case .allConnectionAttemptsFailed(statusCode: let statusCode) = error, statusCode == 400 { completion(NetworkRequestError.badReceipt) return } diff --git a/Tests/PIALibraryTests/Accounts/PaymentUseCaseTests.swift b/Tests/PIALibraryTests/Accounts/PaymentUseCaseTests.swift new file mode 100644 index 00000000..da8e2568 --- /dev/null +++ b/Tests/PIALibraryTests/Accounts/PaymentUseCaseTests.swift @@ -0,0 +1,155 @@ +import XCTest +@testable import PIALibrary + +class PaymentUseCaseTests: XCTestCase { + class Fixture { + let networkClientMock = NetworkRequestClientMock() + let paymentInformationDataConverter = PaymentInformationDataConverter() + let credentials = Credentials(username: "username", password: "password") + let payment = Payment(receipt: Data()) + + func stubSuccessFullNetworkRequest() { + let successResponse = NetworkRequestResponseMock(statusCode: 200, data: Data()) + networkClientMock.executeRequestResponse = successResponse + networkClientMock.executeRequestError = nil + } + + func stubNetworRequestError(_ error: NetworkRequestError) { + networkClientMock.executeRequestError = error + } + } + + var fixture: Fixture! + var sut: PaymentUseCase! + + override func setUp() { + fixture = Fixture() + } + + override func tearDown() { + fixture = nil + sut = nil + } + + private func instantiateSut() { + sut = PaymentUseCase(networkClient: fixture.networkClientMock, paymentInformationDataConverter: fixture.paymentInformationDataConverter) + } + + func test_payment_when_network_request_succeeds() { + // GIVEN that the payment request succeeds + fixture.stubSuccessFullNetworkRequest() + + instantiateSut() + + let expectation = expectation(description: "Payment request is executed") + var capturedError: NetworkRequestError? + + // WHEN executing the request + sut.callAsFunction(with: fixture.credentials, request: fixture.payment) { error in + capturedError = error + expectation.fulfill() + } + + wait(for: [expectation], timeout: 3) + + let executedRequestConfiguration = fixture.networkClientMock.executeRequestWithConfiguation! + + // THEN the payment request is executed + XCTAssertEqual(fixture.networkClientMock.executeRequestCalledAttempt, 1) + XCTAssertEqual(executedRequestConfiguration.path, RequestAPI.Path.iosPayment) + + let userPassToBase64 = "username:password".toBase64()! + let otherHeaders = ["Authorization": "Basic \(userPassToBase64)"] + + // WITH the Basic Authorization Header + XCTAssertEqual(executedRequestConfiguration.otherHeaders!, otherHeaders) + + let requestBody = fixture.paymentInformationDataConverter(payment: fixture.payment)! + + // AND the payment info object encoded int he body + XCTAssertEqual(executedRequestConfiguration.body!.count, requestBody.count) + + // AND no error is retured + XCTAssertNil(capturedError) + + } + + func test_payment_when_network_request_failsWith401() { + // GIVEN that the payment request fails + fixture.stubNetworRequestError(.allConnectionAttemptsFailed(statusCode: 401)) + + instantiateSut() + + let expectation = expectation(description: "Payment request is executed") + var capturedError: NetworkRequestError? + + // WHEN executing the request + sut.callAsFunction(with: fixture.credentials, request: fixture.payment) { error in + capturedError = error + expectation.fulfill() + } + + wait(for: [expectation], timeout: 3) + + let executedRequestConfiguration = fixture.networkClientMock.executeRequestWithConfiguation! + + // THEN the payment request is executed + XCTAssertEqual(fixture.networkClientMock.executeRequestCalledAttempt, 1) + XCTAssertEqual(executedRequestConfiguration.path, RequestAPI.Path.iosPayment) + + let userPassToBase64 = "username:password".toBase64()! + let otherHeaders = ["Authorization": "Basic \(userPassToBase64)"] + + // WITH the Basic Authorization Header + XCTAssertEqual(executedRequestConfiguration.otherHeaders!, otherHeaders) + + let requestBody = fixture.paymentInformationDataConverter(payment: fixture.payment)! + + // AND the payment info object encoded int he body + XCTAssertEqual(executedRequestConfiguration.body!.count, requestBody.count) + + // AND an Error is retured + XCTAssertNotNil(capturedError) + XCTAssertEqual(capturedError, .allConnectionAttemptsFailed(statusCode: 401)) + } + + func test_payment_when_network_request_failsWith400() { + // GIVEN that the payment request fails + fixture.stubNetworRequestError(.allConnectionAttemptsFailed(statusCode: 400)) + + instantiateSut() + + let expectation = expectation(description: "Payment request is executed") + var capturedError: NetworkRequestError? + + // WHEN executing the request + sut.callAsFunction(with: fixture.credentials, request: fixture.payment) { error in + capturedError = error + expectation.fulfill() + } + + wait(for: [expectation], timeout: 3) + + let executedRequestConfiguration = fixture.networkClientMock.executeRequestWithConfiguation! + + // THEN the payment request is executed + XCTAssertEqual(fixture.networkClientMock.executeRequestCalledAttempt, 1) + XCTAssertEqual(executedRequestConfiguration.path, RequestAPI.Path.iosPayment) + + let userPassToBase64 = "username:password".toBase64()! + let otherHeaders = ["Authorization": "Basic \(userPassToBase64)"] + + // WITH the Basic Authorization Header + XCTAssertEqual(executedRequestConfiguration.otherHeaders!, otherHeaders) + + let requestBody = fixture.paymentInformationDataConverter(payment: fixture.payment)! + + // AND the payment info object encoded int he body + XCTAssertEqual(executedRequestConfiguration.body!.count, requestBody.count) + + // AND an Error is retured + XCTAssertNotNil(capturedError) + XCTAssertEqual(capturedError, .badReceipt) + } + +} diff --git a/Tests/PIALibraryTests/Accounts/SubscriptionsUseCaseTests.swift b/Tests/PIALibraryTests/Accounts/SubscriptionsUseCaseTests.swift new file mode 100644 index 00000000..cb8ec05e --- /dev/null +++ b/Tests/PIALibraryTests/Accounts/SubscriptionsUseCaseTests.swift @@ -0,0 +1,320 @@ + +import XCTest +@testable import PIALibrary + +class SubscriptionsUseCaseTests: XCTestCase { + class Fixture { + let networkClientMock = NetworkRequestClientMock() + let refreshAuthTokensCheckerMock = RefreshAuthTokensCheckerMock() + let appStoreInformation = AppStoreInformation( + products: [ + Product(identifier: "id1", plan: .monthly, price: "10", legacy: true), + Product(identifier: "id2", plan: .yearly, price: "100", legacy: true) + ], + eligibleForTrial: true + ) + + let receiptBase64 = Data().base64EncodedString() + + var encodedAppStoreInformationData: Data { + try! JSONEncoder().encode(appStoreInformation) + } + + func stubSuccessfulRequestResponse() { + networkClientMock.executeRequestResponse = NetworkRequestResponseMock(statusCode: 200, data: encodedAppStoreInformationData) + networkClientMock.executeRequestError = nil + } + + func stubRequestResponseWithNoData() { + networkClientMock.executeRequestResponse = NetworkRequestResponseMock(statusCode: 200, data: nil) + } + + func stubRequestResponseWithInvalidAppStoreInfoData() { + networkClientMock.executeRequestResponse = NetworkRequestResponseMock(statusCode: 200, data: Data()) + } + + func stubNetworkRequestError(_ error: NetworkRequestError) { + networkClientMock.executeRequestError = error + } + + func stubRefreshAuthTokensError(_ error: NetworkRequestError) { + refreshAuthTokensCheckerMock.refreshIfNeededError = error + } + + + } + + var fixture: Fixture! + var sut: SubscriptionsUseCase! + + override func setUp() { + fixture = Fixture() + } + + override func tearDown() { + fixture = nil + sut = nil + } + + private func instantiateSut() { + sut = SubscriptionsUseCase(networkClient: fixture.networkClientMock, refreshAuthTokensChecker: fixture.refreshAuthTokensCheckerMock) + } + + func test_getSubscriptions_withoutReceipt_when_network_request_succeeds() { + // GIVEN that the network request succeeds + fixture.stubSuccessfulRequestResponse() + + instantiateSut() + + let expectation = expectation(description: "Subscriptions request is executed") + var capturedError: NetworkRequestError? + var capturedAppStoreInformation: AppStoreInformation? + + // WHEN executing the subscription request without receipt + sut.callAsFunction(receiptBase64: nil) { result in + switch result { + case .failure(let error): + capturedError = error + case .success(let appStoreInfo): + capturedAppStoreInformation = appStoreInfo + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 3) + + // THEN the refresh tokens if needed request is executed + XCTAssertEqual(fixture.refreshAuthTokensCheckerMock.refreshIfNeededCalledAttempt, 1) + + let executedRequestConfiguration = fixture.networkClientMock.executeRequestWithConfiguation! + + // AND the subscriptions request is executed + XCTAssertEqual(fixture.networkClientMock.executeRequestCalledAttempt, 1) + XCTAssertEqual(executedRequestConfiguration.path, RequestAPI.Path.iosSubscriptions) + + // WITH only the 'type' in the query parameters + XCTAssertEqual(executedRequestConfiguration.urlQueryParameters!, ["type": "subscription"]) + + // AND no error is returned + XCTAssertNil(capturedError) + + // AND the AppStoreInformation is returned with 2 products + XCTAssertEqual(capturedAppStoreInformation!.products.count, 2) + // AND eligible for free trial + XCTAssertTrue(capturedAppStoreInformation!.eligibleForTrial) + + } + + func test_getSubscriptions_withReceipt_when_network_request_succeeds() { + // GIVEN that the network request succeeds + fixture.stubSuccessfulRequestResponse() + + instantiateSut() + + let expectation = expectation(description: "Subscriptions request is executed") + var capturedError: NetworkRequestError? + var capturedAppStoreInformation: AppStoreInformation? + + // WHEN executing the subscription request with receipt + sut.callAsFunction(receiptBase64: fixture.receiptBase64) { result in + switch result { + case .failure(let error): + capturedError = error + case .success(let appStoreInfo): + capturedAppStoreInformation = appStoreInfo + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 3) + + // THEN the refresh tokens if needed request is executed + XCTAssertEqual(fixture.refreshAuthTokensCheckerMock.refreshIfNeededCalledAttempt, 1) + + let executedRequestConfiguration = fixture.networkClientMock.executeRequestWithConfiguation! + + // AND the subscriptions request is executed + XCTAssertEqual(fixture.networkClientMock.executeRequestCalledAttempt, 1) + XCTAssertEqual(executedRequestConfiguration.path, RequestAPI.Path.iosSubscriptions) + + // WITH 'type' and 'receipt' in the query parameters + XCTAssertEqual(executedRequestConfiguration.urlQueryParameters!, ["type": "subscription", "receipt": fixture.receiptBase64]) + + // AND no error is returned + XCTAssertNil(capturedError) + + // AND the AppStoreInformation is returned with 2 products + XCTAssertEqual(capturedAppStoreInformation!.products.count, 2) + // AND eligible for free trial + XCTAssertTrue(capturedAppStoreInformation!.eligibleForTrial) + + } + + func test_getSubscriptions_when_network_request_fails() { + // GIVEN that the network request fails + fixture.stubNetworkRequestError(.allConnectionAttemptsFailed(statusCode: 401)) + + instantiateSut() + + let expectation = expectation(description: "Subscriptions request is executed") + var capturedError: NetworkRequestError? + var capturedAppStoreInformation: AppStoreInformation? + + // WHEN executing the subscription request without receipt + sut.callAsFunction(receiptBase64: nil) { result in + switch result { + case .failure(let error): + capturedError = error + case .success(let appStoreInfo): + capturedAppStoreInformation = appStoreInfo + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 3) + + // THEN the refresh tokens if needed request is executed + XCTAssertEqual(fixture.refreshAuthTokensCheckerMock.refreshIfNeededCalledAttempt, 1) + + let executedRequestConfiguration = fixture.networkClientMock.executeRequestWithConfiguation! + + // AND the subscriptions request is executed + XCTAssertEqual(fixture.networkClientMock.executeRequestCalledAttempt, 1) + XCTAssertEqual(executedRequestConfiguration.path, RequestAPI.Path.iosSubscriptions) + + // AND an error is returned + XCTAssertNotNil(capturedError) + XCTAssertEqual(capturedError, .allConnectionAttemptsFailed(statusCode: 401)) + + // AND NO AppStoreInformation is returned + XCTAssertNil(capturedAppStoreInformation) + + } + + func test_getSubscriptions_when_no_data_is_returned() { + // GIVEN that the network request returns no data + fixture.stubRequestResponseWithNoData() + + instantiateSut() + + let expectation = expectation(description: "Subscriptions request is executed") + var capturedError: NetworkRequestError? + var capturedAppStoreInformation: AppStoreInformation? + + // WHEN executing the subscription request without receipt + sut.callAsFunction(receiptBase64: nil) { result in + switch result { + case .failure(let error): + capturedError = error + case .success(let appStoreInfo): + capturedAppStoreInformation = appStoreInfo + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 3) + + // THEN the refresh tokens if needed request is executed + XCTAssertEqual(fixture.refreshAuthTokensCheckerMock.refreshIfNeededCalledAttempt, 1) + + let executedRequestConfiguration = fixture.networkClientMock.executeRequestWithConfiguation! + + // AND the subscriptions request is executed + XCTAssertEqual(fixture.networkClientMock.executeRequestCalledAttempt, 1) + XCTAssertEqual(executedRequestConfiguration.path, RequestAPI.Path.iosSubscriptions) + + // AND an error is returned + XCTAssertNotNil(capturedError) + XCTAssertEqual(capturedError, .noDataContent) + + // AND NO AppStoreInformation is returned + XCTAssertNil(capturedAppStoreInformation) + + } + + func test_getSubscriptions_when_invalid_data_is_returned() { + // GIVEN that the network request returns invalid app store info data + fixture.stubRequestResponseWithInvalidAppStoreInfoData() + + instantiateSut() + + let expectation = expectation(description: "Subscriptions request is executed") + var capturedError: NetworkRequestError? + var capturedAppStoreInformation: AppStoreInformation? + + // WHEN executing the subscription request without receipt + sut.callAsFunction(receiptBase64: nil) { result in + switch result { + case .failure(let error): + capturedError = error + case .success(let appStoreInfo): + capturedAppStoreInformation = appStoreInfo + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 3) + + // THEN the refresh tokens if needed request is executed + XCTAssertEqual(fixture.refreshAuthTokensCheckerMock.refreshIfNeededCalledAttempt, 1) + + let executedRequestConfiguration = fixture.networkClientMock.executeRequestWithConfiguation! + + // AND the subscriptions request is executed + XCTAssertEqual(fixture.networkClientMock.executeRequestCalledAttempt, 1) + XCTAssertEqual(executedRequestConfiguration.path, RequestAPI.Path.iosSubscriptions) + + // AND an error is returned + XCTAssertNotNil(capturedError) + XCTAssertEqual(capturedError, .unableToDecodeData) + + // AND NO AppStoreInformation is returned + XCTAssertNil(capturedAppStoreInformation) + + } + + func test_getSubscriptions_when_refreshAuthTokensFails() { + // GIVEN that the network request succeeds + fixture.stubSuccessfulRequestResponse() + // AND GIVEN that refresh auth tokens request fails + fixture.stubRefreshAuthTokensError(.allConnectionAttemptsFailed(statusCode: 401)) + + instantiateSut() + + let expectation = expectation(description: "Subscriptions request is executed") + var capturedError: NetworkRequestError? + var capturedAppStoreInformation: AppStoreInformation? + + // WHEN executing the subscription request + sut.callAsFunction(receiptBase64: nil) { result in + switch result { + case .failure(let error): + capturedError = error + case .success(let appStoreInfo): + capturedAppStoreInformation = appStoreInfo + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 3) + + // THEN the refresh tokens if needed request is executed + XCTAssertEqual(fixture.refreshAuthTokensCheckerMock.refreshIfNeededCalledAttempt, 1) + + let executedRequestConfiguration = fixture.networkClientMock.executeRequestWithConfiguation! + + // AND the subscriptions request is executed + XCTAssertEqual(fixture.networkClientMock.executeRequestCalledAttempt, 1) + XCTAssertEqual(executedRequestConfiguration.path, RequestAPI.Path.iosSubscriptions) + + // AND no error is returned + XCTAssertNil(capturedError) + + // AND the AppStoreInformation is returned with 2 products + XCTAssertEqual(capturedAppStoreInformation!.products.count, 2) + // AND eligible for free trial + XCTAssertTrue(capturedAppStoreInformation!.eligibleForTrial) + + } + +}