From e828dc424748ec220beee62824409094bc9687eb Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Sun, 31 Dec 2023 15:57:25 +0100 Subject: [PATCH] Implement integration tests - Create a new target that contains the integration tests - Implement three test plans: `AllTests`, `UnitTests`, `IntegrationTests` --- Sources/Flare/Classes/Models/IAPError.swift | 24 +++ .../RefundProvider/IRefundProvider.swift | 1 + .../RefundProvider/RefundProvider.swift | 1 + .../Extensions/StoreKitSessionTestCase.swift | 18 -- .../UnitTests/FlareStoreKit2Tests.swift | 156 ------------------ .../Providers/IAPProviderStoreKit2Tests.swift | 146 ---------------- .../ProductProviderStoreKit2Tests.swift | 54 ------ .../PurchaseProviderStoreKit2Tests.swift | 85 ---------- .../Flare.storekit | 0 .../Helpers/Extensions/Result+.swift | 26 +++ .../Helpers/Extensions/XCTestCase+.swift | 35 ++++ .../StoreSessionTestCase.swift | 0 Tests/IntegrationTests/Tests/FlareTests.swift | 154 +++++++++++++++++ .../Tests/IAPProviderTests.swift | 151 +++++++++++++++++ .../Tests}/ProductProviderHelper.swift | 0 .../Tests/ProductProviderTests.swift | 59 +++++++ .../Tests/PurchaseProviderTests.swift | 90 ++++++++++ Tests/TestPlans/AllTests.xctestplan | 44 +++++ Tests/TestPlans/IntegrationTests.xctestplan | 37 +++++ Tests/TestPlans/UnitTests.xctestplan | 37 +++++ .../UnitTestHostApp/AppDelegate.swift | 11 ++ .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../UnitTestHostApp/Info.plist | 2 +- project.yml | 38 ++++- 26 files changed, 702 insertions(+), 467 deletions(-) delete mode 100644 Tests/FlareTests/UnitTests/Extensions/StoreKitSessionTestCase.swift delete mode 100644 Tests/FlareTests/UnitTests/FlareStoreKit2Tests.swift delete mode 100644 Tests/FlareTests/UnitTests/Providers/IAPProviderStoreKit2Tests.swift delete mode 100644 Tests/FlareTests/UnitTests/Providers/ProductProviderStoreKit2Tests.swift delete mode 100644 Tests/FlareTests/UnitTests/Providers/PurchaseProviderStoreKit2Tests.swift rename Tests/{FlareTests/UnitTests => IntegrationTests}/Flare.storekit (100%) create mode 100644 Tests/IntegrationTests/Helpers/Extensions/Result+.swift create mode 100644 Tests/IntegrationTests/Helpers/Extensions/XCTestCase+.swift rename Tests/{FlareTests/UnitTests/TestHelpers => IntegrationTests/Helpers/StoreSessionTestCase}/StoreSessionTestCase.swift (100%) create mode 100644 Tests/IntegrationTests/Tests/FlareTests.swift create mode 100644 Tests/IntegrationTests/Tests/IAPProviderTests.swift rename Tests/{FlareTests/UnitTests/TestHelpers/Helpers => IntegrationTests/Tests}/ProductProviderHelper.swift (100%) create mode 100644 Tests/IntegrationTests/Tests/ProductProviderTests.swift create mode 100644 Tests/IntegrationTests/Tests/PurchaseProviderTests.swift create mode 100644 Tests/TestPlans/AllTests.xctestplan create mode 100644 Tests/TestPlans/IntegrationTests.xctestplan create mode 100644 Tests/TestPlans/UnitTests.xctestplan rename Tests/{FlareTests => }/UnitTestHostApp/AppDelegate.swift (60%) rename Tests/{FlareTests => }/UnitTestHostApp/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename Tests/{FlareTests => }/UnitTestHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename Tests/{FlareTests => }/UnitTestHostApp/Assets.xcassets/Contents.json (100%) rename Tests/{FlareTests => }/UnitTestHostApp/Info.plist (97%) diff --git a/Sources/Flare/Classes/Models/IAPError.swift b/Sources/Flare/Classes/Models/IAPError.swift index 96c64fc58..83537685e 100644 --- a/Sources/Flare/Classes/Models/IAPError.swift +++ b/Sources/Flare/Classes/Models/IAPError.swift @@ -44,6 +44,30 @@ public enum IAPError: Swift.Error { extension IAPError { init(error: Swift.Error?) { + if #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) { + if let storeKitError = error as? StoreKitError { + self.init(storeKitError: storeKitError) + } else { + self.init(error) + } + } else { + self.init(error) + } + } + + @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) + private init(storeKitError: StoreKit.StoreKitError) { + switch storeKitError { + case .unknown: + self = .unknown + case .userCancelled: + self = .paymentCancelled + default: + self = .with(error: storeKitError) + } + } + + private init(_ error: Swift.Error?) { switch (error as? SKError)?.code { case .paymentNotAllowed: self = .paymentNotAllowed diff --git a/Sources/Flare/Classes/Providers/RefundProvider/IRefundProvider.swift b/Sources/Flare/Classes/Providers/RefundProvider/IRefundProvider.swift index 97ef2dd1c..350c1a3f7 100644 --- a/Sources/Flare/Classes/Providers/RefundProvider/IRefundProvider.swift +++ b/Sources/Flare/Classes/Providers/RefundProvider/IRefundProvider.swift @@ -15,6 +15,7 @@ protocol IRefundProvider { @available(macOS, unavailable) @available(watchOS, unavailable) @available(tvOS, unavailable) + @MainActor func beginRefundRequest(productID: String) async throws -> RefundRequestStatus #endif } diff --git a/Sources/Flare/Classes/Providers/RefundProvider/RefundProvider.swift b/Sources/Flare/Classes/Providers/RefundProvider/RefundProvider.swift index f4a8adc8e..d77bf4f0a 100644 --- a/Sources/Flare/Classes/Providers/RefundProvider/RefundProvider.swift +++ b/Sources/Flare/Classes/Providers/RefundProvider/RefundProvider.swift @@ -75,6 +75,7 @@ extension RefundProvider: IRefundProvider { @available(macOS, unavailable) @available(watchOS, unavailable) @available(tvOS, unavailable) + @MainActor func beginRefundRequest(productID: String) async throws -> RefundRequestStatus { let windowScene = try systemInfoProvider.currentScene let transactionID = try await refundRequestProvider.verifyTransaction(productID: productID) diff --git a/Tests/FlareTests/UnitTests/Extensions/StoreKitSessionTestCase.swift b/Tests/FlareTests/UnitTests/Extensions/StoreKitSessionTestCase.swift deleted file mode 100644 index 8be4b716f..000000000 --- a/Tests/FlareTests/UnitTests/Extensions/StoreKitSessionTestCase.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Flare -// Copyright © 2023 Space Code. All rights reserved. -// - -import StoreKit -import StoreKitTest -import XCTest - -// class StoreKitSessionTestCase: XCTestCase { -// // MARK: Properties -// -// private var session: SKTestSession! -// -// // MARK: XCTestCase -// -//// override func -// } diff --git a/Tests/FlareTests/UnitTests/FlareStoreKit2Tests.swift b/Tests/FlareTests/UnitTests/FlareStoreKit2Tests.swift deleted file mode 100644 index 24922b25b..000000000 --- a/Tests/FlareTests/UnitTests/FlareStoreKit2Tests.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// Flare -// Copyright © 2023 Space Code. All rights reserved. -// - -@testable import Flare -import XCTest - -// MARK: - FlareStoreKit2Tests - -@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) -final class FlareStoreKit2Tests: StoreSessionTestCase { - // MARK: - Properties - - private var iapProviderMock: IAPProviderMock! - - private var sut: Flare! - - // MARK: - XCTestCase - - override func setUp() { - super.setUp() - iapProviderMock = IAPProviderMock() - sut = Flare(iapProvider: iapProviderMock) - } - - override func tearDown() { - iapProviderMock = nil - sut = nil - super.tearDown() - } - - #if os(iOS) || VISION_OS - func test_thatFlareRefundsPurchase() async throws { - // given - iapProviderMock.stubbedBeginRefundRequest = .success - - // when - let state = try await sut.beginRefundRequest(productID: .productID) - - // then - if case .success = state {} - else { XCTFail("state must be `success`") } - } - - func test_thatFlareRefundRequestThrowsAnError_whenBeginRefundRequestFailed() async throws { - // given - iapProviderMock.stubbedBeginRefundRequest = .failed(error: IAPError.unknown) - - // when - let state = try await sut.beginRefundRequest(productID: .productID) - - // then - if case let .failed(error) = state { XCTAssertEqual(error as NSError, IAPError.unknown as NSError) } - else { XCTFail("state must be `failed`") } - } - #endif - - func test_thatFlarePurchasesAProductWithOptions_whenPurchaseCompleted() async throws { - let transaction = StoreTransactionStub() - try await test_purchaseWithOptionsAndCompletion( - transaction: transaction, - canMakePayments: true, - expectedResult: .success(StoreTransaction(storeTransaction: transaction)) - ) - } - - func test_thatFlarePurchaseThrowsAnError_whenPaymentNotAllowed() async throws { - try await test_purchaseWithOptionsAndCompletion( - canMakePayments: false, - expectedResult: .failure(IAPError.paymentNotAllowed) - ) - } - - func test_thatFlarePurchasesAsyncAProductWithOptionsAndCompletionHandler_whenPurchaseCompleted() async throws { - let transaction = StoreTransactionStub() - try await test_purchaseWithOptions( - canMakePayments: true, - expectedResult: .success(StoreTransaction(storeTransaction: transaction)) - ) - } - - func test_thatFlarePurchaseAsyncThrowsAnError_whenPaymentNotAllowed() async throws { - try await test_purchaseWithOptions( - canMakePayments: false, - expectedResult: .failure(IAPError.paymentNotAllowed) - ) - } - - // MARK: Private - - private func test_purchaseWithOptionsAndCompletion( - transaction: StoreTransactionStub? = nil, - canMakePayments: Bool, - expectedResult: Result - ) async throws { - // given - let product = try await ProductProviderHelper.purchases.randomElement() - let storeTransactionStub = transaction ?? StoreTransactionStub() - storeTransactionStub.stubbedProductIdentifier = product?.id - - iapProviderMock.stubbedCanMakePayments = canMakePayments - iapProviderMock.stubbedAsyncPurchaseWithOptions = StoreTransaction( - storeTransaction: storeTransactionStub - ) - - // when - let result: Result = await result(for: { - try await sut.purchase( - product: StoreProduct(product: product!), - options: [.simulatesAskToBuyInSandbox(false)] - ) - }) - - // then - XCTAssertEqual(result, expectedResult) - } - - private func test_purchaseWithOptions( - transaction: StoreTransactionStub? = nil, - canMakePayments: Bool, - expectedResult: Result - ) async throws { - // given - let expectation = XCTestExpectation(description: "Purchase a product") - - let product = try await ProductProviderHelper.purchases.randomElement() - let storeTransactionStub = transaction ?? StoreTransactionStub() - storeTransactionStub.stubbedProductIdentifier = product?.id - - iapProviderMock.stubbedCanMakePayments = canMakePayments - iapProviderMock.stubbedPurchaseWithOptionsResult = .success(StoreTransaction(storeTransaction: storeTransactionStub)) - - // when - sut.purchase( - product: StoreProduct(product: product!), - options: [.simulatesAskToBuyInSandbox(false)] - ) { result in - XCTAssertEqual(result, expectedResult) - expectation.fulfill() - } - - // then - wait(for: [expectation], timeout: .second) - } -} - -// MARK: - Constants - -private extension TimeInterval { - static let second: CGFloat = 1.0 -} - -private extension String { - static let productID = "com.flare.test_purchase_2" -} diff --git a/Tests/FlareTests/UnitTests/Providers/IAPProviderStoreKit2Tests.swift b/Tests/FlareTests/UnitTests/Providers/IAPProviderStoreKit2Tests.swift deleted file mode 100644 index 0d129112e..000000000 --- a/Tests/FlareTests/UnitTests/Providers/IAPProviderStoreKit2Tests.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// Flare -// Copyright © 2023 Space Code. All rights reserved. -// - -@testable import Flare -import XCTest - -// MARK: - IAPProviderStoreKit2Tests - -@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) -final class IAPProviderStoreKit2Tests: StoreSessionTestCase { - // MARK: - Properties - - private var productProviderMock: ProductProviderMock! - private var purchaseProvider: PurchaseProviderMock! - private var refundProviderMock: RefundProviderMock! - - private var sut: IIAPProvider! - - // MARK: - XCTestCase - - override func setUp() { - super.setUp() - productProviderMock = ProductProviderMock() - purchaseProvider = PurchaseProviderMock() - refundProviderMock = RefundProviderMock() - sut = IAPProvider( - paymentQueue: PaymentQueueMock(), - productProvider: productProviderMock, - purchaseProvider: purchaseProvider, - receiptRefreshProvider: ReceiptRefreshProviderMock(), - refundProvider: refundProviderMock - ) - } - - override func tearDown() { - productProviderMock = nil - purchaseProvider = nil - refundProviderMock = nil - sut = nil - super.tearDown() - } - - // MARK: Tests - - func test_thatIAPProviderFetchesSK2Products_whenProductsAreAvailable() async throws { - let productsMock = try await ProductProviderHelper.purchases.map(SK2StoreProduct.init) - productProviderMock.stubbedAsyncFetchResult = .success(productsMock) - - // when - let products = try await sut.fetch(productIDs: [.productID]) - - // then - XCTAssertFalse(products.isEmpty) - XCTAssertEqual(productsMock.count, products.count) - } - - func test_thatIAPProviderThrowsAnIAPError_whenFetchingProductsFailed() async { - productProviderMock.stubbedAsyncFetchResult = .failure(IAPError.unknown) - - // when - let error: IAPError? = await error(for: { try await sut.fetch(productIDs: [.productID]) }) - - // then - XCTAssertEqual(error, .unknown) - } - - func test_thatIAPProviderThrowsAPlainError_whenFetchingProductsFailed() async { - productProviderMock.stubbedAsyncFetchResult = .failure(URLError(.unknown)) - - // when - let error: IAPError? = await error(for: { try await sut.fetch(productIDs: [.productID]) }) - - // then - XCTAssertEqual(error, .with(error: URLError(.unknown))) - } - - #if os(iOS) || VISION_OS - func test_thatIAPProviderRefundsPurchase() async throws { - // given - refundProviderMock.stubbedBeginRefundRequest = .success - - // when - let state = try await sut.beginRefundRequest(productID: .productID) - - // then - if case .success = state {} - else { XCTFail("state must be `success`") } - } - - func test_thatFlareThrowsAnError_whenBeginRefundRequestFailed() async throws { - // given - refundProviderMock.stubbedBeginRefundRequest = .failed(error: IAPError.unknown) - - // when - let state = try await sut.beginRefundRequest(productID: .productID) - - // then - if case let .failed(error) = state { XCTAssertEqual(error as NSError, IAPError.unknown as NSError) } - else { XCTFail("state must be `failed`") } - } - #endif - - func test_thatIAPProviderPurchasesAProduct() async throws { - // given - let transactionMock = StoreTransactionMock() - transactionMock.stubbedTransactionIdentifier = .transactionID - - let storeTransaction = StoreTransaction(storeTransaction: transactionMock) - purchaseProvider.stubbedPurchaseCompletionResult = (.success(storeTransaction), ()) - - let product = try await ProductProviderHelper.purchases[0] - - // when - let transaction = try await sut.purchase(product: StoreProduct(product: product)) - - // then - XCTAssertEqual(transaction.transactionIdentifier, .transactionID) - } - - func test_thatIAPProviderPurchasesAProductWithOptions() async throws { - // given - let transactionMock = StoreTransactionMock() - transactionMock.stubbedTransactionIdentifier = .transactionID - - let storeTransaction = StoreTransaction(storeTransaction: transactionMock) - purchaseProvider.stubbedinvokedPurchaseWithOptionsCompletionResult = (.success(storeTransaction), ()) - - let product = try await ProductProviderHelper.purchases[0] - - // when - let transaction = try await sut.purchase(product: StoreProduct(product: product), options: []) - - // then - XCTAssertEqual(transaction.transactionIdentifier, .transactionID) - } -} - -// MARK: - Constants - -private extension String { -// static let receipt = "receipt" - static let productID = "product_identifier" - static let transactionID = "transaction_identifier" -} diff --git a/Tests/FlareTests/UnitTests/Providers/ProductProviderStoreKit2Tests.swift b/Tests/FlareTests/UnitTests/Providers/ProductProviderStoreKit2Tests.swift deleted file mode 100644 index 87eba9e5a..000000000 --- a/Tests/FlareTests/UnitTests/Providers/ProductProviderStoreKit2Tests.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Flare -// Copyright © 2023 Space Code. All rights reserved. -// - -import Concurrency -@testable import Flare -import TestConcurrency -import XCTest - -// MARK: - ProductProviderStoreKit2Tests - -@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) -final class ProductProviderStoreKit2Tests: StoreSessionTestCase { - // MARK: - Properties - - private var testDispatchQueue: TestDispatchQueue! - private var dispatchQueueFactory: IDispatchQueueFactory! - - private var sut: ProductProvider! - - // MARK: - XCTestCase - - override func setUp() { - super.setUp() - testDispatchQueue = TestDispatchQueue() - dispatchQueueFactory = TestDispatchQueueFactory(testQueue: testDispatchQueue) - sut = ProductProvider(dispatchQueueFactory: dispatchQueueFactory) - } - - override func tearDown() { - testDispatchQueue = nil - dispatchQueueFactory = nil - sut = nil - super.tearDown() - } - - // MARK: - Tests - - func test_thatProductProviderFetchesProductsWithIDs() async throws { - // when - let products = try await sut.fetch(productIDs: [.productID]) - - // then - XCTAssertEqual(products.count, 1) - XCTAssertEqual(products.first?.productIdentifier, .productID) - } -} - -// MARK: - Constants - -private extension String { - static let productID = "com.flare.test_purchase_1" -} diff --git a/Tests/FlareTests/UnitTests/Providers/PurchaseProviderStoreKit2Tests.swift b/Tests/FlareTests/UnitTests/Providers/PurchaseProviderStoreKit2Tests.swift deleted file mode 100644 index eb1ceee9c..000000000 --- a/Tests/FlareTests/UnitTests/Providers/PurchaseProviderStoreKit2Tests.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// Flare -// Copyright © 2023 Space Code. All rights reserved. -// - -@testable import Flare -import XCTest - -// MARK: - PurchaseProviderStoreKit2Tests - -@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) -final class PurchaseProviderStoreKit2Tests: StoreSessionTestCase { - // MARK: Properties - - private var paymentProviderMock: PaymentProviderMock! - - private var sut: PurchaseProvider! - - // MARK: XCTestCase - - override func setUp() { - super.setUp() - paymentProviderMock = PaymentProviderMock() - sut = PurchaseProvider( - paymentProvider: paymentProviderMock - ) - } - - override func tearDown() { - sut = nil - super.tearDown() - } - - // MARK: Tests - - func test_thatPurchaseProviderReturnsPaymentTransaction_whenPurchasesAProductWithOptions() async throws { - let expectation = XCTestExpectation(description: "Purchase a product") - let productMock = try StoreProduct(product: await ProductProviderHelper.purchases.randomElement()!) - - // when - sut.purchase(product: productMock, options: [.simulatesAskToBuyInSandbox(false)]) { result in - switch result { - case let .success(transaction): - XCTAssertEqual(transaction.productIdentifier, productMock.productIdentifier) - expectation.fulfill() - case let .failure(error): - XCTFail(error.localizedDescription) - } - } - - #if swift(>=5.9) - await fulfillment(of: [expectation]) - #else - wait(for: [expectation], timeout: .second) - #endif - } - - func test_thatPurchaseProviderReturnsPaymentTransaction_whenSK2ProductExist() async throws { - let expectation = XCTestExpectation(description: "Purchase a product") - let productMock = try StoreProduct(product: await ProductProviderHelper.purchases.randomElement()!) - - // when - sut.purchase(product: productMock) { result in - switch result { - case let .success(transaction): - XCTAssertEqual(transaction.productIdentifier, productMock.productIdentifier) - expectation.fulfill() - case let .failure(error): - XCTFail(error.localizedDescription) - } - } - - #if swift(>=5.9) - await fulfillment(of: [expectation]) - #else - wait(for: [expectation], timeout: .second) - #endif - } -} - -// MARK: - Constants - -private extension TimeInterval { - static let second: TimeInterval = 1.0 -} diff --git a/Tests/FlareTests/UnitTests/Flare.storekit b/Tests/IntegrationTests/Flare.storekit similarity index 100% rename from Tests/FlareTests/UnitTests/Flare.storekit rename to Tests/IntegrationTests/Flare.storekit diff --git a/Tests/IntegrationTests/Helpers/Extensions/Result+.swift b/Tests/IntegrationTests/Helpers/Extensions/Result+.swift new file mode 100644 index 000000000..9c779804a --- /dev/null +++ b/Tests/IntegrationTests/Helpers/Extensions/Result+.swift @@ -0,0 +1,26 @@ +// +// Flare +// Copyright © 2023 Space Code. All rights reserved. +// + +import Foundation + +extension Result { + var error: Failure? { + switch self { + case let .failure(error): + return error + default: + return nil + } + } + + var success: Success? { + switch self { + case let .success(value): + return value + default: + return nil + } + } +} diff --git a/Tests/IntegrationTests/Helpers/Extensions/XCTestCase+.swift b/Tests/IntegrationTests/Helpers/Extensions/XCTestCase+.swift new file mode 100644 index 000000000..8d55a0f50 --- /dev/null +++ b/Tests/IntegrationTests/Helpers/Extensions/XCTestCase+.swift @@ -0,0 +1,35 @@ +// +// Flare +// Copyright © 2023 Space Code. All rights reserved. +// + +import XCTest + +extension XCTestCase { + func value(for closure: () async throws -> U) async -> U? { + do { + let value = try await closure() + return value + } catch { + return nil + } + } + + func error(for closure: () async throws -> U) async -> T? { + do { + _ = try await closure() + return nil + } catch { + return error as? T + } + } + + func result(for closure: () async throws -> U) async -> Result { + do { + let value = try await closure() + return .success(value) + } catch { + return .failure(error as! T) + } + } +} diff --git a/Tests/FlareTests/UnitTests/TestHelpers/StoreSessionTestCase.swift b/Tests/IntegrationTests/Helpers/StoreSessionTestCase/StoreSessionTestCase.swift similarity index 100% rename from Tests/FlareTests/UnitTests/TestHelpers/StoreSessionTestCase.swift rename to Tests/IntegrationTests/Helpers/StoreSessionTestCase/StoreSessionTestCase.swift diff --git a/Tests/IntegrationTests/Tests/FlareTests.swift b/Tests/IntegrationTests/Tests/FlareTests.swift new file mode 100644 index 000000000..c03449e46 --- /dev/null +++ b/Tests/IntegrationTests/Tests/FlareTests.swift @@ -0,0 +1,154 @@ +// +// Flare +// Copyright © 2023 Space Code. All rights reserved. +// + +@testable import Flare +import StoreKit +import XCTest + +// MARK: - FlareTests + +@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) +final class FlareTests: StoreSessionTestCase { + // MARK: - Properties + + private var sut: Flare! + + // MARK: - XCTestCase + + override func setUp() { + super.setUp() + sut = Flare() + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + // MARK: Tests + + func test_thatFlarePurchasesAProductWithCompletion_whenPurchaseCompleted() async throws { + try await test_purchaseWithOptions( + options: [], + expectedResult: .success(()) + ) + } + + func test_thatFlarePurchasesAProductWithCompletion_whenUnkownErrorOccurred() async throws { + // given + session?.failTransactionsEnabled = true + session?.failureError = .unknown + + // when + try await test_purchaseWithOptions( + options: [], + expectedResult: .failure(.unknown) + ) + } + + func test_thatFlarePurchasesAProductWithOptions_whenPurchaseCompleted() async throws { + try await test_purchaseWithOptionsAndCompletion( + expectedResult: .success(()) + ) + } + + func test_thatFlarePurchaseThrowsAnError_whenUnkownErrorOccurred() async throws { + // given + session?.failTransactionsEnabled = true + session?.failureError = .unknown + + // when + try await test_purchaseWithOptionsAndCompletion( + expectedResult: .failure(IAPError.unknown) + ) + } + + func test_thatFlarePurchasesAsyncAProductWithOptionsAndCompletionHandler_whenPurchaseCompleted() async throws { + try await test_purchaseWithOptions( + expectedResult: .success(()) + ) + } + + func test_thatFlarePurchaseAsyncThrowsAnError_whenUnkownErrorOccurred() async throws { + // given + session?.failTransactionsEnabled = true + session?.failureError = .unknown + + // when + try await test_purchaseWithOptions( + expectedResult: .failure(IAPError.unknown) + ) + } + + // MARK: Private + + private func test_purchaseWithOptionsAndCompletion( + expectedResult: Result + ) async throws { + // given + let product = try await ProductProviderHelper.purchases.randomElement() + + // when + let result: Result = await result(for: { + try await sut.purchase( + product: StoreProduct(product: product!), + options: [.simulatesAskToBuyInSandbox(false)] + ) + }) + + // then + switch expectedResult { + case .success: + XCTAssertEqual(result.success?.productIdentifier, product?.id) + case let .failure(error): + XCTAssertEqual(error, result.error) + } + } + + private func test_purchaseWithOptions( + options: Set = [.simulatesAskToBuyInSandbox(true)], + expectedResult: Result + ) async throws { + // given + let expectation = XCTestExpectation(description: "Purchase a product") + + let product = try await ProductProviderHelper.purchases.randomElement() + + // when + var handler: Closure> = { result in + switch expectedResult { + case .success: + XCTAssertEqual(result.success?.productIdentifier, product?.id) + case let .failure(error): + XCTAssertEqual(error, result.error) + } + expectation.fulfill() + } + + if options.isEmpty { + sut.purchase(product: StoreProduct(product: product!)) { handler($0) } + } else { + sut.purchase( + product: StoreProduct(product: product!), + options: options + ) { [handler] result in + Task { handler(result) } + } + } + + // then + wait(for: [expectation], timeout: .second) + } +} + +// MARK: - Constants + +private extension TimeInterval { + static let second: CGFloat = 1.0 +} + +private extension String { + static let productID = "com.flare.test_purchase_2" +} diff --git a/Tests/IntegrationTests/Tests/IAPProviderTests.swift b/Tests/IntegrationTests/Tests/IAPProviderTests.swift new file mode 100644 index 000000000..d1c0d99d4 --- /dev/null +++ b/Tests/IntegrationTests/Tests/IAPProviderTests.swift @@ -0,0 +1,151 @@ +// +// Flare +// Copyright © 2023 Space Code. All rights reserved. +// + +////// +////// Flare +////// Copyright © 2023 Space Code. All rights reserved. +////// +// +// @testable import Flare +// import XCTest +// +//// MARK: - IAPProviderStoreKit2Tests +// +// @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) +// final class IAPProviderStoreKit2Tests: StoreSessionTestCase { +// // MARK: - Properties +// +// private var productProviderMock: ProductProviderMock! +// private var purchaseProvider: PurchaseProviderMock! +// private var refundProviderMock: RefundProviderMock! +// +// private var sut: IIAPProvider! +// +// // MARK: - XCTestCase +// +// override func setUp() { +// super.setUp() +// productProviderMock = ProductProviderMock() +// purchaseProvider = PurchaseProviderMock() +// refundProviderMock = RefundProviderMock() +// sut = IAPProvider( +// paymentQueue: PaymentQueueMock(), +// productProvider: productProviderMock, +// purchaseProvider: purchaseProvider, +// receiptRefreshProvider: ReceiptRefreshProviderMock(), +// refundProvider: refundProviderMock +// ) +// } +// +// override func tearDown() { +// productProviderMock = nil +// purchaseProvider = nil +// refundProviderMock = nil +// sut = nil +// super.tearDown() +// } +// +// // MARK: Tests +// +// func test_thatIAPProviderFetchesSK2Products_whenProductsAreAvailable() async throws { +// let productsMock = try await ProductProviderHelper.purchases.map(SK2StoreProduct.init) +// productProviderMock.stubbedAsyncFetchResult = .success(productsMock) +// +// // when +// let products = try await sut.fetch(productIDs: [.productID]) +// +// // then +// XCTAssertFalse(products.isEmpty) +// XCTAssertEqual(productsMock.count, products.count) +// } +// +// func test_thatIAPProviderThrowsAnIAPError_whenFetchingProductsFailed() async { +// productProviderMock.stubbedAsyncFetchResult = .failure(IAPError.unknown) +// +// // when +// let error: IAPError? = await error(for: { try await sut.fetch(productIDs: [.productID]) }) +// +// // then +// XCTAssertEqual(error, .unknown) +// } +// +// func test_thatIAPProviderThrowsAPlainError_whenFetchingProductsFailed() async { +// productProviderMock.stubbedAsyncFetchResult = .failure(URLError(.unknown)) +// +// // when +// let error: IAPError? = await error(for: { try await sut.fetch(productIDs: [.productID]) }) +// +// // then +// XCTAssertEqual(error, .with(error: URLError(.unknown))) +// } +// +// #if os(iOS) || VISION_OS +// func test_thatIAPProviderRefundsPurchase() async throws { +// // given +// refundProviderMock.stubbedBeginRefundRequest = .success +// +// // when +// let state = try await sut.beginRefundRequest(productID: .productID) +// +// // then +// if case .success = state {} +// else { XCTFail("state must be `success`") } +// } +// +// func test_thatFlareThrowsAnError_whenBeginRefundRequestFailed() async throws { +// // given +// refundProviderMock.stubbedBeginRefundRequest = .failed(error: IAPError.unknown) +// +// // when +// let state = try await sut.beginRefundRequest(productID: .productID) +// +// // then +// if case let .failed(error) = state { XCTAssertEqual(error as NSError, IAPError.unknown as NSError) } +// else { XCTFail("state must be `failed`") } +// } +// #endif +// +// func test_thatIAPProviderPurchasesAProduct() async throws { +// // given +// let transactionMock = StoreTransactionMock() +// transactionMock.stubbedTransactionIdentifier = .transactionID +// +// let storeTransaction = StoreTransaction(storeTransaction: transactionMock) +// purchaseProvider.stubbedPurchaseCompletionResult = (.success(storeTransaction), ()) +// +// let product = try await ProductProviderHelper.purchases[0] +// +// // when +// let transaction = try await sut.purchase(product: StoreProduct(product: product)) +// +// // then +// XCTAssertEqual(transaction.transactionIdentifier, .transactionID) +// } +// +// func test_thatIAPProviderPurchasesAProductWithOptions() async throws { +// // given +// let transactionMock = StoreTransactionMock() +// transactionMock.stubbedTransactionIdentifier = .transactionID +// +// let storeTransaction = StoreTransaction(storeTransaction: transactionMock) +// purchaseProvider.stubbedinvokedPurchaseWithOptionsCompletionResult = (.success(storeTransaction), ()) +// +// let product = try await ProductProviderHelper.purchases[0] +// +// // when +// let transaction = try await sut.purchase(product: StoreProduct(product: product), options: []) +// +// // then +// XCTAssertEqual(transaction.transactionIdentifier, .transactionID) +// } +// } +// +//// MARK: - Constants +// +// private extension String { +//// static let receipt = "receipt" +// static let productID = "product_identifier" +// static let transactionID = "transaction_identifier" +// } diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Helpers/ProductProviderHelper.swift b/Tests/IntegrationTests/Tests/ProductProviderHelper.swift similarity index 100% rename from Tests/FlareTests/UnitTests/TestHelpers/Helpers/ProductProviderHelper.swift rename to Tests/IntegrationTests/Tests/ProductProviderHelper.swift diff --git a/Tests/IntegrationTests/Tests/ProductProviderTests.swift b/Tests/IntegrationTests/Tests/ProductProviderTests.swift new file mode 100644 index 000000000..3edf243fb --- /dev/null +++ b/Tests/IntegrationTests/Tests/ProductProviderTests.swift @@ -0,0 +1,59 @@ +// +// Flare +// Copyright © 2023 Space Code. All rights reserved. +// + +//// +//// Flare +//// Copyright © 2023 Space Code. All rights reserved. +//// +// +// import Concurrency +// @testable import Flare +// import TestConcurrency +// import XCTest +// +//// MARK: - ProductProviderStoreKit2Tests +// +// @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) +// final class ProductProviderStoreKit2Tests: StoreSessionTestCase { +// // MARK: - Properties +// +// private var testDispatchQueue: TestDispatchQueue! +// private var dispatchQueueFactory: IDispatchQueueFactory! +// +// private var sut: ProductProvider! +// +// // MARK: - XCTestCase +// +// override func setUp() { +// super.setUp() +// testDispatchQueue = TestDispatchQueue() +// dispatchQueueFactory = TestDispatchQueueFactory(testQueue: testDispatchQueue) +// sut = ProductProvider(dispatchQueueFactory: dispatchQueueFactory) +// } +// +// override func tearDown() { +// testDispatchQueue = nil +// dispatchQueueFactory = nil +// sut = nil +// super.tearDown() +// } +// +// // MARK: - Tests +// +// func test_thatProductProviderFetchesProductsWithIDs() async throws { +// // when +// let products = try await sut.fetch(productIDs: [.productID]) +// +// // then +// XCTAssertEqual(products.count, 1) +// XCTAssertEqual(products.first?.productIdentifier, .productID) +// } +// } +// +//// MARK: - Constants +// +// private extension String { +// static let productID = "com.flare.test_purchase_1" +// } diff --git a/Tests/IntegrationTests/Tests/PurchaseProviderTests.swift b/Tests/IntegrationTests/Tests/PurchaseProviderTests.swift new file mode 100644 index 000000000..31170ffb3 --- /dev/null +++ b/Tests/IntegrationTests/Tests/PurchaseProviderTests.swift @@ -0,0 +1,90 @@ +// +// Flare +// Copyright © 2023 Space Code. All rights reserved. +// + +//// +//// Flare +//// Copyright © 2023 Space Code. All rights reserved. +//// +// +// @testable import Flare +// import XCTest +// +//// MARK: - PurchaseProviderStoreKit2Tests +// +// @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) +// final class PurchaseProviderStoreKit2Tests: StoreSessionTestCase { +// // MARK: Properties +// +// private var paymentProviderMock: PaymentProviderMock! +// +// private var sut: PurchaseProvider! +// +// // MARK: XCTestCase +// +// override func setUp() { +// super.setUp() +// paymentProviderMock = PaymentProviderMock() +// sut = PurchaseProvider( +// paymentProvider: paymentProviderMock +// ) +// } +// +// override func tearDown() { +// sut = nil +// super.tearDown() +// } +// +// // MARK: Tests +// +// func test_thatPurchaseProviderReturnsPaymentTransaction_whenPurchasesAProductWithOptions() async throws { +// let expectation = XCTestExpectation(description: "Purchase a product") +// let productMock = try StoreProduct(product: await ProductProviderHelper.purchases.randomElement()!) +// +// // when +// sut.purchase(product: productMock, options: [.simulatesAskToBuyInSandbox(false)]) { result in +// switch result { +// case let .success(transaction): +// XCTAssertEqual(transaction.productIdentifier, productMock.productIdentifier) +// expectation.fulfill() +// case let .failure(error): +// XCTFail(error.localizedDescription) +// } +// } +// +// #if swift(>=5.9) +// await fulfillment(of: [expectation]) +// #else +// wait(for: [expectation], timeout: .second) +// #endif +// } +// +// func test_thatPurchaseProviderReturnsPaymentTransaction_whenSK2ProductExist() async throws { +// let expectation = XCTestExpectation(description: "Purchase a product") +// let productMock = try StoreProduct(product: await ProductProviderHelper.purchases.randomElement()!) +// +// // when +// sut.purchase(product: productMock) { result in +// switch result { +// case let .success(transaction): +// XCTAssertEqual(transaction.productIdentifier, productMock.productIdentifier) +// expectation.fulfill() +// case let .failure(error): +// XCTFail(error.localizedDescription) +// } +// } +// +// #if swift(>=5.9) +// await fulfillment(of: [expectation]) +// #else +// wait(for: [expectation], timeout: .second) +// #endif +// } +// } +// +//// MARK: - Constants +// +// private extension TimeInterval { +// static let second: TimeInterval = 1.0 +// } diff --git a/Tests/TestPlans/AllTests.xctestplan b/Tests/TestPlans/AllTests.xctestplan new file mode 100644 index 000000000..259da0494 --- /dev/null +++ b/Tests/TestPlans/AllTests.xctestplan @@ -0,0 +1,44 @@ +{ + "configurations" : [ + { + "id" : "1CA61D79-6551-4DA7-8DEF-CC28563C2658", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : { + "targets" : [ + { + "containerPath" : "container:Flare.xcodeproj", + "identifier" : "AAEFF2D6694AA197C07481DA", + "name" : "Flare" + } + ] + }, + "targetForVariableExpansion" : { + "containerPath" : "container:Flare.xcodeproj", + "identifier" : "AAEFF2D6694AA197C07481DA", + "name" : "Flare" + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:Flare.xcodeproj", + "identifier" : "2053CB2B5F4780EC86D0DE04", + "name" : "FlareTests" + } + }, + { + "target" : { + "containerPath" : "container:Flare.xcodeproj", + "identifier" : "5A649E8F4319C5B59E9588FD", + "name" : "IntegrationTests" + } + } + ], + "version" : 1 +} diff --git a/Tests/TestPlans/IntegrationTests.xctestplan b/Tests/TestPlans/IntegrationTests.xctestplan new file mode 100644 index 000000000..57f3054e2 --- /dev/null +++ b/Tests/TestPlans/IntegrationTests.xctestplan @@ -0,0 +1,37 @@ +{ + "configurations" : [ + { + "id" : "1CA61D79-6551-4DA7-8DEF-CC28563C2658", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : { + "targets" : [ + { + "containerPath" : "container:Flare.xcodeproj", + "identifier" : "AAEFF2D6694AA197C07481DA", + "name" : "Flare" + } + ] + }, + "targetForVariableExpansion" : { + "containerPath" : "container:Flare.xcodeproj", + "identifier" : "AAEFF2D6694AA197C07481DA", + "name" : "Flare" + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:Flare.xcodeproj", + "identifier" : "5A649E8F4319C5B59E9588FD", + "name" : "IntegrationTests" + } + } + ], + "version" : 1 +} diff --git a/Tests/TestPlans/UnitTests.xctestplan b/Tests/TestPlans/UnitTests.xctestplan new file mode 100644 index 000000000..98e230890 --- /dev/null +++ b/Tests/TestPlans/UnitTests.xctestplan @@ -0,0 +1,37 @@ +{ + "configurations" : [ + { + "id" : "1CA61D79-6551-4DA7-8DEF-CC28563C2658", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : { + "targets" : [ + { + "containerPath" : "container:Flare.xcodeproj", + "identifier" : "AAEFF2D6694AA197C07481DA", + "name" : "Flare" + } + ] + }, + "targetForVariableExpansion" : { + "containerPath" : "container:Flare.xcodeproj", + "identifier" : "AAEFF2D6694AA197C07481DA", + "name" : "Flare" + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:Flare.xcodeproj", + "identifier" : "2053CB2B5F4780EC86D0DE04", + "name" : "FlareTests" + } + } + ], + "version" : 1 +} diff --git a/Tests/FlareTests/UnitTestHostApp/AppDelegate.swift b/Tests/UnitTestHostApp/AppDelegate.swift similarity index 60% rename from Tests/FlareTests/UnitTestHostApp/AppDelegate.swift rename to Tests/UnitTestHostApp/AppDelegate.swift index aea816e94..4f3e9b07c 100644 --- a/Tests/FlareTests/UnitTestHostApp/AppDelegate.swift +++ b/Tests/UnitTestHostApp/AppDelegate.swift @@ -12,6 +12,17 @@ import SwiftUI @main class AppDelegate: NSObject, NSApplicationDelegate {} +#elseif os(watchOS) + + @main + struct TestApp: App { + var body: some Scene { + WindowGroup { + Text("Hello World") + } + } + } + #else @main class AppDelegate: UIResponder, UIApplicationDelegate {} diff --git a/Tests/FlareTests/UnitTestHostApp/Assets.xcassets/AccentColor.colorset/Contents.json b/Tests/UnitTestHostApp/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Tests/FlareTests/UnitTestHostApp/Assets.xcassets/AccentColor.colorset/Contents.json rename to Tests/UnitTestHostApp/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Tests/FlareTests/UnitTestHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/Tests/UnitTestHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Tests/FlareTests/UnitTestHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Tests/UnitTestHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Tests/FlareTests/UnitTestHostApp/Assets.xcassets/Contents.json b/Tests/UnitTestHostApp/Assets.xcassets/Contents.json similarity index 100% rename from Tests/FlareTests/UnitTestHostApp/Assets.xcassets/Contents.json rename to Tests/UnitTestHostApp/Assets.xcassets/Contents.json diff --git a/Tests/FlareTests/UnitTestHostApp/Info.plist b/Tests/UnitTestHostApp/Info.plist similarity index 97% rename from Tests/FlareTests/UnitTestHostApp/Info.plist rename to Tests/UnitTestHostApp/Info.plist index ceae02525..c468f022f 100644 --- a/Tests/FlareTests/UnitTestHostApp/Info.plist +++ b/Tests/UnitTestHostApp/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - StoreKitUnitTestsHostApp + UnitTestsHostApp CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/project.yml b/project.yml index 703146a47..df58e74b5 100644 --- a/project.yml +++ b/project.yml @@ -15,13 +15,16 @@ targets: UnitTestHostApp: type: application supportedDestinations: [iOS, tvOS, macOS] - sources: Tests/FlareTests/UnitTestHostApp + sources: Tests/UnitTestHostApp settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.spacecode.flare + TARGETED_DEVICE_FAMILY: "1,2,3,4" + SUPPORTED_PLATFORMS: "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator" scheme: + storeKitConfiguration: "Tests/FlareTests/IntegrationTests/Flare.storekit" testTargets: - - FlareTests + - IntegrationTests Flare: type: framework supportedDestinations: [iOS, tvOS, macOS] @@ -29,16 +32,37 @@ targets: - package: Concurrency product: Concurrency settings: - GENERATE_INFOPLIST_FILE: YES + base: + GENERATE_INFOPLIST_FILE: YES + TARGETED_DEVICE_FAMILY: "1,2,3,4" + SUPPORTED_PLATFORMS: "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator" sources: - path: Sources scheme: - testTargets: - - FlareTests + testPlans: + - path: Tests/TestPlans/AllTests.xctestplan + defaultPlan: true + - path: Tests/TestPlans/UnitTests.xctestplan + - path: Tests/TestPlans/IntegrationTests.xctestplan gatherCoverageData: true coverageTargets: - Flare FlareTests: + type: bundle.unit-test + supportedDestinations: [iOS, tvOS, macOS] + dependencies: + - package: Concurrency + product: TestConcurrency + - target: Flare + settings: + base: + GENERATE_INFOPLIST_FILE: YES + PRODUCT_BUNDLE_IDENTIFIER: com.spacecode.flare-unit-tests + SUPPORTED_PLATFORMS: "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator" + TARGETED_DEVICE_FAMILY: "1,2,3,4" + sources: + - Tests/FlareTests/UnitTests + IntegrationTests: type: bundle.unit-test supportedDestinations: [iOS, tvOS, macOS] dependencies: @@ -51,6 +75,6 @@ targets: BUNDLE_LOADER: $(TEST_HOST) GENERATE_INFOPLIST_FILE: YES TEST_HOST: $(BUILT_PRODUCTS_DIR)/UnitTestHostApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UnitTestHostApp - PRODUCT_BUNDLE_IDENTIFIER: com.spacecode.flare + PRODUCT_BUNDLE_IDENTIFIER: com.spacecode.flare.storekit-unit-tests sources: - - Tests/FlareTests/UnitTests \ No newline at end of file + - Tests/IntegrationTests \ No newline at end of file