diff --git a/Sources/Error Handling/BackendError.swift b/Sources/Error Handling/BackendError.swift index bcc788ac6f..f3e1b26ca7 100644 --- a/Sources/Error Handling/BackendError.swift +++ b/Sources/Error Handling/BackendError.swift @@ -21,7 +21,7 @@ enum BackendError: Error, Equatable { case networkError(NetworkError) case missingAppUserID(Source) case emptySubscriberAttributes(Source) - case missingReceiptFile(Source) + case missingReceiptFile(URL?, Source) case missingTransactionProductIdentifier(Source) case missingCachedCustomerInfo(Source) case unexpectedBackendResponse(UnexpectedBackendResponseError, extraContext: String?, Source) @@ -49,9 +49,10 @@ extension BackendError { } static func missingReceiptFile( + _ receiptURL: URL?, file: String = #fileID, function: String = #function, line: UInt = #line ) -> Self { - return .missingReceiptFile(.init(file: file, function: function, line: line)) + return .missingReceiptFile(receiptURL, .init(file: file, function: function, line: line)) } static func missingCachedCustomerInfo( @@ -89,8 +90,9 @@ extension BackendError: PurchasesErrorConvertible { functionName: source.function, line: source.line) - case let .missingReceiptFile(source): - return ErrorUtils.missingReceiptFileError(fileName: source.file, + case let .missingReceiptFile(receiptURL, source): + return ErrorUtils.missingReceiptFileError(receiptURL, + fileName: source.file, functionName: source.function, line: source.line) diff --git a/Sources/Error Handling/ErrorUtils.swift b/Sources/Error Handling/ErrorUtils.swift index af78c33047..c481bc3f17 100644 --- a/Sources/Error Handling/ErrorUtils.swift +++ b/Sources/Error Handling/ErrorUtils.swift @@ -138,9 +138,22 @@ enum ErrorUtils { * sandbox or if there are no previous purchases. */ static func missingReceiptFileError( + _ receiptURL: URL?, fileName: String = #fileID, functionName: String = #function, line: UInt = #line ) -> PurchasesError { + let fileExists: Bool = { + if let receiptURL = receiptURL { + return FileManager.default.fileExists(atPath: receiptURL.path) + } else { + return false + } + }() + return error(with: ErrorCode.missingReceiptFileError, + extraUserInfo: [ + "rc_receipt_url": receiptURL?.absoluteString ?? "", + "rc_receipt_file_exists": fileExists + ], fileName: fileName, functionName: functionName, line: line) } diff --git a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift index 51a0d49400..1b0bba2024 100644 --- a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift +++ b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift @@ -233,10 +233,10 @@ final class PurchasesOrchestrator { return } - receiptFetcher.receiptData(refreshPolicy: .onlyIfEmpty) { receiptData in + receiptFetcher.receiptData(refreshPolicy: .onlyIfEmpty) { receiptData, receiptURL in guard let receiptData = receiptData, !receiptData.isEmpty else { - completion(.failure(ErrorUtils.missingReceiptFileError())) + completion(.failure(ErrorUtils.missingReceiptFileError(receiptURL))) return } @@ -944,7 +944,7 @@ private extension PurchasesOrchestrator { // Refresh the receipt and post to backend, this will allow the transactions to be transferred. // https://rev.cat/apple-restoring-purchased-products - self.receiptFetcher.receiptData(refreshPolicy: receiptRefreshPolicy) { receiptData in + self.receiptFetcher.receiptData(refreshPolicy: receiptRefreshPolicy) { receiptData, receiptURL in guard let receiptData = receiptData, !receiptData.isEmpty else { if self.systemInfo.isSandbox { @@ -953,7 +953,7 @@ private extension PurchasesOrchestrator { if let completion = completion { self.operationDispatcher.dispatchOnMainThread { - completion(.failure(ErrorUtils.missingReceiptFileError())) + completion(.failure(ErrorUtils.missingReceiptFileError(receiptURL))) } } return diff --git a/Sources/Purchasing/Purchases/TransactionPoster.swift b/Sources/Purchasing/Purchases/TransactionPoster.swift index c17466cb70..9c7af23684 100644 --- a/Sources/Purchasing/Purchases/TransactionPoster.swift +++ b/Sources/Purchasing/Purchases/TransactionPoster.swift @@ -83,7 +83,7 @@ final class TransactionPoster: TransactionPosterType { completion: @escaping CustomerAPI.CustomerInfoResponseHandler) { self.receiptFetcher.receiptData( refreshPolicy: self.refreshRequestPolicy(forProductIdentifier: transaction.productIdentifier) - ) { receiptData in + ) { receiptData, receiptURL in if let receiptData = receiptData, !receiptData.isEmpty { self.fetchProductsAndPostReceipt( transaction: transaction, @@ -93,7 +93,7 @@ final class TransactionPoster: TransactionPosterType { ) } else { self.handleReceiptPost(withTransaction: transaction, - result: .failure(.missingReceiptFile()), + result: .failure(.missingReceiptFile(receiptURL)), subscriberAttributes: nil, completion: completion) } diff --git a/Sources/Purchasing/ReceiptFetcher.swift b/Sources/Purchasing/ReceiptFetcher.swift index 25856a3c61..9fef858a07 100644 --- a/Sources/Purchasing/ReceiptFetcher.swift +++ b/Sources/Purchasing/ReceiptFetcher.swift @@ -39,7 +39,9 @@ class ReceiptFetcher { self.clock = clock } - func receiptData(refreshPolicy: ReceiptRefreshPolicy, completion: @escaping (Data?) -> Void) { + func receiptData(refreshPolicy: ReceiptRefreshPolicy, completion: @escaping (Data?, URL?) -> Void) { + let receiptURL = self.receiptURL + switch refreshPolicy { case .always: if self.shouldThrottleRefreshRequest() { @@ -61,7 +63,7 @@ class ReceiptFetcher { Logger.debug(Strings.receipt.refreshing_empty_receipt) self.refreshReceipt(completion) } else { - completion(receiptData) + completion(receiptData, receiptURL) } case let .retryUntilProductIsFound(productIdentifier, maximumRetries, sleepDuration): @@ -77,14 +79,14 @@ class ReceiptFetcher { } case .never: - completion(self.receiptData()) + completion(self.receiptData(), receiptURL) } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) func receiptData(refreshPolicy: ReceiptRefreshPolicy) async -> Data? { return await withCheckedContinuation { continuation in - self.receiptData(refreshPolicy: refreshPolicy) { result in + self.receiptData(refreshPolicy: refreshPolicy) { result, _ in continuation.resume(returning: result) } } @@ -152,20 +154,20 @@ private extension ReceiptFetcher { } } - func refreshReceipt(_ completion: @escaping (Data) -> Void) { + func refreshReceipt(_ completion: @escaping (Data, URL?) -> Void) { self.lastReceiptRefreshRequest.value = self.clock.now self.requestFetcher.fetchReceiptData { - completion(self.receiptData() ?? Data()) + completion(self.receiptData() ?? Data(), self.receiptURL) } } /// `async` version of `refreshReceipt(_:)` @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) - func refreshReceipt() async -> Data { + func refreshReceipt() async -> (Data, URL?) { await withCheckedContinuation { continuation in self.refreshReceipt { - continuation.resume(returning: $0) + continuation.resume(returning: ($0, $1)) } } } @@ -176,13 +178,14 @@ private extension ReceiptFetcher { untilProductIsFound productIdentifier: String, maximumRetries: Int, sleepDuration: DispatchTimeInterval - ) async -> Data { + ) async -> (Data, URL?) { var retries = 0 var data: Data = .init() + var receiptURL: URL? repeat { retries += 1 - data = await self.refreshReceipt() + (data, receiptURL) = await self.refreshReceipt() if !data.isEmpty { do { @@ -192,7 +195,7 @@ private extension ReceiptFetcher { }.value if receipt.containsActivePurchase(forProductIdentifier: productIdentifier) { - return data + return (data, receiptURL) } else { Logger.appleWarning(Strings.receipt.local_receipt_missing_purchase( receipt, @@ -208,7 +211,7 @@ private extension ReceiptFetcher { try? await Task.sleep(nanoseconds: UInt64(sleepDuration.nanoseconds)) } while retries <= maximumRetries && !Task.isCancelled - return data + return (data, receiptURL) } } diff --git a/Sources/Purchasing/TrialOrIntroPriceEligibilityChecker.swift b/Sources/Purchasing/TrialOrIntroPriceEligibilityChecker.swift index 435b0f5da6..87a400591b 100644 --- a/Sources/Purchasing/TrialOrIntroPriceEligibilityChecker.swift +++ b/Sources/Purchasing/TrialOrIntroPriceEligibilityChecker.swift @@ -82,7 +82,7 @@ class TrialOrIntroPriceEligibilityChecker: TrialOrIntroPriceEligibilityCheckerTy completion: @escaping ReceiveIntroEligibilityBlock) { // We don't want to refresh receipts because it will likely prompt the user for their credentials, // and intro eligibility is triggered programmatically. - self.receiptFetcher.receiptData(refreshPolicy: .never) { data in + self.receiptFetcher.receiptData(refreshPolicy: .never) { data, _ in if #available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 6.2, *), let data = data { self.sk1CheckEligibility(with: data, diff --git a/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift b/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift index 0c56155818..1e8fbeac59 100644 --- a/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift +++ b/Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift @@ -844,7 +844,7 @@ class PurchasesOrchestratorTests: StoreKitConfigTestCase { self.setUpStoreKit2Listener() - let expectedError: BackendError = .missingReceiptFile() + let expectedError: BackendError = .missingReceiptFile(nil) self.customerInfoManager.stubbedCachedCustomerInfoResult = self.mockCustomerInfo self.backend.stubbedPostReceiptResult = .failure(expectedError) diff --git a/Tests/UnitTests/Misc/PurchasesDiagnosticsTests.swift b/Tests/UnitTests/Misc/PurchasesDiagnosticsTests.swift index 539862d621..9ccb72e103 100644 --- a/Tests/UnitTests/Misc/PurchasesDiagnosticsTests.swift +++ b/Tests/UnitTests/Misc/PurchasesDiagnosticsTests.swift @@ -109,7 +109,7 @@ class PurchasesDiagnosticsTests: TestCase { // MARK: - Errors func testUnknownError() { - let underlyingError = ErrorUtils.missingReceiptFileError() + let underlyingError = ErrorUtils.missingReceiptFileError(nil) let error = PurchasesDiagnostics.Error.unknown(underlyingError) expect(error.errorUserInfo[NSUnderlyingErrorKey] as? NSError).to(matchError(underlyingError)) diff --git a/Tests/UnitTests/Mocks/MockReceiptFetcher.swift b/Tests/UnitTests/Mocks/MockReceiptFetcher.swift index 6a07b5df77..81cf36a4e7 100644 --- a/Tests/UnitTests/Mocks/MockReceiptFetcher.swift +++ b/Tests/UnitTests/Mocks/MockReceiptFetcher.swift @@ -20,19 +20,20 @@ class MockReceiptFetcher: ReceiptFetcher { var receiptDataTimesCalled = 0 var receiptDataReceivedRefreshPolicy: ReceiptRefreshPolicy? var mockReceiptData: Data = .init(1...3) + var mockReceiptURL: URL? - override func receiptData(refreshPolicy: ReceiptRefreshPolicy, completion: @escaping ((Data?) -> Void)) { - receiptDataReceivedRefreshPolicy = refreshPolicy - receiptDataCalled = true - receiptDataTimesCalled += 1 - if shouldReturnReceipt { - if shouldReturnZeroBytesReceipt { - completion(Data()) + override func receiptData(refreshPolicy: ReceiptRefreshPolicy, completion: @escaping ((Data?, URL?) -> Void)) { + self.receiptDataReceivedRefreshPolicy = refreshPolicy + self.receiptDataCalled = true + self.receiptDataTimesCalled += 1 + if self.shouldReturnReceipt { + if self.shouldReturnZeroBytesReceipt { + completion(Data(), self.mockReceiptURL) } else { - completion(self.mockReceiptData) + completion(self.mockReceiptData, self.mockReceiptURL) } } else { - completion(nil) + completion(nil, self.mockReceiptURL) } } diff --git a/Tests/UnitTests/Networking/BackendErrorTests.swift b/Tests/UnitTests/Networking/BackendErrorTests.swift index 64b818b4b1..9d72b2c1c5 100644 --- a/Tests/UnitTests/Networking/BackendErrorTests.swift +++ b/Tests/UnitTests/Networking/BackendErrorTests.swift @@ -30,7 +30,7 @@ class BackendErrorTests: BaseErrorTests { } func testMissingReceiptFile() { - let error: BackendError = .missingReceiptFile() + let error: BackendError = .missingReceiptFile(nil) verifyPurchasesError(error, expectedCode: .missingReceiptFileError) } diff --git a/Tests/UnitTests/Purchasing/ErrorUtilsTests.swift b/Tests/UnitTests/Purchasing/ErrorUtilsTests.swift index abf41ea1cb..8e8162daad 100644 --- a/Tests/UnitTests/Purchasing/ErrorUtilsTests.swift +++ b/Tests/UnitTests/Purchasing/ErrorUtilsTests.swift @@ -34,6 +34,31 @@ class ErrorUtilsTests: TestCase { super.tearDown() } + func testReceiptErrorWithNoURL() { + let error = ErrorUtils.missingReceiptFileError(nil) + expect(error).to(matchError(ErrorCode.missingReceiptFileError)) + expect(error.userInfo["rc_receipt_url"] as? String) == "" + expect(error.userInfo["rc_receipt_file_exists"] as? Bool) == false + } + + func testReceiptErrorWithMissingReceipt() { + let url = URL(string: "file://does_not_exist")! + + let error = ErrorUtils.missingReceiptFileError(url) + expect(error).to(matchError(ErrorCode.missingReceiptFileError)) + expect(error.userInfo["rc_receipt_url"] as? String) == url.absoluteString + expect(error.userInfo["rc_receipt_file_exists"] as? Bool) == false + } + + func testReceiptErrorWithEmptyReceipt() { + let url = Self.createEmptyFile() + + let error = ErrorUtils.missingReceiptFileError(url) + expect(error).to(matchError(ErrorCode.missingReceiptFileError)) + expect(error.userInfo["rc_receipt_url"] as? String) == url.absoluteString + expect(error.userInfo["rc_receipt_file_exists"] as? Bool) == true + } + func testPublicErrorsCanBeConvertedToErrorCode() throws { let error = ErrorUtils.customerInfoError().asPublicError let errorCode = try XCTUnwrap(error as? ErrorCode, "Error couldn't be converted to ErrorCode") @@ -267,3 +292,20 @@ class ErrorUtilsTests: TestCase { } } + +private extension ErrorUtilsTests { + + static func createEmptyFile() -> URL { + let fileManager = FileManager.default + let url = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString) + + expect(fileManager.createFile(atPath: url.path, contents: nil, attributes: nil)) + .to( + beTrue(), + description: "Failed creating file" + ) + + return url + } + +} diff --git a/Tests/UnitTests/Purchasing/Purchases/TransactionPosterTests.swift b/Tests/UnitTests/Purchasing/Purchases/TransactionPosterTests.swift index 6ab61e960a..3123c38106 100644 --- a/Tests/UnitTests/Purchasing/Purchases/TransactionPosterTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/TransactionPosterTests.swift @@ -38,6 +38,7 @@ class TransactionPosterTests: TestCase { } func testHandlePurchasedTransactionWithMissingReceipt() throws { + self.receiptFetcher.mockReceiptURL = URL(string: "file://receipt_file")! self.receiptFetcher.shouldReturnReceipt = false let result = try self.handleTransaction( @@ -47,7 +48,7 @@ class TransactionPosterTests: TestCase { ) ) expect(result).to(beFailure()) - expect(result.error) == BackendError.missingReceiptFile() + expect(result.error) == BackendError.missingReceiptFile(self.receiptFetcher.mockReceiptURL) } func testHandlePurchasedTransaction() throws { diff --git a/Tests/UnitTests/Purchasing/ReceiptFetcherTests.swift b/Tests/UnitTests/Purchasing/ReceiptFetcherTests.swift index 630a0bd96a..f37b6228f1 100644 --- a/Tests/UnitTests/Purchasing/ReceiptFetcherTests.swift +++ b/Tests/UnitTests/Purchasing/ReceiptFetcherTests.swift @@ -53,45 +53,40 @@ class BaseReceiptFetcherTests: TestCase { final class ReceiptFetcherTests: BaseReceiptFetcherTests { func testReceiptDataWithRefreshPolicyNeverReturnsReceiptData() { - let receivedData: Data? = waitUntilValue { completion in - self.receiptFetcher.receiptData(refreshPolicy: .never, completion: completion) - } + let (receivedData, url) = self.receiptData(.never) expect(receivedData).toNot(beNil()) + expect(url).toNot(beNil()) } func testReceiptDataWithRefreshPolicyOnlyIfEmptyReturnsReceiptData() { - let receivedData: Data? = waitUntilValue { completion in - self.receiptFetcher.receiptData(refreshPolicy: .onlyIfEmpty, completion: completion) - } + let (receivedData, url) = self.receiptData(.onlyIfEmpty) expect(receivedData).toNot(beNil()) + expect(url).toNot(beNil()) } func testReceiptDataWithRefreshPolicyAlwaysReturnsReceiptData() { - let receivedData: Data? = waitUntilValue { completion in - self.receiptFetcher.receiptData(refreshPolicy: .always, completion: completion) - } + let (receivedData, url) = self.receiptData(.always) expect(receivedData).toNot(beNil()) + expect(url).toNot(beNil()) } func testReceiptDataWithRefreshPolicyNeverDoesntRefreshIfEmpty() { self.mockBundle.receiptURLResult = .emptyReceipt - let receivedData: Data? = waitUntilValue { completion in - self.receiptFetcher.receiptData(refreshPolicy: .never, completion: completion) - } + let (receivedData, url) = self.receiptData(.never) + expect(self.mockRequestFetcher.refreshReceiptCalled) == false expect(receivedData).to(beNil()) + expect(url).to(beNil()) } func testReceiptDataWithRefreshPolicyOnlyIfEmptyRefreshesIfEmpty() { self.mockBundle.receiptURLResult = .emptyReceipt - let receivedData: Data? = waitUntilValue { completion in - self.receiptFetcher.receiptData(refreshPolicy: .onlyIfEmpty, completion: completion) - } + let (receivedData, _) = self.receiptData(.onlyIfEmpty) expect(receivedData).toNot(beNil()) expect(receivedData).to(beEmpty()) @@ -101,9 +96,7 @@ final class ReceiptFetcherTests: BaseReceiptFetcherTests { func testReceiptDataWithRefreshPolicyOnlyIfEmptyRefreshesIfNil() { self.mockBundle.receiptURLResult = .nilURL - let receivedData: Data? = waitUntilValue { completion in - self.receiptFetcher.receiptData(refreshPolicy: .onlyIfEmpty, completion: completion) - } + let (receivedData, _) = self.receiptData(.onlyIfEmpty) expect(receivedData).toNot(beNil()) expect(receivedData).to(beEmpty()) @@ -113,9 +106,7 @@ final class ReceiptFetcherTests: BaseReceiptFetcherTests { func testReceiptDataWithRefreshPolicyOnlyIfEmptyDoesntRefreshIfTheresData() { self.mockBundle.receiptURLResult = .receiptWithData - let receivedData: Data? = waitUntilValue { completion in - self.receiptFetcher.receiptData(refreshPolicy: .onlyIfEmpty, completion: completion) - } + let (receivedData, _) = self.receiptData(.onlyIfEmpty) expect(receivedData).toNot(beNil()) expect(receivedData).toNot(beEmpty()) @@ -125,9 +116,7 @@ final class ReceiptFetcherTests: BaseReceiptFetcherTests { func testReceiptDataWithRefreshPolicyAlwaysRefreshesEvenIfTheresData() { self.mockBundle.receiptURLResult = .receiptWithData - let receivedData: Data? = waitUntilValue { completion in - self.receiptFetcher.receiptData(refreshPolicy: .always, completion: completion) - } + let (receivedData, _) = self.receiptData(.always) expect(receivedData).toNot(beNil()) expect(receivedData).toNot(beEmpty()) @@ -137,15 +126,11 @@ final class ReceiptFetcherTests: BaseReceiptFetcherTests { } func testReceiptDataWithRefreshPolicyAlwaysDoesNotRefreshIfRequestedWithinThrottleDuration() { - let _: Data? = waitUntilValue { completion in - self.receiptFetcher.receiptData(refreshPolicy: .always, completion: completion) - } + _ = self.receiptData(.always) self.clock.advance(by: ReceiptRefreshPolicy.alwaysRefreshThrottleDuration - .milliseconds(500)) - let receivedData: Data? = waitUntilValue { completion in - self.receiptFetcher.receiptData(refreshPolicy: .always, completion: completion) - } + let (receivedData, _) = self.receiptData(.always) expect(receivedData).toNot(beEmpty()) @@ -156,15 +141,11 @@ final class ReceiptFetcherTests: BaseReceiptFetcherTests { func testReceiptDataWithRefreshPolicyAlwaysRefreshesWithinThrottleDurationIfNoReceiptData() { self.mockBundle.receiptURLResult = .emptyReceipt - let _: Data? = waitUntilValue { completion in - self.receiptFetcher.receiptData(refreshPolicy: .always, completion: completion) - } + _ = self.receiptData(.always) self.clock.advance(by: ReceiptRefreshPolicy.alwaysRefreshThrottleDuration - .milliseconds(500)) - let receivedData: Data? = waitUntilValue { completion in - self.receiptFetcher.receiptData(refreshPolicy: .always, completion: completion) - } + let (receivedData, _) = self.receiptData(.always) expect(receivedData).toNot(beNil()) @@ -173,15 +154,11 @@ final class ReceiptFetcherTests: BaseReceiptFetcherTests { } func testReceiptDataWithRefreshPolicyAlwaysRefreshesAfterThrottleDuration() { - let _: Data? = waitUntilValue { completion in - self.receiptFetcher.receiptData(refreshPolicy: .always, completion: completion) - } + _ = self.receiptData(.always) self.clock.advance(by: ReceiptRefreshPolicy.alwaysRefreshThrottleDuration + .seconds(1)) - let receivedData: Data? = waitUntilValue { completion in - self.receiptFetcher.receiptData(refreshPolicy: .always, completion: completion) - } + let (receivedData, _) = self.receiptData(.always) expect(receivedData).toNot(beEmpty()) @@ -189,6 +166,16 @@ final class ReceiptFetcherTests: BaseReceiptFetcherTests { expect(self.mockRequestFetcher.refreshReceiptCalledCount) == 2 } + private func receiptData(_ policy: ReceiptRefreshPolicy) -> (data: Data?, receiptURL: URL?) { + let result: (Data?, URL?)? = waitUntilValue { completion in + self.receiptFetcher.receiptData(refreshPolicy: policy) { data, url in + completion((data, url)) + } + } + + return result ?? (nil, nil) + } + } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) @@ -217,15 +204,17 @@ final class RetryingReceiptFetcherTests: BaseReceiptFetcherTests { func testReturnsAfterFirstTryIfNoReceiptURL() async { self.mockBundle.receiptURLResult = .nilURL - let data = await self.fetch(productIdentifier: "", retries: 1) + let (data, url) = await self.fetch(productIdentifier: "", retries: 1) expect(data) == Data() + expect(url).to(beNil()) } func testReturnsAfterFirstTryIfDataIsCorrect() async { self.mock(receipt: Self.validReceipt) - let data = await self.fetch(productIdentifier: Self.productID, retries: 1) + let (data, url) = await self.fetch(productIdentifier: Self.productID, retries: 1) expect(data) == Self.validReceipt.asData + expect(url) == self.mockBundle.appStoreReceiptURL expect(self.mockReceiptParser.invokedParseParametersList) == [ Self.validReceipt.asData @@ -235,8 +224,9 @@ final class RetryingReceiptFetcherTests: BaseReceiptFetcherTests { func testDoesNotRetryIfMaximumIsZeroEvenIfDataIsInvalid() async { self.mock(receipt: Self.receiptWithoutPurchases) - let data = await self.fetch(productIdentifier: Self.productID, retries: 0) + let (data, url) = await self.fetch(productIdentifier: Self.productID, retries: 0) expect(data) == Self.receiptWithoutPurchases.asData + expect(url) == self.mockBundle.appStoreReceiptURL expect(self.mockReceiptParser.invokedParseParametersList) == [ Self.receiptWithoutPurchases.asData @@ -248,8 +238,9 @@ final class RetryingReceiptFetcherTests: BaseReceiptFetcherTests { let invalidData = Self.receiptWithoutPurchases.asData - let data = await self.fetch(productIdentifier: Self.productID, retries: 2) + let (data, url) = await self.fetch(productIdentifier: Self.productID, retries: 2) expect(data) == invalidData + expect(url) == self.mockBundle.appStoreReceiptURL expect(self.mockReceiptParser.invokedParseParametersList) == [ invalidData, @@ -262,8 +253,9 @@ final class RetryingReceiptFetcherTests: BaseReceiptFetcherTests { func testRetriesIfFirstReceiptIsInvalid() async { self.mock(receipts: [Self.receiptWithoutPurchases, Self.validReceipt]) - let data = await self.fetch(productIdentifier: Self.productID, retries: 1) + let (data, url) = await self.fetch(productIdentifier: Self.productID, retries: 1) expect(data) == Self.validReceipt.asData + expect(url) == self.mockBundle.appStoreReceiptURL expect(self.mockRequestFetcher.refreshReceiptCalledCount) == 2 expect(self.mockReceiptParser.invokedParseParametersList) == [ @@ -276,8 +268,9 @@ final class RetryingReceiptFetcherTests: BaseReceiptFetcherTests { self.mock(receipts: [.failure(.receiptParsingError), .success(Self.validReceipt)]) - let data = await self.fetch(productIdentifier: Self.productID, retries: 1) + let (data, url) = await self.fetch(productIdentifier: Self.productID, retries: 1) expect(data) == Self.validReceipt.asData + expect(url) == self.mockBundle.appStoreReceiptURL expect(self.mockRequestFetcher.refreshReceiptCalledCount) == 2 expect(self.mockReceiptParser.invokedParseParametersList) == [ @@ -289,8 +282,9 @@ final class RetryingReceiptFetcherTests: BaseReceiptFetcherTests { func testStopsRetryingEvenIfParsingReceiptKeepsThrowingError() async { let invalidData = self.mockReceiptWithInvalidData() - let data = await self.fetch(productIdentifier: Self.productID, retries: 1) + let (data, url) = await self.fetch(productIdentifier: Self.productID, retries: 1) expect(data) == invalidData + expect(url) == self.mockBundle.appStoreReceiptURL expect(self.mockRequestFetcher.refreshReceiptCalledCount) == 2 expect(self.mockReceiptParser.invokedParseParametersList) == [ @@ -302,8 +296,9 @@ final class RetryingReceiptFetcherTests: BaseReceiptFetcherTests { func testStopsRetryingIfFindsValidReceipt() async { self.mock(receipts: [Self.receiptWithoutPurchases, Self.validReceipt]) - let data = await self.fetch(productIdentifier: Self.productID, retries: 2) + let (data, url) = await self.fetch(productIdentifier: Self.productID, retries: 2) expect(data) == Self.validReceipt.asData + expect(url) == self.mockBundle.appStoreReceiptURL expect(self.mockRequestFetcher.refreshReceiptCalledCount) == 2 expect(self.mockReceiptParser.invokedParseParametersList) == [ @@ -314,13 +309,13 @@ final class RetryingReceiptFetcherTests: BaseReceiptFetcherTests { // MARK: - - private func fetch(productIdentifier: String, retries: Int) async -> Data { + private func fetch(productIdentifier: String, retries: Int) async -> (Data, URL?) { return await withCheckedContinuation { continuation in self.receiptFetcher.receiptData(refreshPolicy: .retryUntilProductIsFound( productIdentifier: productIdentifier, maximumRetries: retries )) { - continuation.resume(returning: $0 ?? Data()) + continuation.resume(returning: ($0 ?? Data(), $1)) } } }