From 7ce49f0e5f54194cd611fdcf631b4a03ad0a82f2 Mon Sep 17 00:00:00 2001 From: Peter Nicholls Date: Fri, 8 Jul 2016 14:00:59 +1000 Subject: [PATCH] Added ability to set cassette request options The options added so far allow a user to select what attributes a request should be matched on. Request matching options include URL, Path, HTTPMethod and HTTPBody. --- DVR.xcodeproj/project.pbxproj | 12 +++ DVR/Cassette.swift | 129 +++++++++++++++++++++++++++++-- DVR/CassetteOptions.swift | 20 +++++ DVR/RequestMatchingOptions.swift | 33 ++++++++ DVR/Session.swift | 11 ++- DVR/SessionDataTask.swift | 6 +- DVR/Tests/SessionTests.swift | 19 +++++ 7 files changed, 217 insertions(+), 13 deletions(-) create mode 100644 DVR/CassetteOptions.swift create mode 100644 DVR/RequestMatchingOptions.swift diff --git a/DVR.xcodeproj/project.pbxproj b/DVR.xcodeproj/project.pbxproj index d80fe53..33a259b 100644 --- a/DVR.xcodeproj/project.pbxproj +++ b/DVR.xcodeproj/project.pbxproj @@ -47,6 +47,10 @@ B19D62721CB1A27700E16D11 /* upload-data.json in Resources */ = {isa = PBXBuildFile; fileRef = B19D62701CB1A27700E16D11 /* upload-data.json */; }; B19D62771CB1A42600E16D11 /* upload-file.json in Resources */ = {isa = PBXBuildFile; fileRef = B19D62761CB1A42600E16D11 /* upload-file.json */; }; B19D62781CB1A42600E16D11 /* upload-file.json in Resources */ = {isa = PBXBuildFile; fileRef = B19D62761CB1A42600E16D11 /* upload-file.json */; }; + C191AEF11D2F5B32001EB011 /* CassetteOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C191AEEF1D2F5B32001EB011 /* CassetteOptions.swift */; }; + C191AEF21D2F5B32001EB011 /* CassetteOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C191AEEF1D2F5B32001EB011 /* CassetteOptions.swift */; }; + C191AEF31D2F5B32001EB011 /* RequestMatchingOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C191AEF01D2F5B32001EB011 /* RequestMatchingOptions.swift */; }; + C191AEF41D2F5B32001EB011 /* RequestMatchingOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C191AEF01D2F5B32001EB011 /* RequestMatchingOptions.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -92,6 +96,8 @@ B19D626D1CB1A0DD00E16D11 /* SessionUploadTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionUploadTests.swift; sourceTree = ""; }; B19D62701CB1A27700E16D11 /* upload-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "upload-data.json"; sourceTree = ""; }; B19D62761CB1A42600E16D11 /* upload-file.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "upload-file.json"; sourceTree = ""; }; + C191AEEF1D2F5B32001EB011 /* CassetteOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CassetteOptions.swift; sourceTree = ""; }; + C191AEF01D2F5B32001EB011 /* RequestMatchingOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestMatchingOptions.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -157,6 +163,8 @@ B19D62641CB1860400E16D11 /* SessionUploadTask.swift */, 3647AFB51B335E4A00EF10D4 /* Cassette.swift */, 3647AFB61B335E4A00EF10D4 /* Interaction.swift */, + C191AEEF1D2F5B32001EB011 /* CassetteOptions.swift */, + C191AEF01D2F5B32001EB011 /* RequestMatchingOptions.swift */, 3647AFB71B335E4A00EF10D4 /* URLRequest.swift */, 3647AFBF1B33602A00EF10D4 /* URLResponse.swift */, 3647AFC11B3363C400EF10D4 /* URLHTTPResponse.swift */, @@ -396,8 +404,10 @@ 3647AFBD1B335E4A00EF10D4 /* Session.swift in Sources */, 360F5F731B5C907A001AADD1 /* SessionDownloadTask.swift in Sources */, 3647AFBC1B335E4A00EF10D4 /* URLRequest.swift in Sources */, + C191AEF31D2F5B32001EB011 /* RequestMatchingOptions.swift in Sources */, 3647AFBB1B335E4A00EF10D4 /* Interaction.swift in Sources */, 3647AFBA1B335E4A00EF10D4 /* Cassette.swift in Sources */, + C191AEF11D2F5B32001EB011 /* CassetteOptions.swift in Sources */, 3647AFC21B3363C400EF10D4 /* URLHTTPResponse.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -421,8 +431,10 @@ 3690A0981B33AA9400731222 /* URLResponse.swift in Sources */, 360F5F751B5C907A001AADD1 /* SessionDownloadTask.swift in Sources */, 3690A0931B33AA9400731222 /* Session.swift in Sources */, + C191AEF41D2F5B32001EB011 /* RequestMatchingOptions.swift in Sources */, 3690A0941B33AA9400731222 /* SessionDataTask.swift in Sources */, 3690A0971B33AA9400731222 /* URLRequest.swift in Sources */, + C191AEF21D2F5B32001EB011 /* CassetteOptions.swift in Sources */, 3690A0991B33AA9400731222 /* URLHTTPResponse.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/DVR/Cassette.swift b/DVR/Cassette.swift index 23f5b55..273213b 100644 --- a/DVR/Cassette.swift +++ b/DVR/Cassette.swift @@ -6,32 +6,146 @@ struct Cassette { let name: String let interactions: [Interaction] - + let cassetteOptions: CassetteOptions // MARK: - Initializers - init(name: String, interactions: [Interaction]) { + init(name: String, interactions: [Interaction], cassetteOptions: CassetteOptions) { self.name = name self.interactions = interactions + self.cassetteOptions = cassetteOptions } - // MARK: - Functions func interactionForRequest(request: NSURLRequest) -> Interaction? { for interaction in interactions { let interactionRequest = interaction.request + + if cassetteOptions.requestMatching == [.URL, .Path, .HTTPMethod, .HTTPBody] { + guard + interactionRequest.URL == request.URL && + interactionRequest.URL?.relativePath == request.URL?.relativePath && + interactionRequest.HTTPMethod == request.HTTPMethod && + interactionRequest.hasHTTPBodyEqualToThatOfRequest(request) + else { + continue + } + + return interaction + } + + if cassetteOptions.requestMatching == [.URL, .Path] { + guard + interactionRequest.URL == request.URL && + interactionRequest.URL?.relativePath == request.URL?.relativePath + else { + continue + } + + return interaction + } + + if cassetteOptions.requestMatching == [.URL, .HTTPMethod] { + guard + interactionRequest.URL == request.URL && + interactionRequest.HTTPMethod == request.HTTPMethod + else { + continue + } + + return interaction + } + + if cassetteOptions.requestMatching == [.URL, .HTTPBody] { + guard + interactionRequest.URL == request.URL && + interactionRequest.hasHTTPBodyEqualToThatOfRequest(request) + else { + continue + } + + return interaction + } + + if cassetteOptions.requestMatching == [.Path, .HTTPMethod] { + guard + interactionRequest.URL?.relativePath == request.URL?.relativePath && + interactionRequest.HTTPMethod == request.HTTPMethod + else { + continue + } + + return interaction + } + + if cassetteOptions.requestMatching == [.Path, .HTTPBody] { + guard + interactionRequest.URL?.relativePath == request.URL?.relativePath && + interactionRequest.hasHTTPBodyEqualToThatOfRequest(request) + else { + continue + } + + return interaction + } - // Note: We don't check headers right now - if interactionRequest.HTTPMethod == request.HTTPMethod && interactionRequest.URL == request.URL && interactionRequest.hasHTTPBodyEqualToThatOfRequest(request) { + if cassetteOptions.requestMatching == [.HTTPMethod, .HTTPBody] { + guard + interactionRequest.HTTPMethod == request.HTTPMethod && + interactionRequest.hasHTTPBodyEqualToThatOfRequest(request) + else { + continue + } + + return interaction + } + + if cassetteOptions.requestMatching == [.URL] { + guard + interactionRequest.URL == request.URL + else { + continue + } + + return interaction + } + + if cassetteOptions.requestMatching == [.Path] { + guard + interactionRequest.URL?.relativePath == request.URL?.relativePath + else { + continue + } + + return interaction + } + + if cassetteOptions.requestMatching == [.HTTPMethod] { + guard + interactionRequest.HTTPMethod == request.HTTPMethod + else { + continue + } + + return interaction + } + + if cassetteOptions.requestMatching == [.HTTPBody] { + guard + interactionRequest.hasHTTPBodyEqualToThatOfRequest(request) + else { + continue + } + return interaction } } + return nil } } - extension Cassette { var dictionary: [String: AnyObject] { return [ @@ -40,10 +154,11 @@ extension Cassette { ] } - init?(dictionary: [String: AnyObject]) { + init?(dictionary: [String: AnyObject], cassetteOptions: CassetteOptions) { guard let name = dictionary["name"] as? String else { return nil } self.name = name + self.cassetteOptions = cassetteOptions if let array = dictionary["interactions"] as? [[String: AnyObject]] { interactions = array.flatMap { Interaction(dictionary: $0) } diff --git a/DVR/CassetteOptions.swift b/DVR/CassetteOptions.swift new file mode 100644 index 0000000..ffdf1c6 --- /dev/null +++ b/DVR/CassetteOptions.swift @@ -0,0 +1,20 @@ +// +// CassetteOptions.swift +// DVR +// +// Created by Peter Nicholls on 6/07/2016. +// Copyright © 2016 Venmo. All rights reserved. +// + +public struct CassetteOptions { + + // MARK: - Properties + + public let requestMatching: RequestMatching + + // MARK: - Initializers + + public init(requestMatching: RequestMatching = [.URL, .Path, .HTTPMethod, .HTTPBody]) { + self.requestMatching = requestMatching + } +} diff --git a/DVR/RequestMatchingOptions.swift b/DVR/RequestMatchingOptions.swift new file mode 100644 index 0000000..51a41b2 --- /dev/null +++ b/DVR/RequestMatchingOptions.swift @@ -0,0 +1,33 @@ +// +// RequestMatching.swift +// DVR +// +// Created by Peter Nicholls on 6/07/2016. +// Copyright © 2016 Venmo. All rights reserved. +// + +public struct RequestMatching : OptionSetType { + + // MARK: - Properties + + private enum Method : Int { + case URL = 1, Path = 2, HTTPMethod = 4, HTTPBody = 8 + } + + public let rawValue : Int + + public static let URL = RequestMatching(Method.URL) + public static let Path = RequestMatching(Method.Path) + public static let HTTPMethod = RequestMatching(Method.HTTPMethod) + public static let HTTPBody = RequestMatching(Method.HTTPBody) + + // MARK: - Initializers + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + private init(_ direction: Method) { + self.rawValue = direction.rawValue + } +} \ No newline at end of file diff --git a/DVR/Session.swift b/DVR/Session.swift index a09fa2a..c472bc8 100644 --- a/DVR/Session.swift +++ b/DVR/Session.swift @@ -8,7 +8,8 @@ public class Session: NSURLSession { public let cassetteName: String public let backingSession: NSURLSession public var recordingEnabled = true - + public let cassetteOptions: CassetteOptions + private let testBundle: NSBundle private var recording = false @@ -23,11 +24,13 @@ public class Session: NSURLSession { // MARK: - Initializers - public init(outputDirectory: String = "~/Desktop/DVR/", cassetteName: String, testBundle: NSBundle = NSBundle.allBundles().filter() { $0.bundlePath.hasSuffix(".xctest") }.first!, backingSession: NSURLSession = NSURLSession.sharedSession()) { + public init(outputDirectory: String = "~/Desktop/DVR/", cassetteName: String, testBundle: NSBundle = NSBundle.allBundles().filter() { $0.bundlePath.hasSuffix(".xctest") }.first!, backingSession: NSURLSession = NSURLSession.sharedSession(), cassetteOptions: CassetteOptions = CassetteOptions()) { self.outputDirectory = outputDirectory self.cassetteName = cassetteName self.testBundle = testBundle self.backingSession = backingSession + self.cassetteOptions = cassetteOptions + super.init() } @@ -116,7 +119,7 @@ public class Session: NSURLSession { json = raw as? [String: AnyObject] else { return nil } - return Cassette(dictionary: json) + return Cassette(dictionary: json, cassetteOptions: cassetteOptions) } func finishTask(task: NSURLSessionTask, interaction: Interaction, playback: Bool) { @@ -195,7 +198,7 @@ public class Session: NSURLSession { } } - let cassette = Cassette(name: cassetteName, interactions: interactions) + let cassette = Cassette(name: cassetteName, interactions: interactions, cassetteOptions: cassetteOptions) // Persist diff --git a/DVR/SessionDataTask.swift b/DVR/SessionDataTask.swift index 9e0c32c..4ee78fa 100644 --- a/DVR/SessionDataTask.swift +++ b/DVR/SessionDataTask.swift @@ -37,7 +37,7 @@ class SessionDataTask: NSURLSessionDataTask { override func resume() { let cassette = session.cassette - + // Find interaction if let interaction = session.cassette?.interactionForRequest(request) { self.interaction = interaction @@ -47,12 +47,14 @@ class SessionDataTask: NSURLSessionDataTask { completion(interaction.responseData, interaction.response, nil) } } + session.finishTask(self, interaction: interaction, playback: true) return } - + if cassette != nil { print("[DVR] Invalid request. The request was not found in the cassette.") + print("[DVR] Request: ", request) abort() } diff --git a/DVR/Tests/SessionTests.swift b/DVR/Tests/SessionTests.swift index 3ff4e05..cbe4cb8 100644 --- a/DVR/Tests/SessionTests.swift +++ b/DVR/Tests/SessionTests.swift @@ -133,6 +133,25 @@ class SessionTests: XCTestCase { waitForExpectationsWithTimeout(1, handler: nil) } + + func testDataTaskWithHTTPBodyRequestMatchingCassetteOptions() { + let cassetteOptions = CassetteOptions(requestMatching: [.HTTPBody]) + let session = Session(cassetteName: "example", cassetteOptions: cassetteOptions) + + session.recordingEnabled = false + let expectation = expectationWithDescription("Network") + + session.dataTaskWithRequest(request) { data, response, error in + XCTAssertEqual("hello", String(data: data!, encoding: NSUTF8StringEncoding)) + + let HTTPResponse = response as! NSHTTPURLResponse + XCTAssertEqual(200, HTTPResponse.statusCode) + + expectation.fulfill() + }.resume() + + waitForExpectationsWithTimeout(1, handler: nil) + } func testTaskDelegate() { class Delegate: NSObject, NSURLSessionTaskDelegate {