Skip to content

Commit

Permalink
Add ProductsInfoControllerTests, better dependency injection for Prod…
Browse files Browse the repository at this point in the history
…uctsInfoController
  • Loading branch information
bizz84 committed Aug 21, 2017
1 parent 1e73de5 commit ca94438
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 25 deletions.
4 changes: 4 additions & 0 deletions SwiftyStoreKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -345,6 +347,7 @@
65F70AC61E2ECBB300BF040D /* PaymentTransactionObserverFake.swift */,
C3099C081E2FCE3A00392A54 /* TestProduct.swift */,
C3099C0A1E2FD13200392A54 /* TestPaymentTransaction.swift */,
65BF8E2F1F4AEEBA00CBFC00 /* ProductsInfoControllerTests.swift */,
);
path = SwiftyStoreKitTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
18 changes: 5 additions & 13 deletions SwiftyStoreKit/InAppProductQueryRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
39 changes: 27 additions & 12 deletions SwiftyStoreKit/ProductsInfoController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
120 changes: 120 additions & 0 deletions SwiftyStoreKitTests/ProductsInfoControllerTests.swift
Original file line number Diff line number Diff line change
@@ -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.