Skip to content

Commit

Permalink
Added TransactionPoster tests (#2557)
Browse files Browse the repository at this point in the history
Follow up to #2540.
We could add more tests (which are a lot simpler than
`PurchasesOrchestratorTests`) but this is just a start.
  • Loading branch information
NachoSoto authored May 30, 2023
1 parent c6de563 commit 81a867a
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 11 deletions.
4 changes: 4 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
4F0201C42A13C85500091612 /* Assertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0201C32A13C85500091612 /* Assertions.swift */; };
4F0BBA812A1D0524000E75AB /* DefaultDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0BBA802A1D0524000E75AB /* DefaultDecodable.swift */; };
4F0BBAAC2A1D253D000E75AB /* OfflineCustomerInfoCreatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0BBAAB2A1D253D000E75AB /* OfflineCustomerInfoCreatorTests.swift */; };
4F0CE2BD2A215CE600561895 /* TransactionPosterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0CE2BC2A215CE600561895 /* TransactionPosterTests.swift */; };
4F2017D52A15587F0061F6EF /* OfflineStoreKitIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F2017D42A15587F0061F6EF /* OfflineStoreKitIntegrationTests.swift */; };
4F2018732A15797D0061F6EF /* TestLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57057FF728B0048900995F21 /* TestLogHandler.swift */; };
4F3D56632A1E66A10070105A /* CustomerInfoManagerPostReceiptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F3D56622A1E66A10070105A /* CustomerInfoManagerPostReceiptTests.swift */; };
Expand Down Expand Up @@ -886,6 +887,7 @@
4F0201C32A13C85500091612 /* Assertions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assertions.swift; sourceTree = "<group>"; };
4F0BBA802A1D0524000E75AB /* DefaultDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultDecodable.swift; sourceTree = "<group>"; };
4F0BBAAB2A1D253D000E75AB /* OfflineCustomerInfoCreatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineCustomerInfoCreatorTests.swift; sourceTree = "<group>"; };
4F0CE2BC2A215CE600561895 /* TransactionPosterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionPosterTests.swift; sourceTree = "<group>"; };
4F2017D42A15587F0061F6EF /* OfflineStoreKitIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineStoreKitIntegrationTests.swift; sourceTree = "<group>"; };
4F3D56622A1E66A10070105A /* CustomerInfoManagerPostReceiptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerInfoManagerPostReceiptTests.swift; sourceTree = "<group>"; };
4F54DF3E2A1D8C7500FD72BF /* MockStoreKit2TransactionFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreKit2TransactionFetcher.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2163,6 +2165,7 @@
57FDAA952846BDE2009A48F1 /* PurchasesTransactionHandlingTests.swift */,
57FDAA992846C2BD009A48F1 /* PurchasesDelegateTests.swift */,
57DBFA5C28AADA43002D18CA /* PurchasesLogInTests.swift */,
4F0CE2BC2A215CE600561895 /* TransactionPosterTests.swift */,
);
path = Purchases;
sourceTree = "<group>";
Expand Down Expand Up @@ -3219,6 +3222,7 @@
4F8A58172A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift in Sources */,
2DDF41E824F6F61B005BC22D /* MockSKProductDiscount.swift in Sources */,
579189B728F4747700BF4963 /* EitherTests.swift in Sources */,
4F0CE2BD2A215CE600561895 /* TransactionPosterTests.swift in Sources */,
5753EE00294B8C0C00CBAB54 /* AttributionFetcherTests.swift in Sources */,
57069A5E28E398E100B86355 /* AsyncExtensionsTests.swift in Sources */,
35272E2226D0048D00F22C3B /* HTTPClientTests.swift in Sources */,
Expand Down
12 changes: 6 additions & 6 deletions Sources/Purchasing/Purchases/TransactionPoster.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import Foundation

/// Determines what triggered a purchase and whether it comes from a restore.
struct PurchaseSource {
struct PurchaseSource: Equatable {

let isRestore: Bool
let initiationSource: ProductRequestData.InitiationSource
Expand All @@ -37,7 +37,7 @@ protocol TransactionPosterType: AnyObject, Sendable {

/// Starts a `PostReceiptDataOperation` for the transaction.
func handlePurchasedTransaction(
_ transaction: StoreTransaction,
_ transaction: StoreTransactionType,
data: PurchasedTransactionData,
completion: @escaping CustomerAPI.CustomerInfoResponseHandler
)
Expand Down Expand Up @@ -77,7 +77,7 @@ final class TransactionPoster: TransactionPosterType {
self.operationDispatcher = operationDispatcher
}

func handlePurchasedTransaction(_ transaction: StoreTransaction,
func handlePurchasedTransaction(_ transaction: StoreTransactionType,
data: PurchasedTransactionData,
completion: @escaping CustomerAPI.CustomerInfoResponseHandler) {
self.receiptFetcher.receiptData(
Expand Down Expand Up @@ -125,7 +125,7 @@ final class TransactionPoster: TransactionPosterType {
private extension TransactionPoster {

func fetchProductsAndPostReceipt(
transaction: StoreTransaction,
transaction: StoreTransactionType,
data: PurchasedTransactionData,
receiptData: Data,
completion: @escaping CustomerAPI.CustomerInfoResponseHandler
Expand All @@ -146,7 +146,7 @@ private extension TransactionPoster {
}
}

func handleReceiptPost(withTransaction transaction: StoreTransaction,
func handleReceiptPost(withTransaction transaction: StoreTransactionType,
result: Result<CustomerInfo, BackendError>,
subscriberAttributes: SubscriberAttribute.Dictionary?,
completion: @escaping CustomerAPI.CustomerInfoResponseHandler) {
Expand All @@ -173,7 +173,7 @@ private extension TransactionPoster {
}
}

func postReceipt(transaction: StoreTransaction,
func postReceipt(transaction: StoreTransactionType,
purchasedTransactionData: PurchasedTransactionData,
receiptData: Data,
product: StoreProduct?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class CustomerInfoManagerPostReceiptTests: BaseCustomerInfoManagerTests {

let parameters = try XCTUnwrap(self.mockTransactionPoster.invokedHandlePurchasedTransactionParameters.value)

expect(parameters.transaction) === transaction
expect(parameters.transaction as? StoreTransaction) === transaction
expect(parameters.data.appUserID) == Self.userID
expect(parameters.data.presentedOfferingID).to(beNil())
expect(parameters.data.unsyncedAttributes).to(beEmpty())
Expand All @@ -98,8 +98,10 @@ class CustomerInfoManagerPostReceiptTests: BaseCustomerInfoManagerTests {
isAppBackgrounded: false)
expect(self.mockBackend.invokedGetSubscriberData) == false
expect(self.mockTransactionPoster.invokedHandlePurchasedTransaction.value) == true
expect(self.mockTransactionPoster.invokedHandlePurchasedTransactionParameters.value?.transaction)
=== transaction
expect(
self.mockTransactionPoster.invokedHandlePurchasedTransactionParameters
.value?.transaction as? StoreTransaction
) === transaction

logger.verifyMessageWasLogged(
Strings.customerInfo.posting_transaction_in_lieu_of_fetching_customerinfo(transaction),
Expand Down
4 changes: 2 additions & 2 deletions Tests/UnitTests/Mocks/MockTransactionPoster.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ final class MockTransactionPoster: TransactionPosterType {
)
let invokedHandlePurchasedTransaction: Atomic<Bool> = false
let invokedHandlePurchasedTransactionCount: Atomic<Int> = .init(0)
let invokedHandlePurchasedTransactionParameters: Atomic<(transaction: StoreTransaction,
let invokedHandlePurchasedTransactionParameters: Atomic<(transaction: StoreTransactionType,
data: PurchasedTransactionData)?> = nil

func handlePurchasedTransaction(
_ transaction: StoreTransaction,
_ transaction: StoreTransactionType,
data: PurchasedTransactionData,
completion: @escaping CustomerAPI.CustomerInfoResponseHandler
) {
Expand Down
168 changes: 168 additions & 0 deletions Tests/UnitTests/Purchasing/Purchases/TransactionPosterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// TransactionPosterTests.swift
//
// Created by Nacho Soto on 5/26/23.

import Nimble
import XCTest

@testable import RevenueCat

class TransactionPosterTests: TestCase {

private var productsManager: MockProductsManager!
private var receiptFetcher: MockReceiptFetcher!
private var backend: MockBackend!
private var paymentQueueWrapper: MockPaymentQueueWrapper!
private var systemInfo: MockSystemInfo!
private var operationDispatcher: MockOperationDispatcher!

private var poster: TransactionPoster!

private var mockTransaction: MockStoreTransaction!
private static let mockCustomerInfo: CustomerInfo = .emptyInfo

override func setUpWithError() throws {
try super.setUpWithError()

self.setUp(observerMode: false)
self.mockTransaction = .init()
}

func testHandlePurchasedTransactionWithMissingReceipt() throws {
self.receiptFetcher.shouldReturnReceipt = false

let result = try self.handleTransaction(
.init(
appUserID: "user",
source: .init(isRestore: false, initiationSource: .queue)
)
)
expect(result).to(beFailure())
expect(result.error) == BackendError.missingReceiptFile()
}

func testHandlePurchasedTransaction() throws {
let product = MockSK1Product(mockProductIdentifier: "product")
let transactionData = PurchasedTransactionData(
appUserID: "user",
source: .init(isRestore: false, initiationSource: .queue)
)

self.receiptFetcher.shouldReturnReceipt = true
self.productsManager.stubbedProductsCompletionResult = .success([StoreProduct(sk1Product: product)])
self.backend.stubbedPostReceiptResult = .success(Self.mockCustomerInfo)

let result = try self.handleTransaction(transactionData)
expect(result).to(beSuccess())
expect(result.value) === Self.mockCustomerInfo

expect(self.backend.invokedPostReceiptData) == true
expect(self.backend.invokedPostReceiptDataParameters?.transactionData).to(match(transactionData))
expect(self.backend.invokedPostReceiptDataParameters?.observerMode) == self.systemInfo.observerMode
expect(self.mockTransaction.finishInvoked) == true
}

func testHandlePurchasedTransactionDoesNotFinishTransactionInObserverMode() throws {
self.setUp(observerMode: true)

let product = MockSK1Product(mockProductIdentifier: "product")
let transactionData = PurchasedTransactionData(
appUserID: "user",
source: .init(isRestore: false, initiationSource: .queue)
)

self.receiptFetcher.shouldReturnReceipt = true
self.productsManager.stubbedProductsCompletionResult = .success([StoreProduct(sk1Product: product)])
self.backend.stubbedPostReceiptResult = .success(Self.mockCustomerInfo)

let result = try self.handleTransaction(transactionData)
expect(result).to(beSuccess())
expect(result.value) === Self.mockCustomerInfo

expect(self.backend.invokedPostReceiptData) == true
expect(self.backend.invokedPostReceiptDataParameters?.observerMode) == true
expect(self.mockTransaction.finishInvoked) == false
}

func testFinishTransactionInObserverMode() throws {
let logger = TestLogHandler()

waitUntil { completed in
self.poster.finishTransactionIfNeeded(self.mockTransaction) {
completed()
}
}

logger.verifyMessageWasLogged(Strings.purchase.finishing_transaction(self.mockTransaction))
}

func testFinishTransactionDoesNotFinishInObserverMode() throws {
self.setUp(observerMode: true)
let logger = TestLogHandler()

waitUntil { completed in
self.poster.finishTransactionIfNeeded(self.mockTransaction) {
completed()
}
}

logger.verifyMessageWasNotLogged("Finished transaction")
}

}

// MARK: -

private extension TransactionPosterTests {

func setUp(observerMode: Bool) {
self.operationDispatcher = .init()
self.systemInfo = .init(finishTransactions: !observerMode)
self.productsManager = .init(systemInfo: self.systemInfo, requestTimeout: 0)
self.receiptFetcher = .init(requestFetcher: .init(operationDispatcher: self.operationDispatcher),
systemInfo: self.systemInfo)
self.backend = .init()
self.paymentQueueWrapper = .init()

self.poster = .init(
productsManager: self.productsManager,
receiptFetcher: self.receiptFetcher,
backend: self.backend,
paymentQueueWrapper: .right(self.paymentQueueWrapper),
systemInfo: self.systemInfo,
operationDispatcher: self.operationDispatcher
)
}

func handleTransaction(_ data: PurchasedTransactionData) throws -> Result<CustomerInfo, BackendError> {
let result = waitUntilValue { completion in
self.poster.handlePurchasedTransaction(self.mockTransaction, data: data) {
completion($0)
}
}

return try XCTUnwrap(result)
}

}

private func match(_ data: PurchasedTransactionData) -> Predicate<PurchasedTransactionData> {
return .init {
let other = try $0.evaluate()
let matches = (other?.appUserID == data.appUserID &&
other?.presentedOfferingID == data.presentedOfferingID &&
other?.source == data.source &&
other?.unsyncedAttributes == data.unsyncedAttributes)

return .init(bool: matches, message: .fail("PurchasedTransactionData do not match"))
}
}

0 comments on commit 81a867a

Please sign in to comment.