From 7456ec62888534a1333f2fa9920d28c6c464f509 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Mon, 11 Apr 2022 15:28:57 -0700 Subject: [PATCH] `OfferingsManager`/`OfferingsFactory`: using `OfferingsResponse` This takes full advantage of `Decodable` when decoding and creating `Offerings`. --- RevenueCat.xcodeproj/project.pbxproj | 12 + Sources/Networking/Backend.swift | 2 +- .../Caching/OfferingsCallback.swift | 2 +- .../Operations/GetOfferingsOperation.swift | 3 +- .../Responses/OfferingsResponse.swift | 57 ++++ Sources/Purchasing/OfferingsFactory.swift | 80 +++--- Sources/Purchasing/OfferingsManager.swift | 93 ++----- Sources/Purchasing/Purchases.swift | 4 +- .../UnitTests/Caching/DeviceCacheTests.swift | 39 +-- Tests/UnitTests/Mocks/MockBackend.swift | 2 +- .../Mocks/MockOfferingsFactory.swift | 5 +- .../Mocks/MockOfferingsManager.swift | 18 +- .../Backend/BackendGetOfferingsTests.swift | 35 +-- .../Purchasing/OfferingsManagerTests.swift | 148 +++++----- .../UnitTests/Purchasing/OfferingsTests.swift | 254 +++++++++--------- .../UnitTests/Purchasing/PurchasesTests.swift | 56 ++-- 16 files changed, 414 insertions(+), 396 deletions(-) create mode 100644 Sources/Networking/Responses/OfferingsResponse.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index fd8024402c..e5a2f52262 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -266,6 +266,7 @@ 57CFB96D27FE0E79002A6730 /* MockCurrentUserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57CFB96B27FE0E79002A6730 /* MockCurrentUserProvider.swift */; }; 57D04BB827D947C6006DAC06 /* HTTPResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D04BB727D947C6006DAC06 /* HTTPResponseTests.swift */; }; 57D5414227F656D9004CC35C /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D5414127F656D9004CC35C /* NetworkError.swift */; }; + 57D5412E27F6311C004CC35C /* OfferingsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D5412D27F6311C004CC35C /* OfferingsResponse.swift */; }; 57DC9F4627CC2E4900DA6AF9 /* HTTPRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DC9F4527CC2E4900DA6AF9 /* HTTPRequest.swift */; }; 57DC9F4A27CD37BA00DA6AF9 /* HTTPStatusCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DC9F4927CD37BA00DA6AF9 /* HTTPStatusCodeTests.swift */; }; 57E0473B277260DE0082FE91 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 57E0473A277260DE0082FE91 /* SnapshotTesting */; }; @@ -693,6 +694,7 @@ 57CFB96B27FE0E79002A6730 /* MockCurrentUserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCurrentUserProvider.swift; sourceTree = ""; }; 57D04BB727D947C6006DAC06 /* HTTPResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResponseTests.swift; sourceTree = ""; }; 57D5414127F656D9004CC35C /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; + 57D5412D27F6311C004CC35C /* OfferingsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingsResponse.swift; sourceTree = ""; }; 57DC9F4527CC2E4900DA6AF9 /* HTTPRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPRequest.swift; sourceTree = ""; }; 57DC9F4927CD37BA00DA6AF9 /* HTTPStatusCodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStatusCodeTests.swift; sourceTree = ""; }; 57E0474B27729A1E0082FE91 /* __Snapshots__ */ = {isa = PBXFileReference; lastKnownFileType = folder; path = __Snapshots__; sourceTree = ""; }; @@ -1392,6 +1394,7 @@ 35D832CB262A5B3400E60AC5 /* Networking */ = { isa = PBXGroup; children = ( + 57D5412C27F63108004CC35C /* Responses */, B34605A1279A6E380031CA74 /* Caching */, B34605AA279A6E380031CA74 /* Operations */, B3C4AAD426B8911300E1B3C8 /* Backend.swift */, @@ -1561,6 +1564,14 @@ path = ..; sourceTree = ""; }; + 57D5412C27F63108004CC35C /* Responses */ = { + isa = PBXGroup; + children = ( + 57D5412D27F6311C004CC35C /* OfferingsResponse.swift */, + ); + path = Responses; + sourceTree = ""; + }; B324DC482720C15300103EE9 /* Error Handling */ = { isa = PBXGroup; children = ( @@ -2091,6 +2102,7 @@ 2DDF41B624F6F387005BC22D /* ASN1ObjectIdentifierBuilder.swift in Sources */, 35D832D2262E56DB00E60AC5 /* HTTPStatusCode.swift in Sources */, 572247D127BEC28E00C524A7 /* Array+Extensions.swift in Sources */, + 57D5412E27F6311C004CC35C /* OfferingsResponse.swift in Sources */, 57AC4C1C2770F56200DDE30F /* SK1StoreProduct.swift in Sources */, B34605C3279A6E380031CA74 /* PostOfferForSigningOperation.swift in Sources */, B34605C7279A6E380031CA74 /* SubscriberAttributesMarshaller.swift in Sources */, diff --git a/Sources/Networking/Backend.swift b/Sources/Networking/Backend.swift index 73bc460d94..1badbc8e85 100644 --- a/Sources/Networking/Backend.swift +++ b/Sources/Networking/Backend.swift @@ -19,7 +19,7 @@ class Backend { typealias CustomerInfoResponseHandler = (Result) -> Void typealias IntroEligibilityResponseHandler = ([String: IntroEligibility], BackendError?) -> Void - typealias OfferingsResponseHandler = (Result<[String: Any], BackendError>) -> Void + typealias OfferingsResponseHandler = (Result) -> Void typealias OfferSigningResponseHandler = (Result) -> Void typealias SimpleResponseHandler = (BackendError?) -> Void typealias LogInResponseHandler = (Result<(info: CustomerInfo, created: Bool), BackendError>) -> Void diff --git a/Sources/Networking/Caching/OfferingsCallback.swift b/Sources/Networking/Caching/OfferingsCallback.swift index dac7ffec0a..1af8aabab1 100644 --- a/Sources/Networking/Caching/OfferingsCallback.swift +++ b/Sources/Networking/Caching/OfferingsCallback.swift @@ -16,6 +16,6 @@ import Foundation struct OfferingsCallback: CacheKeyProviding { let cacheKey: String - let completion: (Result<[String: Any], BackendError>) -> Void + let completion: (Result) -> Void } diff --git a/Sources/Networking/Operations/GetOfferingsOperation.swift b/Sources/Networking/Operations/GetOfferingsOperation.swift index 31fb7f49c4..f3904dab22 100644 --- a/Sources/Networking/Operations/GetOfferingsOperation.swift +++ b/Sources/Networking/Operations/GetOfferingsOperation.swift @@ -46,7 +46,8 @@ private extension GetOfferingsOperation { let request = HTTPRequest(method: .get, path: .getOfferings(appUserID: appUserID)) - httpClient.perform(request, authHeaders: self.authHeaders) { (response: HTTPResponse<[String: Any]>.Result) in + httpClient.perform(request, + authHeaders: self.authHeaders) { (response: HTTPResponse.Result) in defer { completion() } diff --git a/Sources/Networking/Responses/OfferingsResponse.swift b/Sources/Networking/Responses/OfferingsResponse.swift new file mode 100644 index 0000000000..a1f26aab65 --- /dev/null +++ b/Sources/Networking/Responses/OfferingsResponse.swift @@ -0,0 +1,57 @@ +// +// 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 +// +// OfferingsResponse.swift +// +// Created by Nacho Soto on 3/31/22. + +import Foundation + +// swiftlint:disable nesting + +struct OfferingsResponse { + + struct Offering { + + struct Package { + + let identifier: String + let platformProductIdentifier: String + + } + + let identifier: String + let description: String + let packages: [Package] + + } + + let currentOfferingId: String? + let offerings: [Offering] + +} + +extension OfferingsResponse { + + var productIdentifiers: Set { + return Set( + self.offerings + .lazy + .flatMap { $0.packages } + .map { $0.platformProductIdentifier } + ) + } + +} + +extension OfferingsResponse.Offering.Package: Decodable {} +extension OfferingsResponse.Offering: Decodable {} +extension OfferingsResponse: Decodable {} + +extension OfferingsResponse: HTTPResponseBody {} diff --git a/Sources/Purchasing/OfferingsFactory.swift b/Sources/Purchasing/OfferingsFactory.swift index 42f77cd2a2..cfc519d03b 100644 --- a/Sources/Purchasing/OfferingsFactory.swift +++ b/Sources/Purchasing/OfferingsFactory.swift @@ -17,66 +17,66 @@ import StoreKit class OfferingsFactory { - func createOfferings(from storeProductsByID: [String: StoreProduct], data: [String: Any]) -> Offerings? { - guard let offeringsData = data["offerings"] as? [[String: Any]] else { - return nil - } - - let offerings = offeringsData.reduce([String: Offering]()) { (dict, offeringData) -> [String: Offering] in - var dict = dict - if let offering = createOffering(from: storeProductsByID, - offeringData: offeringData) { - dict[offering.identifier] = offering - if offering.availablePackages.isEmpty { - Logger.warn(Strings.offering.offering_empty(offeringIdentifier: offering.identifier)) - } + func createOfferings(from storeProductsByID: [String: StoreProduct], data: OfferingsResponse) -> Offerings? { + let offerings: [String: Offering] = data + .offerings + .compactMap { offeringData in + createOffering(from: storeProductsByID, offering: offeringData) } - return dict - } + .dictionaryAllowingDuplicateKeys { $0.identifier } guard !offerings.isEmpty else { return nil } - let currentOfferingID = data["current_offering_id"] as? String - - return Offerings(offerings: offerings, currentOfferingID: currentOfferingID) + return Offerings(offerings: offerings, currentOfferingID: data.currentOfferingId) } - func createOffering(from storeProductsByID: [String: StoreProduct], offeringData: [String: Any]) -> Offering? { - guard let offeringIdentifier = offeringData["identifier"] as? String, - let packagesData = offeringData["packages"] as? [[String: Any]], - let serverDescription = offeringData["description"] as? String else { - return nil + func createOffering( + from storeProductsByID: [String: StoreProduct], + offering: OfferingsResponse.Offering + ) -> Offering? { + let availablePackages: [Package] = offering.packages.compactMap { package in + createPackage(with: package, productsByID: storeProductsByID, offeringIdentifier: offering.identifier) } - let availablePackages = packagesData.compactMap { packageData -> Package? in - createPackage(with: packageData, - storeProductsByID: storeProductsByID, - offeringIdentifier: offeringIdentifier) - } guard !availablePackages.isEmpty else { + Logger.warn(Strings.offering.offering_empty(offeringIdentifier: offering.identifier)) return nil } - return Offering(identifier: offeringIdentifier, serverDescription: serverDescription, + return Offering(identifier: offering.identifier, + serverDescription: offering.description, availablePackages: availablePackages) } - func createPackage(with data: [String: Any], - storeProductsByID: [String: StoreProduct], - offeringIdentifier: String) -> Package? { - guard let platformProductIdentifier = data["platform_product_identifier"] as? String, - let product = storeProductsByID[platformProductIdentifier], - let identifier = data["identifier"] as? String else { + func createPackage( + with data: OfferingsResponse.Offering.Package, + productsByID: [String: StoreProduct], + offeringIdentifier: String + ) -> Package? { + guard let product = productsByID[data.platformProductIdentifier] else { return nil } - let packageType = Package.packageType(from: identifier) - return Package(identifier: identifier, - packageType: packageType, - storeProduct: product, - offeringIdentifier: offeringIdentifier) + return .init(package: data, + product: product, + offeringIdentifier: offeringIdentifier) + } + +} + +private extension Package { + + convenience init( + package: OfferingsResponse.Offering.Package, + product: StoreProduct, + offeringIdentifier: String + ) { + self.init(identifier: package.identifier, + packageType: Package.packageType(from: package.identifier), + storeProduct: product, + offeringIdentifier: offeringIdentifier) } } diff --git a/Sources/Purchasing/OfferingsManager.swift b/Sources/Purchasing/OfferingsManager.swift index 4938e2962d..a049b1f387 100644 --- a/Sources/Purchasing/OfferingsManager.swift +++ b/Sources/Purchasing/OfferingsManager.swift @@ -37,7 +37,7 @@ class OfferingsManager { self.productsManager = productsManager } - func offerings(appUserID: String, completion: ((Offerings?, Error?) -> Void)?) { + func offerings(appUserID: String, completion: ((Result) -> Void)?) { guard let cachedOfferings = deviceCache.cachedOfferings else { Logger.debug(Strings.offering.no_cached_offerings_fetching_from_network) systemInfo.isApplicationBackgrounded { isAppBackgrounded in @@ -49,9 +49,7 @@ class OfferingsManager { } Logger.debug(Strings.offering.vending_offerings_cache) - dispatchCompletionOnMainThreadIfPossible(completion, - offerings: cachedOfferings, - error: nil) + dispatchCompletionOnMainThreadIfPossible(completion, result: .success(cachedOfferings)) systemInfo.isApplicationBackgrounded { isAppBackgrounded in if self.deviceCache.isOfferingsCacheStale(isAppBackgrounded: isAppBackgrounded) { @@ -68,13 +66,17 @@ class OfferingsManager { } } - func updateOfferingsCache(appUserID: String, isAppBackgrounded: Bool, completion: ((Offerings?, Error?) -> Void)?) { + func updateOfferingsCache( + appUserID: String, + isAppBackgrounded: Bool, + completion: ((Result) -> Void)? + ) { deviceCache.setOfferingsCacheTimestampToNow() operationDispatcher.dispatchOnWorkerThread(withRandomDelay: isAppBackgrounded) { self.backend.getOfferings(appUserID: appUserID) { result in switch result { - case let .success(data): - self.handleOfferingsBackendResult(with: data, completion: completion) + case let .success(response): + self.handleOfferingsBackendResult(with: response, completion: completion) case let .failure(error): self.handleOfferingsUpdateError(.backendError(error), completion: completion) @@ -96,8 +98,12 @@ class OfferingsManager { private extension OfferingsManager { - func handleOfferingsBackendResult(with data: [String: Any], completion: ((Offerings?, Error?) -> Void)?) { - let productIdentifiers = extractProductIdentifiers(fromOfferingsData: data) + func handleOfferingsBackendResult( + with response: OfferingsResponse, + completion: ((Result) -> Void)? + ) { + let productIdentifiers = response.productIdentifiers + guard !productIdentifiers.isEmpty else { let errorMessage = Strings.offering.configuration_error_no_products_for_offering.description self.handleOfferingsUpdateError(.configurationError(errorMessage), @@ -125,42 +131,26 @@ private extension OfferingsManager { ) } - if let createdOfferings = self.offeringsFactory.createOfferings(from: productsByID, - data: data) { + if let createdOfferings = self.offeringsFactory.createOfferings(from: productsByID, data: response) { self.deviceCache.cache(offerings: createdOfferings) - self.dispatchCompletionOnMainThreadIfPossible(completion, - offerings: createdOfferings, - error: nil) + self.dispatchCompletionOnMainThreadIfPossible(completion, result: .success(createdOfferings)) } else { self.handleOfferingsUpdateError(.noOfferingsFound(), completion: completion) } } } - func handleOfferingsUpdateError(_ error: Error, completion: ((Offerings?, Error?) -> Void)?) { + func handleOfferingsUpdateError(_ error: Error, completion: ((Result) -> Void)?) { Logger.appleError(Strings.offering.fetching_offerings_error(error: error.localizedDescription)) deviceCache.clearOfferingsCacheTimestamp() - dispatchCompletionOnMainThreadIfPossible(completion, - offerings: nil, - error: error) + dispatchCompletionOnMainThreadIfPossible(completion, result: .failure(error)) } - func extractProductIdentifiers(fromOfferingsData offeringsData: [String: Any]) -> Set { - // Fixme: parse Data directly instead of converting from Data to Dictionary back to Data - guard let data = try? JSONSerialization.data(withJSONObject: offeringsData), - let response: OfferingsResponse = try? JSONDecoder.default.decode(jsonData: data) else { - return [] - } - - return Set(response.productIdentifiers) - } - - func dispatchCompletionOnMainThreadIfPossible(_ completion: ((Offerings?, Error?) -> Void)?, - offerings: Offerings?, - error: Error?) { + func dispatchCompletionOnMainThreadIfPossible(_ completion: ((Result) -> Void)?, + result: Result) { if let completion = completion { operationDispatcher.dispatchOnMainThread { - completion(offerings, error) + completion(result) } } } @@ -217,42 +207,3 @@ extension OfferingsManager.Error: ErrorCodeConvertible { } } - -// swiftlint:disable nesting - -private struct OfferingsResponse { - - struct Offering { - - struct Package { - - let identifier: String - let platformProductIdentifier: String - - } - - let description: String - let identifier: String - let packages: [Package] - - } - - let currentOfferingId: String - let offerings: [Offering] - -} - -extension OfferingsResponse { - - var productIdentifiers: [String] { - return self.offerings - .lazy - .flatMap { $0.packages } - .map { $0.platformProductIdentifier } - } - -} - -extension OfferingsResponse.Offering.Package: Decodable {} -extension OfferingsResponse.Offering: Decodable {} -extension OfferingsResponse: Decodable {} diff --git a/Sources/Purchasing/Purchases.swift b/Sources/Purchasing/Purchases.swift index bd37f1db58..97305dee18 100644 --- a/Sources/Purchasing/Purchases.swift +++ b/Sources/Purchasing/Purchases.swift @@ -869,8 +869,8 @@ public extension Purchases { * - [Displaying Products](https://docs.revenuecat.com/docs/displaying-products) */ @objc func getOfferings(completion: @escaping (Offerings?, Error?) -> Void) { - offeringsManager.offerings(appUserID: appUserID) { offerings, error in - completion(offerings, error?.asPurchasesError) + offeringsManager.offerings(appUserID: appUserID) { result in + completion(result.value, result.error?.asPurchasesError) } } diff --git a/Tests/UnitTests/Caching/DeviceCacheTests.swift b/Tests/UnitTests/Caching/DeviceCacheTests.swift index bc356b3527..e9fead4356 100644 --- a/Tests/UnitTests/Caching/DeviceCacheTests.swift +++ b/Tests/UnitTests/Caching/DeviceCacheTests.swift @@ -182,7 +182,7 @@ class DeviceCacheTests: XCTestCase { expect(self.deviceCache.isCustomerInfoCacheStale(appUserID: "cesar", isAppBackgrounded: false)) == false } - func testOfferingsAreProperlyCached() { + func testOfferingsAreProperlyCached() throws { let annualProduct = MockSK1Product(mockProductIdentifier: "com.myproduct.annual") let monthlyProduct = MockSK1Product(mockProductIdentifier: "com.myproduct.monthly") let products = [ @@ -192,23 +192,32 @@ class DeviceCacheTests: XCTestCase { let offeringIdentifier = "offering_a" let serverDescription = "This is the base offering" - let optionalOffering = OfferingsFactory().createOffering(from: products, offeringData: [ - "identifier": offeringIdentifier, - "description": serverDescription, - "packages": [ - ["identifier": "$rc_monthly", - "platform_product_identifier": "com.myproduct.monthly"], - ["identifier": "$rc_annual", - "platform_product_identifier": "com.myproduct.annual"], - ["identifier": "$rc_six_month", - "platform_product_identifier": "com.myproduct.sixMonth"] - ] - ]) - guard let offering = optionalOffering else { fatalError("couldn't create offering for tests") } + + let offeringsJSON = """ + { + "identifier": "\(offeringIdentifier)", + "description": "\(serverDescription)", + "packages": [ + {"identifier": "$rc_monthly", + "platform_product_identifier": "com.myproduct.monthly"}, + {"identifier": "$rc_annual", + "platform_product_identifier": "com.myproduct.annual"}, + {"identifier": "$rc_six_month", + "platform_product_identifier": "com.myproduct.sixMonth"} + ] + } + """ + let offeringsData: OfferingsResponse.Offering = try JSONDecoder.default.decode( + jsonData: offeringsJSON.data(using: .utf8)! + ) + + let offering = try XCTUnwrap( + OfferingsFactory().createOffering(from: products, offering: offeringsData) + ) let expectedOfferings = Offerings(offerings: ["offering1": offering], currentOfferingID: "base") self.deviceCache.cache(offerings: expectedOfferings) - expect(self.deviceCache.cachedOfferings).to(beIdenticalTo(expectedOfferings)) + expect(self.deviceCache.cachedOfferings) === expectedOfferings } func testAssertionHappensWhenAppUserIDIsDeleted() { diff --git a/Tests/UnitTests/Mocks/MockBackend.swift b/Tests/UnitTests/Mocks/MockBackend.swift index 48cc4b5ad3..cd38ecb74f 100644 --- a/Tests/UnitTests/Mocks/MockBackend.swift +++ b/Tests/UnitTests/Mocks/MockBackend.swift @@ -110,7 +110,7 @@ class MockBackend: Backend { var invokedGetOfferingsForAppUserIDCount = 0 var invokedGetOfferingsForAppUserIDParameters: (appUserID: String?, completion: OfferingsResponseHandler?)? var invokedGetOfferingsForAppUserIDParametersList = [(appUserID: String?, completion: OfferingsResponseHandler?)]() - var stubbedGetOfferingsCompletionResult: Result<[String: Any], BackendError>? + var stubbedGetOfferingsCompletionResult: Result? override func getOfferings(appUserID: String, completion: @escaping OfferingsResponseHandler) { invokedGetOfferingsForAppUserID = true diff --git a/Tests/UnitTests/Mocks/MockOfferingsFactory.swift b/Tests/UnitTests/Mocks/MockOfferingsFactory.swift index 4aa1f6857a..0230cb6fe1 100644 --- a/Tests/UnitTests/Mocks/MockOfferingsFactory.swift +++ b/Tests/UnitTests/Mocks/MockOfferingsFactory.swift @@ -10,7 +10,10 @@ class MockOfferingsFactory: OfferingsFactory { var emptyOfferings = false var nilOfferings = false - override func createOfferings(from storeProductsByID: [String: StoreProduct], data: [String: Any]) -> Offerings? { + override func createOfferings( + from storeProductsByID: [String: StoreProduct], + data: OfferingsResponse + ) -> Offerings? { if emptyOfferings { return Offerings(offerings: [:], currentOfferingID: "base") } diff --git a/Tests/UnitTests/Mocks/MockOfferingsManager.swift b/Tests/UnitTests/Mocks/MockOfferingsManager.swift index 465675b516..d8e4d17a39 100644 --- a/Tests/UnitTests/Mocks/MockOfferingsManager.swift +++ b/Tests/UnitTests/Mocks/MockOfferingsManager.swift @@ -18,35 +18,35 @@ class MockOfferingsManager: OfferingsManager { var invokedOfferings = false var invokedOfferingsCount = 0 - var invokedOfferingsParameters: (appUserID: String, completion: ((Offerings?, Error?) -> Void)?)? - var invokedOfferingsParametersList = [(appUserID: String, completion: ((Offerings?, Error?) -> Void)?)]() - var stubbedOfferingsCompletionResult: (offerings: Offerings?, error: Error?)? + var invokedOfferingsParameters: (appUserID: String, completion: ((Result) -> Void)?)? + var invokedOfferingsParametersList = [(appUserID: String, completion: ((Result) -> Void)?)]() + var stubbedOfferingsCompletionResult: Result? - override func offerings(appUserID: String, completion: ((Offerings?, Error?) -> Void)?) { + override func offerings(appUserID: String, completion: ((Result) -> Void)?) { invokedOfferings = true invokedOfferingsCount += 1 invokedOfferingsParameters = (appUserID, completion) invokedOfferingsParametersList.append((appUserID, completion)) - completion?(stubbedOfferingsCompletionResult?.offerings, stubbedOfferingsCompletionResult?.error) + completion?(stubbedOfferingsCompletionResult!) } struct InvokedUpdateOfferingsCacheParameters { let appUserID: String let isAppBackgrounded: Bool - let completion: ((Offerings?, Error?) -> Void)? + let completion: ((Result) -> Void)? } var invokedUpdateOfferingsCache = false var invokedUpdateOfferingsCacheCount = 0 var invokedUpdateOfferingsCacheParameters: InvokedUpdateOfferingsCacheParameters? var invokedUpdateOfferingsCachesParametersList = [InvokedUpdateOfferingsCacheParameters]() - var stubbedUpdateOfferingsCompletionResult: (offerings: Offerings?, error: Error?)? + var stubbedUpdateOfferingsCompletionResult: Result? override func updateOfferingsCache( appUserID: String, isAppBackgrounded: Bool, - completion: ((Offerings?, Error?) -> Void)? + completion: ((Result) -> Void)? ) { invokedUpdateOfferingsCache = true invokedUpdateOfferingsCacheCount += 1 @@ -60,7 +60,7 @@ class MockOfferingsManager: OfferingsManager { invokedUpdateOfferingsCacheParameters = parameters invokedUpdateOfferingsCachesParametersList.append(parameters) - completion?(stubbedUpdateOfferingsCompletionResult?.offerings, stubbedUpdateOfferingsCompletionResult?.error) + completion?(stubbedUpdateOfferingsCompletionResult!) } } diff --git a/Tests/UnitTests/Networking/Backend/BackendGetOfferingsTests.swift b/Tests/UnitTests/Networking/Backend/BackendGetOfferingsTests.swift index 2392fe8892..f9bffb1217 100644 --- a/Tests/UnitTests/Networking/Backend/BackendGetOfferingsTests.swift +++ b/Tests/UnitTests/Networking/Backend/BackendGetOfferingsTests.swift @@ -29,7 +29,7 @@ class BackendGetOfferingsTests: BaseBackendTests { response: .init(statusCode: .success, response: Self.noOfferingsResponse as [String: Any]) ) - var result: Result<[String: Any], BackendError>? + var result: Result? backend.getOfferings(appUserID: Self.userID) { result = $0 @@ -70,27 +70,27 @@ class BackendGetOfferingsTests: BaseBackendTests { response: .init(statusCode: .success, response: Self.oneOfferingResponse) ) - var result: Result<[String: Any], BackendError>? + var result: Result? backend.getOfferings(appUserID: Self.userID) { result = $0 } expect(result).toEventuallyNot(beNil()) - let offerings = try XCTUnwrap(result?.value?["offerings"] as? [[String: Any]]) - let offeringA = try XCTUnwrap(offerings[0]) - let packages = try XCTUnwrap(offeringA["packages"] as? [[String: String]]) + let offerings = try XCTUnwrap(result?.value?.offerings) + let offeringA = try XCTUnwrap(offerings.first) + let packages = try XCTUnwrap(offeringA.packages) let packageA = packages[0] let packageB = packages[1] expect(offerings).to(haveCount(1)) - expect(offeringA["identifier"] as? String) == "offering_a" - expect(offeringA["description"] as? String) == "This is the base offering" - expect(packageA["identifier"]) == "$rc_monthly" - expect(packageA["platform_product_identifier"]) == "monthly_freetrial" - expect(packageB["identifier"]) == "$rc_annual" - expect(packageB["platform_product_identifier"]) == "annual_freetrial" - expect(result?.value?["current_offering_id"] as? String) == "offering_a" + expect(offeringA.identifier) == "offering_a" + expect(offeringA.description) == "This is the base offering" + expect(packageA.identifier) == "$rc_monthly" + expect(packageA.platformProductIdentifier) == "monthly_freetrial" + expect(packageB.identifier) == "$rc_annual" + expect(packageB.platformProductIdentifier) == "annual_freetrial" + expect(result?.value?.currentOfferingId) == "offering_a" } func testGetOfferingsFailSendsNil() { @@ -99,13 +99,14 @@ class BackendGetOfferingsTests: BaseBackendTests { response: .init(error: .unexpectedResponse(nil)) ) - var offerings: [String: Any]? = [:] + var result: Result? - backend.getOfferings(appUserID: Self.userID) { result in - offerings = result.value + backend.getOfferings(appUserID: Self.userID) { + result = $0 } - expect(offerings).toEventually(beNil()) + expect(result).toEventuallyNot(beNil()) + expect(result).to(beFailure()) } func testGetOfferingsNetworkErrorSendsError() { @@ -116,7 +117,7 @@ class BackendGetOfferingsTests: BaseBackendTests { response: .init(error: mockedError) ) - var result: Result<[String: Any], BackendError>? + var result: Result? backend.getOfferings(appUserID: Self.userID) { result = $0 } diff --git a/Tests/UnitTests/Purchasing/OfferingsManagerTests.swift b/Tests/UnitTests/Purchasing/OfferingsManagerTests.swift index 51748dbbe8..b2fbbd926e 100644 --- a/Tests/UnitTests/Purchasing/OfferingsManagerTests.swift +++ b/Tests/UnitTests/Purchasing/OfferingsManagerTests.swift @@ -48,37 +48,36 @@ extension OfferingsManagerTests { func testOfferingsForAppUserIDReturnsNilIfMissingStoreProduct() throws { // given mockOfferingsFactory.emptyOfferings = true - mockBackend.stubbedGetOfferingsCompletionResult = .success(MockData.anyBackendOfferingsData) + mockBackend.stubbedGetOfferingsCompletionResult = .success(MockData.anyBackendOfferingsResponse) // when - var obtainedOfferings: Offerings? - var completionCalled = false - offeringsManager.offerings(appUserID: MockData.anyAppUserID) { offerings, _ in - obtainedOfferings = offerings - completionCalled = true + var result: Result? + offeringsManager.offerings(appUserID: MockData.anyAppUserID) { + result = $0 } // then - expect(completionCalled).toEventually(beTrue()) - let unwrappedOfferings = try XCTUnwrap(obtainedOfferings) + expect(result).toEventuallyNot(beNil()) + expect(result).to(beSuccess()) + + let unwrappedOfferings = try XCTUnwrap(result?.value) expect(unwrappedOfferings["base"]).to(beNil()) } func testOfferingsForAppUserIDReturnsOfferingsIfSuccessBackendRequest() throws { // given - mockBackend.stubbedGetOfferingsCompletionResult = .success(MockData.anyBackendOfferingsData) + mockBackend.stubbedGetOfferingsCompletionResult = .success(MockData.anyBackendOfferingsResponse) // when - var obtainedOfferings: Offerings? - var completionCalled = false - offeringsManager.offerings(appUserID: MockData.anyAppUserID) { offerings, _ in - obtainedOfferings = offerings - completionCalled = true + var result: Result? + offeringsManager.offerings(appUserID: MockData.anyAppUserID) { + result = $0 } // then - expect(completionCalled).toEventually(beTrue()) - let unwrappedOfferings = try XCTUnwrap(obtainedOfferings) + expect(result).toEventuallyNot(beNil()) + + let unwrappedOfferings = try XCTUnwrap(result?.value) expect(unwrappedOfferings["base"]).toNot(beNil()) expect(unwrappedOfferings["base"]!.monthly).toNot(beNil()) expect(unwrappedOfferings["base"]!.monthly?.storeProduct).toNot(beNil()) @@ -90,83 +89,72 @@ extension OfferingsManagerTests { mockOfferingsFactory.emptyOfferings = true // when - var obtainedOfferings: Offerings? - var completionCalled = false - offeringsManager.offerings(appUserID: MockData.anyAppUserID) { offerings, _ in - obtainedOfferings = offerings - completionCalled = true + var result: Result? + offeringsManager.offerings(appUserID: MockData.anyAppUserID) { + result = $0 } // then - expect(completionCalled).toEventually(beTrue()) - expect(obtainedOfferings).to(beNil()) + expect(result).toEventuallyNot(beNil()) + expect(result).to(beFailure()) + expect(result?.error) == .backendError(.unexpectedBackendResponse(.customerInfoNil)) } func testOfferingsForAppUserIDReturnsConfigurationErrorIfBackendReturnsEmpty() throws { // given - mockBackend.stubbedGetOfferingsCompletionResult = .success([:]) + mockBackend.stubbedGetOfferingsCompletionResult = .success( + .init(currentOfferingId: "", offerings: []) + ) mockOfferingsFactory.emptyOfferings = true // when - var obtainedOfferings: Offerings? - var completionCalled = false - var obtainedError: OfferingsManager.Error? - offeringsManager.offerings(appUserID: MockData.anyAppUserID) { offerings, error in - obtainedOfferings = offerings - completionCalled = true - obtainedError = error + var result: Result? + offeringsManager.offerings(appUserID: MockData.anyAppUserID) { + result = $0 } // then - expect(completionCalled).toEventually(beTrue()) - expect(obtainedOfferings).to(beNil()) - let error = try XCTUnwrap(obtainedError) - expect(error) == .configurationError(Strings.offering.configuration_error_no_products_for_offering.description) + expect(result).toEventuallyNot(beNil()) + expect(result).to(beFailure()) + expect(result?.error) == .configurationError( + Strings.offering.configuration_error_no_products_for_offering.description + ) } func testOfferingsForAppUserIDReturnsConfigurationErrorIfProductsRequestsReturnsEmpty() throws { // given - mockBackend.stubbedGetOfferingsCompletionResult = .success(MockData.anyBackendOfferingsData) + mockBackend.stubbedGetOfferingsCompletionResult = .success(MockData.anyBackendOfferingsResponse) mockProductsManager.stubbedProductsCompletionResult = Set() // when - var obtainedOfferings: Offerings? - var completionCalled = false - var obtainedError: OfferingsManager.Error? - offeringsManager.offerings(appUserID: MockData.anyAppUserID) { offerings, error in - obtainedOfferings = offerings - completionCalled = true - obtainedError = error + var result: Result? + offeringsManager.offerings(appUserID: MockData.anyAppUserID) { + result = $0 } // then - expect(completionCalled).toEventually(beTrue()) - expect(obtainedOfferings).to(beNil()) - let error = try XCTUnwrap(obtainedError) - expect(error) == .configurationError(Strings.offering.configuration_error_skproducts_not_found.description) + expect(result).toEventuallyNot(beNil()) + expect(result).to(beFailure()) + expect(result?.error) == .configurationError( + Strings.offering.configuration_error_skproducts_not_found.description + ) } func testOfferingsForAppUserIDReturnsUnexpectedBackendResponseIfOfferingsFactoryCantCreateOfferings() throws { // given - mockBackend.stubbedGetOfferingsCompletionResult = .success(MockData.anyBackendOfferingsData) + mockBackend.stubbedGetOfferingsCompletionResult = .success(MockData.anyBackendOfferingsResponse) mockOfferingsFactory.nilOfferings = true // when - var obtainedOfferings: Offerings? - var completionCalled = false - var obtainedError: OfferingsManager.Error? - offeringsManager.offerings(appUserID: MockData.anyAppUserID) { offerings, error in - obtainedOfferings = offerings - completionCalled = true - obtainedError = error + var result: Result? + offeringsManager.offerings(appUserID: MockData.anyAppUserID) { + result = $0 } // then - expect(completionCalled).toEventually(beTrue()) - expect(obtainedOfferings).to(beNil()) - - let error = try XCTUnwrap(obtainedError) - expect(error) == .noOfferingsFound() + expect(result).toEventuallyNot(beNil()) + expect(result).to(beFailure()) + expect(result?.error) == .noOfferingsFound() } func testOfferingsForAppUserIDReturnsUnexpectedBackendErrorIfBadBackendRequest() throws { @@ -175,17 +163,15 @@ extension OfferingsManagerTests { mockOfferingsFactory.nilOfferings = true // when - var receivedError: OfferingsManager.Error? - var completionCalled = false - offeringsManager.offerings(appUserID: MockData.anyAppUserID) { _, error in - receivedError = error - completionCalled = true + var result: Result? + offeringsManager.offerings(appUserID: MockData.anyAppUserID) { + result = $0 } // then - expect(completionCalled).toEventually(beTrue()) - let unwrappedError = try XCTUnwrap(receivedError) - expect(unwrappedError) == .backendError(MockData.unexpectedBackendResponseError) + expect(result).toEventuallyNot(beNil()) + expect(result).to(beFailure()) + expect(result?.error) == .backendError(MockData.unexpectedBackendResponseError) } func testFailBackendDeviceCacheClearsOfferingsCache() { @@ -205,7 +191,7 @@ extension OfferingsManagerTests { func testUpdateOfferingsCacheOK() { // given - mockBackend.stubbedGetOfferingsCompletionResult = .success(MockData.anyBackendOfferingsData) + mockBackend.stubbedGetOfferingsCompletionResult = .success(MockData.anyBackendOfferingsResponse) let expectedCallCount = 1 // when @@ -235,19 +221,17 @@ private extension OfferingsManagerTests { enum MockData { static let anyAppUserID = "" - static let anyBackendOfferingsData: [String: Any] = [ - "offerings": [ - [ - "identifier": "base", - "description": "This is the base offering", - "packages": [ - ["identifier": "$rc_monthly", - "platform_product_identifier": "monthly_freetrial"] - ] - ] - ], - "current_offering_id": "base" - ] + + static let anyBackendOfferingsResponse: OfferingsResponse = .init( + currentOfferingId: "base", + offerings: [ + .init(identifier: "base", + description: "This is the base offering", + packages: [ + .init(identifier: "$rc_monthly", platformProductIdentifier: "monthly_freetrial") + ]) + ] + ) static let unexpectedBackendResponseError: BackendError = .unexpectedBackendResponse( .customerInfoNil ) diff --git a/Tests/UnitTests/Purchasing/OfferingsTests.swift b/Tests/UnitTests/Purchasing/OfferingsTests.swift index 794f0d6807..be780ffc75 100644 --- a/Tests/UnitTests/Purchasing/OfferingsTests.swift +++ b/Tests/UnitTests/Purchasing/OfferingsTests.swift @@ -15,15 +15,16 @@ import XCTest class OfferingsTests: XCTestCase { - let offeringsFactory = OfferingsFactory() + private let offeringsFactory = OfferingsFactory() func testPackageIsNotCreatedIfNoValidProducts() { - let package = offeringsFactory.createPackage(with: [ - "identifier": "$rc_monthly", - "platform_product_identifier": "com.myproduct.monthly" - ], storeProductsByID: [ - "com.myproduct.annual": StoreProduct(sk1Product: SK1Product()) - ], offeringIdentifier: "offering") + let package = self.offeringsFactory.createPackage( + with: .init(identifier: "$rc_monthly", platformProductIdentifier: "com.myproduct.monthly"), + productsByID: [ + "com.myproduct.annual": StoreProduct(sk1Product: SK1Product()) + ], + offeringIdentifier: "offering" + ) expect(package).to(beNil()) } @@ -33,38 +34,39 @@ class OfferingsTests: XCTestCase { let product = MockSK1Product(mockProductIdentifier: productIdentifier) let packageIdentifier = "$rc_monthly" let package = try XCTUnwrap( - offeringsFactory.createPackage(with: [ - "identifier": packageIdentifier, - "platform_product_identifier": productIdentifier - ], storeProductsByID: [ - productIdentifier: StoreProduct(sk1Product: product) - ], offeringIdentifier: "offering") + self.offeringsFactory.createPackage( + with: .init(identifier: packageIdentifier, platformProductIdentifier: productIdentifier), + productsByID: [ + productIdentifier: StoreProduct(sk1Product: product) + ], + offeringIdentifier: "offering" + ) ) expect(package.storeProduct.product).to(beAnInstanceOf(SK1StoreProduct.self)) let sk1StoreProduct = try XCTUnwrap(package.storeProduct.product as? SK1StoreProduct) expect(sk1StoreProduct.underlyingSK1Product).to(equal(product)) - expect(package.identifier).to(equal(packageIdentifier)) - expect(package.packageType).to(equal(PackageType.monthly)) + expect(package.identifier) == packageIdentifier + expect(package.packageType) == PackageType.monthly } func testOfferingIsNotCreatedIfNoValidPackage() { let products = ["com.myproduct.bad": StoreProduct(sk1Product: SK1Product())] - let offering = offeringsFactory.createOffering(from: products, offeringData: [ - "identifier": "offering_a", - "description": "This is the base offering", - "packages": [ - ["identifier": "$rc_monthly", - "platform_product_identifier": "com.myproduct.monthly"], - ["identifier": "$rc_annual", - "platform_product_identifier": "com.myproduct.annual"] - ] - ]) + let offering = self.offeringsFactory.createOffering( + from: products, + offering: .init( + identifier: "offering_a", + description: "This is the base offering", + packages: [ + .init(identifier: "$rc_monthly", platformProductIdentifier: "com.myproduct.monthly"), + .init(identifier: "$rc_annual", platformProductIdentifier: "com.myproduct.annual") + ]) + ) expect(offering).to(beNil()) } - func testOfferingIsCreatedIfValidPackages() { + func testOfferingIsCreatedIfValidPackages() throws { let annualProduct = MockSK1Product(mockProductIdentifier: "com.myproduct.annual") let monthlyProduct = MockSK1Product(mockProductIdentifier: "com.myproduct.monthly") let products = [ @@ -73,49 +75,47 @@ class OfferingsTests: XCTestCase { ] let offeringIdentifier = "offering_a" let serverDescription = "This is the base offering" - let offering = offeringsFactory.createOffering(from: products, offeringData: [ - "identifier": offeringIdentifier, - "description": serverDescription, - "packages": [ - ["identifier": "$rc_monthly", - "platform_product_identifier": "com.myproduct.monthly"], - ["identifier": "$rc_annual", - "platform_product_identifier": "com.myproduct.annual"], - ["identifier": "$rc_six_month", - "platform_product_identifier": "com.myproduct.sixMonth"] - ] - ]) - expect(offering).toNot(beNil()) - expect(offering?.identifier).to(equal(offeringIdentifier)) - expect(offering?.serverDescription).to(equal(serverDescription)) - expect(offering?.availablePackages).to(haveCount(2)) - expect(offering?.monthly).toNot(beNil()) - expect(offering?.annual).toNot(beNil()) - expect(offering?.sixMonth).to(beNil()) + let offering = try XCTUnwrap( + self.offeringsFactory.createOffering( + from: products, + offering: .init( + identifier: offeringIdentifier, + description: serverDescription, + packages: [ + .init(identifier: "$rc_monthly", platformProductIdentifier: "com.myproduct.monthly"), + .init(identifier: "$rc_annual", platformProductIdentifier: "com.myproduct.annual"), + .init(identifier: "$rc_six_month", platformProductIdentifier: "com.myproduct.sixMonth") + ]) + ) + ) + + expect(offering.identifier) == offeringIdentifier + expect(offering.serverDescription) == serverDescription + expect(offering.availablePackages).to(haveCount(2)) + expect(offering.monthly).toNot(beNil()) + expect(offering.annual).toNot(beNil()) + expect(offering.sixMonth).to(beNil()) } func testListOfOfferingsIsNilIfNoValidOffering() { - let offerings = offeringsFactory.createOfferings(from: [:], data: [ - "offerings": [ - [ - "identifier": "offering_a", - "description": "This is the base offering", - "packages": [ - ["identifier": "$rc_six_month", - "platform_product_identifier": "com.myproduct.sixMonth"] - ] - ], - [ - "identifier": "offering_b", - "description": "This is the base offering b", - "packages": [ - ["identifier": "$rc_monthly", - "platform_product_identifier": "com.myproduct.monthly"] - ] + let offerings = self.offeringsFactory.createOfferings( + from: [:], + data: .init( + currentOfferingId: "offering_a", + offerings: [ + .init(identifier: "offering_a", + description: "This is the base offering", + packages: [ + .init(identifier: "$rc_six_month", platformProductIdentifier: "com.myproduct.sixMonth") + ]), + .init(identifier: "offering_b", + description: "This is the base offering b", + packages: [ + .init(identifier: "$rc_monthly", platformProductIdentifier: "com.myproduct.monthly") + ]) ] - ], - "current_offering_id": "offering_a" - ]) + ) + ) expect(offerings).to(beNil()) } @@ -128,32 +128,29 @@ class OfferingsTests: XCTestCase { "com.myproduct.monthly": StoreProduct(sk1Product: monthlyProduct) ] let offerings = try XCTUnwrap( - offeringsFactory.createOfferings(from: products, data: [ - "offerings": [ - [ - "identifier": "offering_a", - "description": "This is the base offering", - "packages": [ - ["identifier": "$rc_six_month", - "platform_product_identifier": "com.myproduct.annual"] - ] - ], - [ - "identifier": "offering_b", - "description": "This is the base offering b", - "packages": [ - ["identifier": "$rc_monthly", - "platform_product_identifier": "com.myproduct.monthly"] - ] + self.offeringsFactory.createOfferings( + from: products, + data: .init( + currentOfferingId: "offering_a", + offerings: [ + .init(identifier: "offering_a", + description: "This is the base offering", + packages: [ + .init(identifier: "$rc_six_month", platformProductIdentifier: "com.myproduct.annual") + ]), + .init(identifier: "offering_b", + description: "This is the base offering b", + packages: [ + .init(identifier: "$rc_monthly", platformProductIdentifier: "com.myproduct.monthly") + ]) ] - ], - "current_offering_id": "offering_a" - ]) + ) + ) ) expect(offerings["offering_a"]).toNot(beNil()) expect(offerings["offering_b"]).toNot(beNil()) - expect(offerings.current).to(be(offerings["offering_a"])) + expect(offerings.current) == offerings["offering_a"] } func testLifetimePackage() throws { @@ -202,11 +199,15 @@ class OfferingsTests: XCTestCase { } func testOfferingsIsNilIfNoOfferingCanBeCreated() throws { - let data = [ + let json = """ + { "offerings": [], - "current_offering_id": nil - ] - let offerings = offeringsFactory.createOfferings(from: [:], data: data as [String: Any]) + "current_offering_id": null + } + """.data(using: .utf8)! + + let offeringsResponse: OfferingsResponse = try JSONDecoder.default.decode(jsonData: json) + let offerings = self.offeringsFactory.createOfferings(from: [:], data: offeringsResponse) expect(offerings).to(beNil()) } @@ -218,63 +219,56 @@ class OfferingsTests: XCTestCase { ) ] - let data: [String: Any] = [ - "offerings": [ - [ - "identifier": "offering_a", - "description": "This is the base offering", - "packages": [ - ["identifier": "$rc_six_month", - "platform_product_identifier": "com.myproduct.annual"] - ] - ] - ], - "current_offering_id": "offering_with_broken_product" - ] - let offerings = offeringsFactory.createOfferings(from: storeProductsByID, data: data) - - let unwrappedOfferings = try XCTUnwrap(offerings) - expect(unwrappedOfferings.current).to(beNil()) - } - - func testBadOfferingsDataReturnsNil() { - let data = [:] as [String: Any] - let offerings = offeringsFactory.createOfferings(from: [:], data: data as [String: Any]) + let response: OfferingsResponse = .init( + currentOfferingId: "offering_with_broken_product", + offerings: [ + .init(identifier: "offering_a", + description: "This is the base offering", + packages: [ + .init(identifier: "$rc_six_month", platformProductIdentifier: "com.myproduct.annual") + ]) + ] + ) + let offerings = try XCTUnwrap( + self.offeringsFactory.createOfferings(from: storeProductsByID, data: response) + ) - expect(offerings).to(beNil()) + expect(offerings.current).to(beNil()) } } private extension OfferingsTests { + func testPackageType(packageType: PackageType, product: StoreProduct? = nil) throws { - var identifier = Package.string(from: packageType) - if identifier == nil { + let defaultIdentifier: String = { if packageType == PackageType.unknown { - identifier = "$rc_unknown_id_from_the_future" + return "$rc_unknown_id_from_the_future" } else { - identifier = "custom" + return "custom" } - } + }() + + let identifier = Package.string(from: packageType) ?? defaultIdentifier let productIdentifier = product?.productIdentifier ?? "com.myproduct" let products = [ productIdentifier: product ?? StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: productIdentifier)) ] let offerings = try XCTUnwrap( - offeringsFactory.createOfferings(from: products, data: [ - "offerings": [ - [ - "identifier": "offering_a", - "description": "This is the base offering", - "packages": [ - ["identifier": identifier, - "platform_product_identifier": productIdentifier] - ] + offeringsFactory.createOfferings( + from: products, + data: .init( + currentOfferingId: "offering_a", + offerings: [ + .init(identifier: "offering_a", + description: "This is the base offering", + packages: [ + .init(identifier: identifier, platformProductIdentifier: productIdentifier) + ]) ] - ], - "current_offering_id": "offering_a" - ]) + ) + ) ) expect(offerings.current).toNot(beNil()) diff --git a/Tests/UnitTests/Purchasing/PurchasesTests.swift b/Tests/UnitTests/Purchasing/PurchasesTests.swift index 69208731a2..d6faca0522 100644 --- a/Tests/UnitTests/Purchasing/PurchasesTests.swift +++ b/Tests/UnitTests/Purchasing/PurchasesTests.swift @@ -192,25 +192,11 @@ class PurchasesTests: XCTestCase { return } if badOfferingsResponse { - completion(.success([:])) + completion(.failure(.networkError(.decoding(CodableError.invalidJSONObject(value: [:]), Data())))) return } - let offeringsData = [ - "offerings": [ - [ - "identifier": "base", - "description": "This is the base offering", - "packages": [ - ["identifier": "$rc_monthly", - "platform_product_identifier": "monthly_freetrial"] - ] - ] - ], - "current_offering_id": "base" - ] as [String: Any] - - completion(.success(offeringsData)) + completion(.success(.mockResponse)) } override func createAlias(appUserID: String, newAppUserID: String, completion: ((BackendError?) -> Void)?) { @@ -1829,10 +1815,11 @@ class PurchasesTests: XCTestCase { expect(self.mockOfferingsManager.invokedUpdateOfferingsCacheCount).toEventually(equal(0)) } - func testProductDataIsCachedForOfferings() { + func testProductDataIsCachedForOfferings() throws { setupPurchases() - mockOfferingsManager.stubbedOfferingsCompletionResult = - (offeringsFactory.createOfferings(from: [:], data: [:]), nil) + mockOfferingsManager.stubbedOfferingsCompletionResult = .success( + try XCTUnwrap(self.offeringsFactory.createOfferings(from: [:], data: .mockResponse)) + ) self.purchases?.getOfferings { (newOfferings, _) in let storeProduct = newOfferings!["base"]!.monthly!.storeProduct let product = storeProduct.sk1Product! @@ -2393,10 +2380,12 @@ class PurchasesTests: XCTestCase { self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) } - func testPostsOfferingIfPurchasingPackage() { + func testPostsOfferingIfPurchasingPackage() throws { setupPurchases() - mockOfferingsManager.stubbedOfferingsCompletionResult = - (offeringsFactory.createOfferings(from: [:], data: [:]), nil) + mockOfferingsManager.stubbedOfferingsCompletionResult = .success( + try XCTUnwrap(self.offeringsFactory.createOfferings(from: [:], data: .mockResponse)) + ) + self.purchases!.getOfferings { (newOfferings, _) in let package = newOfferings!["base"]!.monthly! self.purchases!.purchase(package: package) { (_, _, _, _) in @@ -2424,12 +2413,14 @@ class PurchasesTests: XCTestCase { } } - func testPurchasingPackageDoesntThrowPurchaseAlreadyInProgressIfCallbackMakesANewPurchase() { + func testPurchasingPackageDoesntThrowPurchaseAlreadyInProgressIfCallbackMakesANewPurchase() throws { setupPurchases() var receivedError: NSError? var secondCompletionCalled = false - mockOfferingsManager.stubbedOfferingsCompletionResult = - (offeringsFactory.createOfferings(from: [:], data: [:]), nil) + mockOfferingsManager.stubbedOfferingsCompletionResult = .success( + try XCTUnwrap(self.offeringsFactory.createOfferings(from: [:], data: .mockResponse)) + ) + self.purchases!.getOfferings { (newOfferings, _) in let package = newOfferings!["base"]!.monthly! self.purchases!.purchase(package: package) { _, _, _, _ in @@ -2715,3 +2706,18 @@ class PurchasesTests: XCTestCase { } } + +private extension OfferingsResponse { + + static let mockResponse: Self = .init( + currentOfferingId: "base", + offerings: [ + .init(identifier: "base", + description: "This is the base offering", + packages: [ + .init(identifier: "$rc_monthly", platformProductIdentifier: "monthly_freetrial") + ]) + ] + ) + +}