Przeglądaj źródła

Make InAppReceiptVerificator testable, add unit tests

Andrea Bizzotto 8 lat temu
rodzic
commit
aadfd8402b

+ 6 - 3
SwiftyStoreKit.xcodeproj/project.pbxproj

@@ -47,6 +47,7 @@
 		65BB6CE81DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */; };
 		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 */; };
 		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 */; };
 		65BB6CEA1DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */; };
+		65CEF0F41ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65CEF0F31ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift */; };
 		65E9E0791ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */; };
 		65E9E0791ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */; };
 		65E9E07A1ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */; };
 		65E9E07A1ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */; };
 		65E9E07B1ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */; };
 		65E9E07B1ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */; };
@@ -182,6 +183,7 @@
 		658A084B1E2EC5960074A98F /* PaymentQueueSpy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentQueueSpy.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftyStoreKit+Types.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>"; };
 		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>"; };
 		65F70AC61E2ECBB300BF040D /* PaymentTransactionObserverFake.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentTransactionObserverFake.swift; sourceTree = "<group>"; };
 		65F70AC81E2EDC3700BF040D /* PaymentsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentsController.swift; sourceTree = "<group>"; };
 		65F70AC81E2EDC3700BF040D /* PaymentsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentsController.swift; sourceTree = "<group>"; };
@@ -338,6 +340,7 @@
 				650307F11E3163AA001332A4 /* RestorePurchasesControllerTests.swift */,
 				650307F11E3163AA001332A4 /* RestorePurchasesControllerTests.swift */,
 				C3099C181E3206C700392A54 /* CompleteTransactionsControllerTests.swift */,
 				C3099C181E3206C700392A54 /* CompleteTransactionsControllerTests.swift */,
 				65B8C9281EC0BE62009439D9 /* InAppReceiptTests.swift */,
 				65B8C9281EC0BE62009439D9 /* InAppReceiptTests.swift */,
+				65CEF0F31ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift */,
 				658A084B1E2EC5960074A98F /* PaymentQueueSpy.swift */,
 				658A084B1E2EC5960074A98F /* PaymentQueueSpy.swift */,
 				65F70AC61E2ECBB300BF040D /* PaymentTransactionObserverFake.swift */,
 				65F70AC61E2ECBB300BF040D /* PaymentTransactionObserverFake.swift */,
 				C3099C081E2FCE3A00392A54 /* TestProduct.swift */,
 				C3099C081E2FCE3A00392A54 /* TestProduct.swift */,
@@ -563,7 +566,6 @@
 					};
 					};
 					6502F5FD1B985833004E342D = {
 					6502F5FD1B985833004E342D = {
 						CreatedOnToolsVersion = 7.0;
 						CreatedOnToolsVersion = 7.0;
-						DevelopmentTeam = M54ZVB688G;
 						LastSwiftMigration = 0800;
 						LastSwiftMigration = 0800;
 						ProvisioningStyle = Automatic;
 						ProvisioningStyle = Automatic;
 					};
 					};
@@ -772,6 +774,7 @@
 			files = (
 			files = (
 				C3099C071E2FCDAA00392A54 /* PaymentsControllerTests.swift in Sources */,
 				C3099C071E2FCDAA00392A54 /* PaymentsControllerTests.swift in Sources */,
 				650307F21E3163AA001332A4 /* RestorePurchasesControllerTests.swift in Sources */,
 				650307F21E3163AA001332A4 /* RestorePurchasesControllerTests.swift in Sources */,
+				65CEF0F41ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift in Sources */,
 				C3099C0B1E2FD13200392A54 /* TestPaymentTransaction.swift in Sources */,
 				C3099C0B1E2FD13200392A54 /* TestPaymentTransaction.swift in Sources */,
 				65F70AC71E2ECBB300BF040D /* PaymentTransactionObserverFake.swift in Sources */,
 				65F70AC71E2ECBB300BF040D /* PaymentTransactionObserverFake.swift in Sources */,
 				658A084A1E2EC5350074A98F /* PaymentQueueControllerTests.swift in Sources */,
 				658A084A1E2EC5350074A98F /* PaymentQueueControllerTests.swift in Sources */,
@@ -1024,7 +1027,7 @@
 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
-				DEVELOPMENT_TEAM = M54ZVB688G;
+				DEVELOPMENT_TEAM = "";
 				INFOPLIST_FILE = "$(SRCROOT)/SwiftyStoreKit-iOS-Demo/Info.plist";
 				INFOPLIST_FILE = "$(SRCROOT)/SwiftyStoreKit-iOS-Demo/Info.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -1040,7 +1043,7 @@
 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
-				DEVELOPMENT_TEAM = M54ZVB688G;
+				DEVELOPMENT_TEAM = "";
 				INFOPLIST_FILE = "$(SRCROOT)/SwiftyStoreKit-iOS-Demo/Info.plist";
 				INFOPLIST_FILE = "$(SRCROOT)/SwiftyStoreKit-iOS-Demo/Info.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";

+ 2 - 1
SwiftyStoreKit/InAppReceiptRefreshRequest.swift

@@ -34,6 +34,7 @@ class InAppReceiptRefreshRequest: NSObject, SKRequestDelegate {
     }
     }
 
 
     typealias RequestCallback = (ResultType) -> Void
     typealias RequestCallback = (ResultType) -> Void
+    typealias ReceiptRefresh = (_ receiptProperties: [String : Any]?, _ callback: @escaping RequestCallback) -> InAppReceiptRefreshRequest
 
 
     class func refresh(_ receiptProperties: [String : Any]? = nil, callback: @escaping RequestCallback) -> InAppReceiptRefreshRequest {
     class func refresh(_ receiptProperties: [String : Any]? = nil, callback: @escaping RequestCallback) -> InAppReceiptRefreshRequest {
         let request = InAppReceiptRefreshRequest(receiptProperties: receiptProperties, callback: callback)
         let request = InAppReceiptRefreshRequest(receiptProperties: receiptProperties, callback: callback)
@@ -48,7 +49,7 @@ class InAppReceiptRefreshRequest: NSObject, SKRequestDelegate {
         refreshReceiptRequest.delegate = nil
         refreshReceiptRequest.delegate = nil
     }
     }
 
 
-    private init(receiptProperties: [String : Any]? = nil, callback: @escaping RequestCallback) {
+    init(receiptProperties: [String : Any]? = nil, callback: @escaping RequestCallback) {
         self.callback = callback
         self.callback = callback
         self.refreshReceiptRequest = SKReceiptRefreshRequest(receiptProperties: receiptProperties)
         self.refreshReceiptRequest = SKReceiptRefreshRequest(receiptProperties: receiptProperties)
         super.init()
         super.init()

+ 15 - 10
SwiftyStoreKit/InAppReceiptVerificator.swift

@@ -27,39 +27,44 @@ import Foundation
 
 
 class InAppReceiptVerificator: NSObject {
 class InAppReceiptVerificator: NSObject {
 
 
-    static var appStoreReceiptUrl: URL? {
-        return Bundle.main.appStoreReceiptURL
+    let appStoreReceiptURL: URL?
+    init(appStoreReceiptURL: URL? = Bundle.main.appStoreReceiptURL) {
+        self.appStoreReceiptURL = appStoreReceiptURL
     }
     }
-    
-    static var appStoreReceiptData: Data? {
-        guard let receiptDataURL = appStoreReceiptUrl, let data = try? Data(contentsOf: receiptDataURL) else {
+
+    var appStoreReceiptData: Data? {
+        guard let receiptDataURL = appStoreReceiptURL,
+            let data = try? Data(contentsOf: receiptDataURL) else {
             return nil
             return nil
         }
         }
         return data
         return data
     }
     }
 
 
     private var receiptRefreshRequest: InAppReceiptRefreshRequest?
     private var receiptRefreshRequest: InAppReceiptRefreshRequest?
-    
+
     /**
     /**
      *  Verify application receipt
      *  Verify application receipt
      *  - Parameter password: Only used for receipts that contain auto-renewable subscriptions. Your app’s shared secret (a hexadecimal string).
      *  - Parameter password: Only used for receipts that contain auto-renewable subscriptions. Your app’s shared secret (a hexadecimal string).
      *  - Parameter session: the session used to make remote call.
      *  - Parameter session: the session used to make remote call.
      *  - Parameter completion: handler for result
      *  - Parameter completion: handler for result
      */
      */
-    public func verifyReceipt(using validator: ReceiptValidator, password: String? = nil, completion: @escaping (VerifyReceiptResult) -> Void) {
+    public func verifyReceipt(using validator: ReceiptValidator,
+                              password: String? = nil,
+                              refresh: InAppReceiptRefreshRequest.ReceiptRefresh = InAppReceiptRefreshRequest.refresh,
+                              completion: @escaping (VerifyReceiptResult) -> Void) {
         
         
-        if let receiptData = InAppReceiptVerificator.appStoreReceiptData {
+        if let receiptData = appStoreReceiptData {
             
             
             verify(receiptData: receiptData, using: validator, password: password, completion: completion)
             verify(receiptData: receiptData, using: validator, password: password, completion: completion)
         } else {
         } else {
             
             
-            receiptRefreshRequest = InAppReceiptRefreshRequest.refresh { result in
+            receiptRefreshRequest = refresh(nil) { result in
                 
                 
                 self.receiptRefreshRequest = nil
                 self.receiptRefreshRequest = nil
                 
                 
                 switch result {
                 switch result {
                 case .success:
                 case .success:
-                    if let receiptData = InAppReceiptVerificator.appStoreReceiptData {
+                    if let receiptData = self.appStoreReceiptData {
                         self.verify(receiptData: receiptData, using: validator, password: password, completion: completion)
                         self.verify(receiptData: receiptData, using: validator, password: password, completion: completion)
                     } else {
                     } else {
                         completion(.error(error: .noReceiptData))
                         completion(.error(error: .noReceiptData))

+ 1 - 1
SwiftyStoreKit/SwiftyStoreKit.swift

@@ -184,7 +184,7 @@ extension SwiftyStoreKit {
      * Return receipt data from the application bundle. This is read from Bundle.main.appStoreReceiptURL
      * Return receipt data from the application bundle. This is read from Bundle.main.appStoreReceiptURL
      */
      */
     public static var localReceiptData: Data? {
     public static var localReceiptData: Data? {
-        return InAppReceiptVerificator.appStoreReceiptData
+        return sharedInstance.receiptVerificator.appStoreReceiptData
     }
     }
 
 
     /**
     /**

+ 226 - 0
SwiftyStoreKitTests/InAppReceiptVerificatorTests.swift

@@ -0,0 +1,226 @@
+//
+//  InAppReceiptVerificatorTests.swift
+//  SwiftyStoreKit
+//
+//  Created by Andrea Bizzotto on 17/05/2017.
+// 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 TestReceiptValidator: ReceiptValidator {
+    var validateCalled = false
+    func validate(receipt: String, password autoRenewPassword: String?, completion: @escaping (VerifyReceiptResult) -> Void) {
+        validateCalled = true
+        completion(.success(receipt: [:]))
+    }
+}
+
+class TestInAppReceiptRefreshRequest: InAppReceiptRefreshRequest {
+    
+    override func start() {
+        // do nothing
+    }
+}
+
+extension VerifyReceiptResult: Equatable {
+    
+    static public func == (lhs: VerifyReceiptResult, rhs: VerifyReceiptResult) -> Bool {
+        switch (lhs, rhs) {
+        case (.success(_), .success(_)): return true
+        case (.error(let lhsError), .error(let rhsError)): return lhsError == rhsError
+        default: return false
+        }
+    }
+}
+
+extension ReceiptError: Equatable {
+    
+    static public func == (lhs: ReceiptError, rhs: ReceiptError) -> Bool {
+        switch (lhs, rhs) {
+        case (.noReceiptData, .noReceiptData): return true
+        case (.noRemoteData, .noRemoteData): return true
+        case (.requestBodyEncodeError(_), .requestBodyEncodeError(_)): return true
+        case (.networkError(_), .networkError(_)): return true
+        case (.jsonDecodeError(_), .jsonDecodeError(_)): return true
+        case (.receiptInvalid(_, _), .receiptInvalid(_, _)): return true
+        default: return false
+        }
+    }
+}
+
+class InAppReceiptVerificatorTests: XCTestCase {
+    
+    // MARK: refresh tests (no receipt url or no receipt data)
+    func testVerifyReceipt_when_appStoreReceiptURLIsNil_then_callsRefresh() {
+        
+        let validator = TestReceiptValidator()
+        let verificator = InAppReceiptVerificator(appStoreReceiptURL: nil)
+        
+        var refreshCalled = false
+        verificator.verifyReceipt(using: validator, password: nil, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
+            
+            refreshCalled = true
+            return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
+            
+        }) { _ in
+            
+        }
+        XCTAssertTrue(refreshCalled)
+    }
+
+    func testVerifyReceipt_when_appStoreReceiptURLIsNotNil_noReceiptData_then_callsRefresh() {
+        
+        let testReceiptURL = makeReceiptURL()
+        
+        let validator = TestReceiptValidator()
+        let verificator = InAppReceiptVerificator(appStoreReceiptURL: testReceiptURL)
+        
+        var refreshCalled = false
+        verificator.verifyReceipt(using: validator, password: nil, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
+            
+            refreshCalled = true
+            return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
+            
+        }) { _ in
+            
+        }
+        XCTAssertTrue(refreshCalled)
+    }
+
+    func testVerifyReceipt_when_appStoreReceiptURLIsNil_refreshCallbackError_then_errorNetworkError() {
+        
+        let validator = TestReceiptValidator()
+        let verificator = InAppReceiptVerificator(appStoreReceiptURL: nil)
+        let refreshError = NSError(domain: "", code: 0, userInfo: nil)
+        
+        verificator.verifyReceipt(using: validator, password: nil, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
+            
+            callback(.error(e: refreshError))
+            return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
+            
+        }) { result in
+            
+            XCTAssertEqual(result, VerifyReceiptResult.error(error: ReceiptError.networkError(error: refreshError)))
+        }
+    }
+
+    func testVerifyReceipt_when_appStoreReceiptURLIsNil_refreshCallbackSuccess_receiptDataNotWritten_then_errorNoReceiptData_validateNotCalled() {
+        
+        let validator = TestReceiptValidator()
+        let verificator = InAppReceiptVerificator(appStoreReceiptURL: nil)
+        
+        verificator.verifyReceipt(using: validator, password: nil, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
+            
+            callback(.success)
+            return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
+            
+        }) { result in
+
+            XCTAssertEqual(result, VerifyReceiptResult.error(error: ReceiptError.noReceiptData))
+        }
+        XCTAssertFalse(validator.validateCalled)
+    }
+
+    func testVerifyReceipt_when_appStoreReceiptURLIsNil_noReceiptData_refreshCallbackSuccess_receiptDataWritten_then_errorNoReceiptData_validateNotCalled() {
+        
+        let testReceiptURL = makeReceiptURL()
+        
+        let validator = TestReceiptValidator()
+        let verificator = InAppReceiptVerificator(appStoreReceiptURL: nil)
+        
+        verificator.verifyReceipt(using: validator, password: nil, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
+            
+            writeReceiptData(to: testReceiptURL)
+            callback(.success)
+            return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
+            
+        }) { result in
+            
+            XCTAssertEqual(result, VerifyReceiptResult.error(error: ReceiptError.noReceiptData))
+        }
+        XCTAssertFalse(validator.validateCalled)
+        removeReceiptData(at: testReceiptURL)
+    }
+    
+    func testVerifyReceipt_when_appStoreReceiptURLIsNotNil_noReceiptData_refreshCallbackSuccess_receiptDataWritten_then_validateIsCalled() {
+        
+        let testReceiptURL = makeReceiptURL()
+        
+        let validator = TestReceiptValidator()
+        let verificator = InAppReceiptVerificator(appStoreReceiptURL: testReceiptURL)
+        
+        verificator.verifyReceipt(using: validator, password: nil, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
+            
+            writeReceiptData(to: testReceiptURL)
+            callback(.success)
+            return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
+            
+        }) { _ in
+            
+        }
+        XCTAssertTrue(validator.validateCalled)
+        removeReceiptData(at: testReceiptURL)
+    }
+    
+    // MARK: non-refresh tests (receipt url and data are set)
+    func testVerifyReceipt_when_appStoreReceiptURLIsNotNil_hasReceiptData_then_refreshNotCalled_validateIsCalled() {
+        
+        let testReceiptURL = makeReceiptURL()
+        writeReceiptData(to: testReceiptURL)
+
+        let validator = TestReceiptValidator()
+        let verificator = InAppReceiptVerificator(appStoreReceiptURL: testReceiptURL)
+        
+        verificator.verifyReceipt(using: validator, password: nil, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
+            
+            XCTFail("refresh should not be called if we already have a receipt")
+            return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
+            
+        }) { _ in
+            
+        }
+        XCTAssertTrue(validator.validateCalled)
+        removeReceiptData(at: testReceiptURL)
+    }
+    
+    // MARK: Helpers
+    func makeReceiptURL() -> URL {
+        
+        guard let testFolderURL = try? FileManager.default.url(for: .documentDirectory, in: .allDomainsMask, appropriateFor: nil, create: false) else {
+            fatalError("Invalid test folder")
+        }
+        return testFolderURL.appendingPathComponent("receipt.data")
+    }
+    
+    func writeReceiptData(to url: URL) {
+        
+        guard let testReceiptData = NSData(base64Encoded: "encrypted-receipt", options: .ignoreUnknownCharacters) else {
+            fatalError("Invalid receipt data")
+        }
+        try? testReceiptData.write(to: url)
+    }
+    
+    func removeReceiptData(at url: URL) {
+        try? FileManager.default.removeItem(at: url)
+    }
+
+}