Permalink
Browse files

Add ProductsInfoControllerTests, better dependency injection for Prod…

…uctsInfoController
  • Loading branch information...
bizz84 committed Aug 21, 2017
1 parent 1e73de5 commit ca94438d4b02e77f56cef5bba236e7dd02e114cd
@@ -47,6 +47,7 @@
65BB6CE81DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */; };
65BB6CE91DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */; };
65BB6CEA1DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */; };
65BF8E301F4AEEBA00CBFC00 /* ProductsInfoControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BF8E2F1F4AEEBA00CBFC00 /* ProductsInfoControllerTests.swift */; };
65CEF0F41ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65CEF0F31ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift */; };
65E9E0791ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */; };
65E9E07A1ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */; };
@@ -183,6 +184,7 @@
658A084B1E2EC5960074A98F /* PaymentQueueSpy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentQueueSpy.swift; sourceTree = "<group>"; };
65B8C9281EC0BE62009439D9 /* InAppReceiptTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppReceiptTests.swift; sourceTree = "<group>"; };
65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftyStoreKit+Types.swift"; sourceTree = "<group>"; };
65BF8E2F1F4AEEBA00CBFC00 /* ProductsInfoControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsInfoControllerTests.swift; sourceTree = "<group>"; };
65CEF0F31ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppReceiptVerificatorTests.swift; sourceTree = "<group>"; };
65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppReceiptVerificator.swift; sourceTree = "<group>"; };
65F70AC61E2ECBB300BF040D /* PaymentTransactionObserverFake.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentTransactionObserverFake.swift; sourceTree = "<group>"; };
@@ -345,6 +347,7 @@
65F70AC61E2ECBB300BF040D /* PaymentTransactionObserverFake.swift */,
C3099C081E2FCE3A00392A54 /* TestProduct.swift */,
C3099C0A1E2FD13200392A54 /* TestPaymentTransaction.swift */,
65BF8E2F1F4AEEBA00CBFC00 /* ProductsInfoControllerTests.swift */,
);
path = SwiftyStoreKitTests;
sourceTree = "<group>";
@@ -774,6 +777,7 @@
files = (
C3099C071E2FCDAA00392A54 /* PaymentsControllerTests.swift in Sources */,
650307F21E3163AA001332A4 /* RestorePurchasesControllerTests.swift in Sources */,
65BF8E301F4AEEBA00CBFC00 /* ProductsInfoControllerTests.swift in Sources */,
65CEF0F41ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift in Sources */,
C3099C0B1E2FD13200392A54 /* TestPaymentTransaction.swift in Sources */,
65F70AC71E2ECBB300BF040D /* PaymentTransactionObserverFake.swift in Sources */,
@@ -26,24 +26,16 @@ import StoreKit
typealias InAppProductRequestCallback = (RetrieveResults) -> Void
protocol InAppProductRetriever: class {
func retrieveProducts(productIds: Set<String>, callback: @escaping InAppProductRequestCallback) -> InAppProductQueryRequest
protocol InAppProductRequest: class {
func start()
func cancel()
}
class InAppProductQueryRetriever: InAppProductRetriever {
func retrieveProducts(productIds: Set<String>, callback: @escaping InAppProductRequestCallback) -> InAppProductQueryRequest {
let request = InAppProductQueryRequest(productIds: productIds, callback: callback)
request.start()
return request
}
}
class InAppProductQueryRequest: NSObject, SKProductsRequestDelegate {
class InAppProductQueryRequest: NSObject, InAppProductRequest, SKProductsRequestDelegate {
private let callback: InAppProductRequestCallback
private let request: SKProductsRequest
// http://stackoverflow.com/questions/24011575/what-is-the-difference-between-a-weak-reference-and-an-unowned-reference
deinit {
request.delegate = nil
}
@@ -25,36 +25,51 @@
import Foundation
import StoreKit
protocol InAppProductRequestBuilder: class {
func request(productIds: Set<String>, callback: @escaping InAppProductRequestCallback) -> InAppProductRequest
}
class InAppProductQueryRequestBuilder: InAppProductRequestBuilder {
func request(productIds: Set<String>, callback: @escaping InAppProductRequestCallback) -> InAppProductRequest {
return InAppProductQueryRequest(productIds: productIds, callback: callback)
}
}
class ProductsInfoController: NSObject {
struct InAppProductQuery {
let request: InAppProductQueryRequest
let request: InAppProductRequest
var completionHandlers: [InAppProductRequestCallback]
}
let inAppProductRetriever: InAppProductRetriever
init(inAppProductRetriever: InAppProductRetriever = InAppProductQueryRetriever()) {
self.inAppProductRetriever = inAppProductRetriever
let inAppProductRequestBuilder: InAppProductRequestBuilder
init(inAppProductRequestBuilder: InAppProductRequestBuilder = InAppProductQueryRequestBuilder()) {
self.inAppProductRequestBuilder = inAppProductRequestBuilder
}
// As we can have multiple inflight queries and purchases, we store them in a dictionary by product id
private var inflightQueries: [Set<String>: InAppProductQuery] = [:]
// As we can have multiple inflight requests, we store them in a dictionary by product ids
private var inflightRequests: [Set<String>: InAppProductQuery] = [:]
func retrieveProductsInfo(_ productIds: Set<String>, completion: @escaping (RetrieveResults) -> Void) {
if inflightQueries[productIds] == nil {
let request = self.inAppProductRetriever.retrieveProducts(productIds: productIds) { results in
if inflightRequests[productIds] == nil {
let request = inAppProductRequestBuilder.request(productIds: productIds) { results in
if let query = self.inflightQueries[productIds] {
if let query = self.inflightRequests[productIds] {
for completion in query.completionHandlers {
completion(results)
}
self.inflightQueries[productIds] = nil
self.inflightRequests[productIds] = nil
} else {
// should not get here, but if it does it seems reasonable to call the outer completion block
completion(results)
}
}
inflightQueries[productIds] = InAppProductQuery(request: request, completionHandlers: [completion])
inflightRequests[productIds] = InAppProductQuery(request: request, completionHandlers: [completion])
request.start()
} else {
inflightQueries[productIds]!.completionHandlers.append(completion)
inflightRequests[productIds]!.completionHandlers.append(completion)
}
}
}
@@ -0,0 +1,120 @@
//
// ProductsInfoControllerTests.swift
// SwiftyStoreKit
//
// Copyright (c) 2017 Andrea Bizzotto (bizz84@gmail.com)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import XCTest
@testable import SwiftyStoreKit
class TestInAppProductRequest: InAppProductRequest {
private let productIds: Set<String>
private let callback: InAppProductRequestCallback
init(productIds: Set<String>, callback: @escaping InAppProductRequestCallback) {
self.productIds = productIds
self.callback = callback
}
func start() {
}
func cancel() {
}
func fireCallback() {
callback(RetrieveResults(retrievedProducts: [], invalidProductIDs: [], error: nil))
}
}
class TestInAppProductRequestBuilder: InAppProductRequestBuilder {
var requests: [ TestInAppProductRequest ] = []
func request(productIds: Set<String>, callback: @escaping InAppProductRequestCallback) -> InAppProductRequest {
let request = TestInAppProductRequest(productIds: productIds, callback: callback)
requests.append(request)
return request
}
func fireCallbacks() {
requests.forEach {
$0.fireCallback()
}
requests = []
}
}
class ProductsInfoControllerTests: XCTestCase {
let sampleProductIdentifiers: Set<String> = ["com.iap.purchase1"]
func testRetrieveProductsInfo_when_calledOnce_then_completionCalledOnce() {
let requestBuilder = TestInAppProductRequestBuilder()
let productInfoController = ProductsInfoController(inAppProductRequestBuilder: requestBuilder)
var completionCount = 0
productInfoController.retrieveProductsInfo(sampleProductIdentifiers) { _ in
completionCount += 1
}
requestBuilder.fireCallbacks()
XCTAssertEqual(completionCount, 1)
}
func testRetrieveProductsInfo_when_calledTwiceConcurrently_then_eachCompletionCalledOnce() {
let requestBuilder = TestInAppProductRequestBuilder()
let productInfoController = ProductsInfoController(inAppProductRequestBuilder: requestBuilder)
var completionCount = 0
productInfoController.retrieveProductsInfo(sampleProductIdentifiers) { _ in
completionCount += 1
}
productInfoController.retrieveProductsInfo(sampleProductIdentifiers) { _ in
completionCount += 1
}
requestBuilder.fireCallbacks()
XCTAssertEqual(completionCount, 2)
}
func testRetrieveProductsInfo_when_calledTwiceNotConcurrently_then_eachCompletionCalledOnce() {
let requestBuilder = TestInAppProductRequestBuilder()
let productInfoController = ProductsInfoController(inAppProductRequestBuilder: requestBuilder)
var completionCount = 0
productInfoController.retrieveProductsInfo(sampleProductIdentifiers) { _ in
completionCount += 1
}
requestBuilder.fireCallbacks()
XCTAssertEqual(completionCount, 1)
productInfoController.retrieveProductsInfo(sampleProductIdentifiers) { _ in
completionCount += 1
}
requestBuilder.fireCallbacks()
XCTAssertEqual(completionCount, 2)
}
}

0 comments on commit ca94438

Please sign in to comment.