From e2ed18211525d3d562fd1fb01c02e34cb73ffa88 Mon Sep 17 00:00:00 2001 From: Nate Armstrong Date: Sun, 3 Apr 2016 13:31:21 -0600 Subject: [PATCH 01/11] Add support for upload tasks --- DVR.xcodeproj/project.pbxproj | 28 +++++++ DVR/Session.swift | 26 +++++++ DVR/SessionUploadTask.swift | 32 ++++++++ DVR/Tests/Fixtures/testfile.txt | 1 + DVR/Tests/Fixtures/upload-data.json | 52 +++++++++++++ DVR/Tests/Fixtures/upload-file.json | 52 +++++++++++++ DVR/Tests/SessionUploadTests.swift | 110 ++++++++++++++++++++++++++++ DVR/URLRequest.swift | 6 ++ 8 files changed, 307 insertions(+) create mode 100644 DVR/SessionUploadTask.swift create mode 100644 DVR/Tests/Fixtures/testfile.txt create mode 100644 DVR/Tests/Fixtures/upload-data.json create mode 100644 DVR/Tests/Fixtures/upload-file.json create mode 100644 DVR/Tests/SessionUploadTests.swift diff --git a/DVR.xcodeproj/project.pbxproj b/DVR.xcodeproj/project.pbxproj index 46db810..0a38672 100644 --- a/DVR.xcodeproj/project.pbxproj +++ b/DVR.xcodeproj/project.pbxproj @@ -37,6 +37,15 @@ 3690A0A21B33AA9E00731222 /* SessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3647AFCB1B33689000EF10D4 /* SessionTests.swift */; }; 36BDDB891B6716AB00878665 /* json-example.json in Resources */ = {isa = PBXBuildFile; fileRef = 36BDDB881B6716AB00878665 /* json-example.json */; }; 36BDDB8A1B6716AB00878665 /* json-example.json in Resources */ = {isa = PBXBuildFile; fileRef = 36BDDB881B6716AB00878665 /* json-example.json */; }; + B19D62651CB1860400E16D11 /* SessionUploadTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = B19D62641CB1860400E16D11 /* SessionUploadTask.swift */; }; + B19D62681CB19AA000E16D11 /* testfile.txt in Resources */ = {isa = PBXBuildFile; fileRef = B19D62661CB18EDB00E16D11 /* testfile.txt */; }; + B19D62691CB19AA200E16D11 /* testfile.txt in Resources */ = {isa = PBXBuildFile; fileRef = B19D62661CB18EDB00E16D11 /* testfile.txt */; }; + B19D626E1CB1A0DD00E16D11 /* SessionUploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B19D626D1CB1A0DD00E16D11 /* SessionUploadTests.swift */; }; + B19D626F1CB1A0DD00E16D11 /* SessionUploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B19D626D1CB1A0DD00E16D11 /* SessionUploadTests.swift */; }; + B19D62711CB1A27700E16D11 /* upload-data.json in Resources */ = {isa = PBXBuildFile; fileRef = B19D62701CB1A27700E16D11 /* upload-data.json */; }; + 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 */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -77,6 +86,11 @@ 3690A07B1B33AA3B00731222 /* DVR.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DVR.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3690A0841B33AA3C00731222 /* DVRTests-OSX.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DVRTests-OSX.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 36BDDB881B6716AB00878665 /* json-example.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "json-example.json"; sourceTree = ""; }; + B19D62641CB1860400E16D11 /* SessionUploadTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionUploadTask.swift; sourceTree = ""; }; + B19D62661CB18EDB00E16D11 /* testfile.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = testfile.txt; sourceTree = ""; }; + 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -139,6 +153,7 @@ 3647AFB81B335E4A00EF10D4 /* Session.swift */, 3647AFB91B335E4A00EF10D4 /* SessionDataTask.swift */, 360F5F721B5C907A001AADD1 /* SessionDownloadTask.swift */, + B19D62641CB1860400E16D11 /* SessionUploadTask.swift */, 3647AFB51B335E4A00EF10D4 /* Cassette.swift */, 3647AFB61B335E4A00EF10D4 /* Interaction.swift */, 3647AFB71B335E4A00EF10D4 /* URLRequest.swift */, @@ -164,6 +179,7 @@ 3647AFCB1B33689000EF10D4 /* SessionTests.swift */, 3647AFCF1B33693F00EF10D4 /* Fixtures */, 3647AFCC1B33689000EF10D4 /* Info.plist */, + B19D626D1CB1A0DD00E16D11 /* SessionUploadTests.swift */, ); name = Tests; path = DVR/Tests; @@ -176,6 +192,9 @@ 215F927C1B33D46C00EDC60F /* example.json */, 21E548F11BF69E29004855AE /* text.json */, 2119D37E1BF6BB2300E91D6F /* multiple.json */, + B19D62701CB1A27700E16D11 /* upload-data.json */, + B19D62761CB1A42600E16D11 /* upload-file.json */, + B19D62661CB18EDB00E16D11 /* testfile.txt */, ); path = Fixtures; sourceTree = ""; @@ -332,7 +351,10 @@ files = ( 36BDDB891B6716AB00878665 /* json-example.json in Resources */, 2119D3801BF6BB2700E91D6F /* multiple.json in Resources */, + B19D62711CB1A27700E16D11 /* upload-data.json in Resources */, + B19D62771CB1A42600E16D11 /* upload-file.json in Resources */, 215F927D1B33D46C00EDC60F /* example.json in Resources */, + B19D62681CB19AA000E16D11 /* testfile.txt in Resources */, 21E548F31BF69E30004855AE /* text.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -350,7 +372,10 @@ files = ( 36BDDB8A1B6716AB00878665 /* json-example.json in Resources */, 2119D3811BF6BB2700E91D6F /* multiple.json in Resources */, + B19D62721CB1A27700E16D11 /* upload-data.json in Resources */, + B19D62781CB1A42600E16D11 /* upload-file.json in Resources */, 215F927E1B33D46C00EDC60F /* example.json in Resources */, + B19D62691CB19AA200E16D11 /* testfile.txt in Resources */, 21E548F41BF69E30004855AE /* text.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -363,6 +388,7 @@ buildActionMask = 2147483647; files = ( 3647AFC01B33602A00EF10D4 /* URLResponse.swift in Sources */, + B19D62651CB1860400E16D11 /* SessionUploadTask.swift in Sources */, 3647AFBE1B335E4A00EF10D4 /* SessionDataTask.swift in Sources */, 3647AFBD1B335E4A00EF10D4 /* Session.swift in Sources */, 360F5F731B5C907A001AADD1 /* SessionDownloadTask.swift in Sources */, @@ -378,6 +404,7 @@ buildActionMask = 2147483647; files = ( 3647AFCD1B33689000EF10D4 /* SessionTests.swift in Sources */, + B19D626E1CB1A0DD00E16D11 /* SessionUploadTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -401,6 +428,7 @@ buildActionMask = 2147483647; files = ( 3690A0A21B33AA9E00731222 /* SessionTests.swift in Sources */, + B19D626F1CB1A0DD00E16D11 /* SessionUploadTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DVR/Session.swift b/DVR/Session.swift index 3ccba99..a09fa2a 100644 --- a/DVR/Session.swift +++ b/DVR/Session.swift @@ -50,6 +50,24 @@ public class Session: NSURLSession { return addDownloadTask(request, completionHandler: completionHandler) } + public override func uploadTaskWithRequest(request: NSURLRequest, fromData bodyData: NSData) -> NSURLSessionUploadTask { + return addUploadTask(request, fromData: bodyData) + } + + public override func uploadTaskWithRequest(request: NSURLRequest, fromData bodyData: NSData?, completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionUploadTask { + return addUploadTask(request, fromData: bodyData, completionHandler: completionHandler) + } + + public override func uploadTaskWithRequest(request: NSURLRequest, fromFile fileURL: NSURL) -> NSURLSessionUploadTask { + let data = NSData(contentsOfURL: fileURL)! + return addUploadTask(request, fromData: data) + } + + public override func uploadTaskWithRequest(request: NSURLRequest, fromFile fileURL: NSURL, completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionUploadTask { + let data = NSData(contentsOfURL: fileURL)! + return addUploadTask(request, fromData: data, completionHandler: completionHandler) + } + public override func invalidateAndCancel() { recording = false outstandingTasks.removeAll() @@ -140,6 +158,14 @@ public class Session: NSURLSession { return task } + private func addUploadTask(request: NSURLRequest, fromData data: NSData?, completionHandler: SessionUploadTask.Completion? = nil) -> NSURLSessionUploadTask { + var modifiedRequest = backingSession.configuration.HTTPAdditionalHeaders.map(request.requestByAppendingHeaders) ?? request + modifiedRequest = data.map(modifiedRequest.requestWithBody) ?? modifiedRequest + let task = SessionUploadTask(session: self, request: modifiedRequest, completion: completionHandler) + addTask(task.dataTask) + return task + } + private func addTask(task: NSURLSessionTask) { let shouldRecord = !recording if shouldRecord { diff --git a/DVR/SessionUploadTask.swift b/DVR/SessionUploadTask.swift new file mode 100644 index 0000000..b6b3b78 --- /dev/null +++ b/DVR/SessionUploadTask.swift @@ -0,0 +1,32 @@ +class SessionUploadTask: NSURLSessionUploadTask { + + // MARK: - Types + + typealias Completion = (NSData?, NSURLResponse?, NSError?) -> Void + + // MARK: - Properties + + weak var session: Session! + let request: NSURLRequest + let completion: Completion? + let dataTask: SessionDataTask + + // MARK: - Initializers + + init(session: Session, request: NSURLRequest, completion: Completion? = nil) { + self.session = session + self.request = request + self.completion = completion + dataTask = SessionDataTask(session: session, request: request, completion: completion) + } + + // MARK: - NSURLSessionTask + + override func cancel() { + // Don't do anything + } + + override func resume() { + dataTask.resume() + } +} diff --git a/DVR/Tests/Fixtures/testfile.txt b/DVR/Tests/Fixtures/testfile.txt new file mode 100644 index 0000000..16b14f5 --- /dev/null +++ b/DVR/Tests/Fixtures/testfile.txt @@ -0,0 +1 @@ +test file diff --git a/DVR/Tests/Fixtures/upload-data.json b/DVR/Tests/Fixtures/upload-data.json new file mode 100644 index 0000000..87cf312 --- /dev/null +++ b/DVR/Tests/Fixtures/upload-data.json @@ -0,0 +1,52 @@ +{ + "interactions" : [ + { + "recorded_at" : 1459710669.692243, + "response" : { + "body" : { + "json" : null, + "origin" : "104.153.125.238", + "url" : "https:\/\/httpbin.org\/post", + "data" : "", + "headers" : { + "Accept" : "*\/*", + "Accept-Encoding" : "gzip, deflate", + "Content-Type" : "multipart\/form-data; boundary=<2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d33 6b6c6665 6e616c6b 736a666c 6b6a6f69 39617566 38396573 68616a73 6e6c336b 6a6e7761 6c>", + "Host" : "httpbin.org", + "Content-Length" : "365", + "User-Agent" : "xctest (unknown version) CFNetwork\/758.3.15 Darwin\/15.3.0", + "Accept-Language" : "en-us" + }, + "args" : { + + }, + "files" : { + + }, + "form" : { + "file" : "test file\n" + } + }, + "status" : 200, + "url" : "https:\/\/httpbin.org\/post", + "headers" : { + "access-control-allow-credentials" : "true", + "Date" : "Sun, 03 Apr 2016 19:10:15 GMT", + "Content-Type" : "application\/json", + "Content-Length" : "635", + "Server" : "nginx", + "Access-Control-Allow-Origin" : "*" + } + }, + "request" : { + "method" : "POST", + "body" : "LS08MmQyZDJkMmQgMmQyZDJkMmQgMmQyZDJkMmQgMmQyZDJkMmQgMmQyZDJkMmQgMmQyZDJkMmQgMmQyZDJkMzMgNmI2YzY2NjUgNmU2MTZjNmIgNzM2YTY2NmMgNmI2YTZmNjkgMzk2MTc1NjYgMzgzOTY1NzMgNjg2MTZhNzMgNmU2YzMzNmIgNmE2ZTc3NjEgNmM+DQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImZpbGUiDQoNCnRlc3QgZmlsZQoNCi0tPDJkMmQyZDJkIDJkMmQyZDJkIDJkMmQyZDJkIDJkMmQyZDJkIDJkMmQyZDJkIDJkMmQyZDJkIDJkMmQyZDMzIDZiNmM2NjY1IDZlNjE2YzZiIDczNmE2NjZjIDZiNmE2ZjY5IDM5NjE3NTY2IDM4Mzk2NTczIDY4NjE2YTczIDZlNmMzMzZiIDZhNmU3NzYxIDZjPi0tDQo=", + "url" : "https:\/\/httpbin.org\/post", + "headers" : { + "Content-Type" : "multipart\/form-data; boundary=<2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d33 6b6c6665 6e616c6b 736a666c 6b6a6f69 39617566 38396573 68616a73 6e6c336b 6a6e7761 6c>" + } + } + } + ], + "name" : "upload-data" +} diff --git a/DVR/Tests/Fixtures/upload-file.json b/DVR/Tests/Fixtures/upload-file.json new file mode 100644 index 0000000..ea4a0a3 --- /dev/null +++ b/DVR/Tests/Fixtures/upload-file.json @@ -0,0 +1,52 @@ +{ + "interactions" : [ + { + "recorded_at" : 1459711091.300466, + "response" : { + "body" : { + "json" : null, + "origin" : "104.153.125.238", + "url" : "https:\/\/httpbin.org\/post", + "data" : "", + "headers" : { + "Accept" : "*\/*", + "Accept-Encoding" : "gzip, deflate", + "Content-Type" : "multipart\/form-data; boundary=<2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d33 6b6c6665 6e616c6b 736a666c 6b6a6f69 39617566 38396573 68616a73 6e6c336b 6a6e7761 6c>", + "Host" : "httpbin.org", + "Content-Length" : "365", + "User-Agent" : "xctest (unknown version) CFNetwork\/758.3.15 Darwin\/15.3.0", + "Accept-Language" : "en-us" + }, + "args" : { + + }, + "files" : { + + }, + "form" : { + "file" : "test file\n" + } + }, + "status" : 200, + "url" : "https:\/\/httpbin.org\/post", + "headers" : { + "access-control-allow-credentials" : "true", + "Date" : "Sun, 03 Apr 2016 19:17:16 GMT", + "Content-Type" : "application\/json", + "Content-Length" : "635", + "Server" : "nginx", + "Access-Control-Allow-Origin" : "*" + } + }, + "request" : { + "method" : "POST", + "body" : "LS08MmQyZDJkMmQgMmQyZDJkMmQgMmQyZDJkMmQgMmQyZDJkMmQgMmQyZDJkMmQgMmQyZDJkMmQgMmQyZDJkMzMgNmI2YzY2NjUgNmU2MTZjNmIgNzM2YTY2NmMgNmI2YTZmNjkgMzk2MTc1NjYgMzgzOTY1NzMgNjg2MTZhNzMgNmU2YzMzNmIgNmE2ZTc3NjEgNmM+DQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImZpbGUiDQoNCnRlc3QgZmlsZQoNCi0tPDJkMmQyZDJkIDJkMmQyZDJkIDJkMmQyZDJkIDJkMmQyZDJkIDJkMmQyZDJkIDJkMmQyZDJkIDJkMmQyZDMzIDZiNmM2NjY1IDZlNjE2YzZiIDczNmE2NjZjIDZiNmE2ZjY5IDM5NjE3NTY2IDM4Mzk2NTczIDY4NjE2YTczIDZlNmMzMzZiIDZhNmU3NzYxIDZjPi0tDQo=", + "url" : "https:\/\/httpbin.org\/post", + "headers" : { + "Content-Type" : "multipart\/form-data; boundary=<2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d2d 2d2d2d33 6b6c6665 6e616c6b 736a666c 6b6a6f69 39617566 38396573 68616a73 6e6c336b 6a6e7761 6c>" + } + } + } + ], + "name" : "upload-file" +} diff --git a/DVR/Tests/SessionUploadTests.swift b/DVR/Tests/SessionUploadTests.swift new file mode 100644 index 0000000..f3009e8 --- /dev/null +++ b/DVR/Tests/SessionUploadTests.swift @@ -0,0 +1,110 @@ +import XCTest +@testable import DVR + +class SessionUploadTests: XCTestCase { + + lazy var request: NSURLRequest = { + let request = NSMutableURLRequest(URL: NSURL(string: "https://httpbin.org/post")!) + request.HTTPMethod = "POST" + + let contentType = "multipart/form-data; boundary=\(self.multipartBoundary)" + request.addValue(contentType, forHTTPHeaderField: "Content-Type") + return request + }() + let multipartBoundary = "---------------------------3klfenalksjflkjoi9auf89eshajsnl3kjnwal".UTF8Data() + lazy var testFile: NSURL = { + return NSBundle(forClass: self.dynamicType).URLForResource("testfile", withExtension: "txt")! + }() + + func testUploadFile() { + let session = Session(cassetteName: "upload-file") + session.recordingEnabled = false + let expectation = expectationWithDescription("Network") + + let data = encodeMultipartBody(NSData(contentsOfURL: testFile)!, parameters: [:]) + let file = writeDataToFile(data, fileName: "upload-file") + + session.uploadTaskWithRequest(request, fromFile: file) { data, response, error in + do { + let JSON = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as? [String: AnyObject] + XCTAssertEqual("test file\n", (JSON?["form"] as? [String: AnyObject])?["file"] as? String) + } catch { + XCTFail("Failed to read JSON.") + } + + let HTTPResponse = response as! NSHTTPURLResponse + XCTAssertEqual(200, HTTPResponse.statusCode) + + expectation.fulfill() + }.resume() + + waitForExpectationsWithTimeout(4, handler: nil) + } + + func testUploadData() { + let session = Session(cassetteName: "upload-data") + session.recordingEnabled = false + let expectation = expectationWithDescription("Network") + + let data = encodeMultipartBody(NSData(contentsOfURL: testFile)!, parameters: [:]) + + session.uploadTaskWithRequest(request, fromData: data) { data, response, error in + do { + let JSON = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as? [String: AnyObject] + XCTAssertEqual("test file\n", (JSON?["form"] as? [String: AnyObject])?["file"] as? String) + } catch { + XCTFail("Failed to read JSON.") + } + + let HTTPResponse = response as! NSHTTPURLResponse + XCTAssertEqual(200, HTTPResponse.statusCode) + + expectation.fulfill() + }.resume() + + waitForExpectationsWithTimeout(4, handler: nil) + } + + // MARK: Helpers + + func encodeMultipartBody(data: NSData, parameters: [String: AnyObject]) -> NSData { + let delim = "--\(multipartBoundary)\r\n".UTF8Data() + + let body = NSMutableData() + body += delim + for (key, value) in parameters { + body += "Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n\(value)\r\n".UTF8Data() + body += delim + } + + body += "Content-Disposition: form-data; name=\"file\"\r\n\r\n".UTF8Data() + body += data + body += "\r\n--\(multipartBoundary)--\r\n".UTF8Data() + + return body + } + + func writeDataToFile(data: NSData, fileName: String) -> NSURL { + let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] + let documentsURL = NSURL(fileURLWithPath: documentsPath, isDirectory: true) + + let url = documentsURL.URLByAppendingPathComponent(fileName + ".tmp") + + data.writeToURL(url, atomically: true) + return url + } + +} + +// MARK: - Helpers + +extension String { + func UTF8Data() -> NSData { + return dataUsingEncoding(NSUTF8StringEncoding)! + } +} + + +public func +=(lhs: NSMutableData, rhs: NSData) { + lhs.appendData(rhs) +} diff --git a/DVR/URLRequest.swift b/DVR/URLRequest.swift index 467af9a..aa84f7d 100644 --- a/DVR/URLRequest.swift +++ b/DVR/URLRequest.swift @@ -31,6 +31,12 @@ extension NSURLRequest { request.appendHeaders(headers) return request.copy() as! NSURLRequest } + + func requestWithBody(body: NSData) -> NSURLRequest { + let request = mutableCopy() as! NSMutableURLRequest + request.HTTPBody = body + return request.copy() as! NSURLRequest + } } From 103d17ae57c952969362e1c671383202e3f7f691 Mon Sep 17 00:00:00 2001 From: Nate Armstrong Date: Sun, 3 Apr 2016 19:19:13 -0600 Subject: [PATCH 02/11] Fix task response when recording and using delegate --- DVR/SessionDataTask.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DVR/SessionDataTask.swift b/DVR/SessionDataTask.swift index 2bf1ca2..9e0c32c 100644 --- a/DVR/SessionDataTask.swift +++ b/DVR/SessionDataTask.swift @@ -81,8 +81,8 @@ class SessionDataTask: NSURLSessionDataTask { } // Create interaction - let interaction = Interaction(request: this.request, response: response, responseData: data) - this.session.finishTask(this, interaction: interaction, playback: false) + this.interaction = Interaction(request: this.request, response: response, responseData: data) + this.session.finishTask(this, interaction: this.interaction!, playback: false) } task.resume() } From 5466e0cfefff2e32d989fec468c3c434aef4fbb6 Mon Sep 17 00:00:00 2001 From: Nate Armstrong Date: Sun, 3 Apr 2016 19:46:08 -0600 Subject: [PATCH 03/11] Notify delegate before finishRecording --- DVR/Session.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DVR/Session.swift b/DVR/Session.swift index a09fa2a..0dde342 100644 --- a/DVR/Session.swift +++ b/DVR/Session.swift @@ -128,10 +128,6 @@ public class Session: NSURLSession { completedInteractions.append(interaction) - if !recording && outstandingTasks.count == 0 { - finishRecording() - } - if let delegate = delegate as? NSURLSessionDataDelegate, task = task as? NSURLSessionDataTask, data = interaction.responseData { delegate.URLSession?(self, dataTask: task, didReceiveData: data) } @@ -139,6 +135,10 @@ public class Session: NSURLSession { if let delegate = delegate as? NSURLSessionTaskDelegate { delegate.URLSession?(self, task: task, didCompleteWithError: nil) } + + if !recording && outstandingTasks.count == 0 { + finishRecording() + } } From 043b1294759feb2db783685b09711be15e08afab Mon Sep 17 00:00:00 2001 From: Nate Armstrong Date: Sun, 3 Apr 2016 22:09:45 -0600 Subject: [PATCH 04/11] Finish task with upload task instead of data task Returning the upload and then calling complete with the data task can make it difficult for consumer delegates to keep things straight. This is a bit of a hacky solution but it works for now. --- DVR/Session.swift | 2 +- DVR/SessionDataTask.swift | 8 +++++--- DVR/SessionUploadTask.swift | 5 +++-- DVR/Tests/SessionUploadTests.swift | 31 ++++++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/DVR/Session.swift b/DVR/Session.swift index 0dde342..5a594eb 100644 --- a/DVR/Session.swift +++ b/DVR/Session.swift @@ -162,7 +162,7 @@ public class Session: NSURLSession { var modifiedRequest = backingSession.configuration.HTTPAdditionalHeaders.map(request.requestByAppendingHeaders) ?? request modifiedRequest = data.map(modifiedRequest.requestWithBody) ?? modifiedRequest let task = SessionUploadTask(session: self, request: modifiedRequest, completion: completionHandler) - addTask(task.dataTask) + addTask(task) return task } diff --git a/DVR/SessionDataTask.swift b/DVR/SessionDataTask.swift index 9e0c32c..206a87e 100644 --- a/DVR/SessionDataTask.swift +++ b/DVR/SessionDataTask.swift @@ -14,6 +14,7 @@ class SessionDataTask: NSURLSessionDataTask { let completion: Completion? private let queue = dispatch_queue_create("com.venmo.DVR.sessionDataTaskQueue", nil) private var interaction: Interaction? + private var backingTask: NSURLSessionTask? override var response: NSURLResponse? { return interaction?.response @@ -22,9 +23,10 @@ class SessionDataTask: NSURLSessionDataTask { // MARK: - Initializers - init(session: Session, request: NSURLRequest, completion: (Completion)? = nil) { + init(session: Session, request: NSURLRequest, backingTask: NSURLSessionTask? = nil, completion: (Completion)? = nil) { self.session = session self.request = request + self.backingTask = backingTask self.completion = completion } @@ -47,7 +49,7 @@ class SessionDataTask: NSURLSessionDataTask { completion(interaction.responseData, interaction.response, nil) } } - session.finishTask(self, interaction: interaction, playback: true) + session.finishTask(self.backingTask ?? self, interaction: interaction, playback: true) return } @@ -82,7 +84,7 @@ class SessionDataTask: NSURLSessionDataTask { // Create interaction this.interaction = Interaction(request: this.request, response: response, responseData: data) - this.session.finishTask(this, interaction: this.interaction!, playback: false) + this.session.finishTask(this.backingTask ?? this, interaction: this.interaction!, playback: false) } task.resume() } diff --git a/DVR/SessionUploadTask.swift b/DVR/SessionUploadTask.swift index b6b3b78..bba232b 100644 --- a/DVR/SessionUploadTask.swift +++ b/DVR/SessionUploadTask.swift @@ -9,7 +9,7 @@ class SessionUploadTask: NSURLSessionUploadTask { weak var session: Session! let request: NSURLRequest let completion: Completion? - let dataTask: SessionDataTask + var dataTask: SessionDataTask! // MARK: - Initializers @@ -17,7 +17,8 @@ class SessionUploadTask: NSURLSessionUploadTask { self.session = session self.request = request self.completion = completion - dataTask = SessionDataTask(session: session, request: request, completion: completion) + super.init() + dataTask = SessionDataTask(session: session, request: request, backingTask: self, completion: completion) } // MARK: - NSURLSessionTask diff --git a/DVR/Tests/SessionUploadTests.swift b/DVR/Tests/SessionUploadTests.swift index f3009e8..b0285d4 100644 --- a/DVR/Tests/SessionUploadTests.swift +++ b/DVR/Tests/SessionUploadTests.swift @@ -65,6 +65,37 @@ class SessionUploadTests: XCTestCase { waitForExpectationsWithTimeout(4, handler: nil) } + func testUploadDelegate() { + class Delegate: NSObject, NSURLSessionDataDelegate { + var task: NSURLSessionTask? + let expectation: XCTestExpectation + + init(expectation: XCTestExpectation) { + self.expectation = expectation + } + + @objc func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) { + task = dataTask + expectation.fulfill() + } + } + + let expectation = expectationWithDescription("didCompleteWithError") + let delegate = Delegate(expectation: expectation) + let config = NSURLSessionConfiguration.defaultSessionConfiguration() + let backingSession = NSURLSession(configuration: config, delegate: delegate, delegateQueue: nil) + let session = Session(cassetteName: "upload-data", backingSession: backingSession) + session.recordingEnabled = false + + let data = encodeMultipartBody(NSData(contentsOfURL: testFile)!, parameters: [:]) + + let task = session.uploadTaskWithRequest(request, fromData: data) + task.resume() + + waitForExpectationsWithTimeout(1, handler: nil) + XCTAssertEqual(task, delegate.task) + } + // MARK: Helpers func encodeMultipartBody(data: NSData, parameters: [String: AnyObject]) -> NSData { From 03cc85924f5e77db9ab53f3fdfdf7caea2e64123 Mon Sep 17 00:00:00 2001 From: Nate Armstrong Date: Sun, 3 Apr 2016 22:13:48 -0600 Subject: [PATCH 05/11] Override upload task response --- DVR/SessionDataTask.swift | 2 +- DVR/SessionUploadTask.swift | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/DVR/SessionDataTask.swift b/DVR/SessionDataTask.swift index 206a87e..da35ea1 100644 --- a/DVR/SessionDataTask.swift +++ b/DVR/SessionDataTask.swift @@ -13,7 +13,7 @@ class SessionDataTask: NSURLSessionDataTask { let request: NSURLRequest let completion: Completion? private let queue = dispatch_queue_create("com.venmo.DVR.sessionDataTaskQueue", nil) - private var interaction: Interaction? + internal var interaction: Interaction? private var backingTask: NSURLSessionTask? override var response: NSURLResponse? { diff --git a/DVR/SessionUploadTask.swift b/DVR/SessionUploadTask.swift index bba232b..cef0105 100644 --- a/DVR/SessionUploadTask.swift +++ b/DVR/SessionUploadTask.swift @@ -11,6 +11,10 @@ class SessionUploadTask: NSURLSessionUploadTask { let completion: Completion? var dataTask: SessionDataTask! + override var response: NSURLResponse? { + return dataTask.interaction?.response + } + // MARK: - Initializers init(session: Session, request: NSURLRequest, completion: Completion? = nil) { From 8f51820ccffc5568757b06177a96a80fbab52cf3 Mon Sep 17 00:00:00 2001 From: Nate Armstrong Date: Sun, 3 Apr 2016 22:44:58 -0600 Subject: [PATCH 06/11] Notify task delegate of bytes sent --- DVR/Session.swift | 2 ++ DVR/Tests/SessionTests.swift | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/DVR/Session.swift b/DVR/Session.swift index 5a594eb..704bbe0 100644 --- a/DVR/Session.swift +++ b/DVR/Session.swift @@ -133,6 +133,8 @@ public class Session: NSURLSession { } if let delegate = delegate as? NSURLSessionTaskDelegate { + let bytes = Int64(interaction.responseData?.length ?? 0) + delegate.URLSession?(self, task: task, didSendBodyData: bytes, totalBytesSent: bytes, totalBytesExpectedToSend: bytes) delegate.URLSession?(self, task: task, didCompleteWithError: nil) } diff --git a/DVR/Tests/SessionTests.swift b/DVR/Tests/SessionTests.swift index 3ff4e05..a613589 100644 --- a/DVR/Tests/SessionTests.swift +++ b/DVR/Tests/SessionTests.swift @@ -138,6 +138,7 @@ class SessionTests: XCTestCase { class Delegate: NSObject, NSURLSessionTaskDelegate { let expectation: XCTestExpectation var response: NSURLResponse? + var bytes: Int64? init(expectation: XCTestExpectation) { self.expectation = expectation @@ -147,6 +148,10 @@ class SessionTests: XCTestCase { response = task.response expectation.fulfill() } + + @objc private func URLSession(session: NSURLSession, task: NSURLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + bytes = bytesSent + } } let expectation = expectationWithDescription("didCompleteWithError") @@ -160,6 +165,7 @@ class SessionTests: XCTestCase { task.resume() waitForExpectationsWithTimeout(1, handler: nil) + XCTAssertNotNil(delegate.bytes) } func testDataDelegate() { From 76086aeeebcb05df2f42a622ab1af800d12ebb23 Mon Sep 17 00:00:00 2001 From: Nate Armstrong Date: Fri, 8 Apr 2016 13:32:47 -0600 Subject: [PATCH 07/11] Support task descriptions --- DVR/SessionDataTask.swift | 10 ++++++++++ DVR/Tests/SessionTests.swift | 1 + 2 files changed, 11 insertions(+) diff --git a/DVR/SessionDataTask.swift b/DVR/SessionDataTask.swift index da35ea1..b836f80 100644 --- a/DVR/SessionDataTask.swift +++ b/DVR/SessionDataTask.swift @@ -16,6 +16,16 @@ class SessionDataTask: NSURLSessionDataTask { internal var interaction: Interaction? private var backingTask: NSURLSessionTask? + private var _taskDescription: String? + override var taskDescription: String? { + get { + return _taskDescription + } + set { + _taskDescription = newValue + } + } + override var response: NSURLResponse? { return interaction?.response } diff --git a/DVR/Tests/SessionTests.swift b/DVR/Tests/SessionTests.swift index a613589..c27c24a 100644 --- a/DVR/Tests/SessionTests.swift +++ b/DVR/Tests/SessionTests.swift @@ -18,6 +18,7 @@ class SessionTests: XCTestCase { func testDataTask() { let request = NSURLRequest(URL: NSURL(string: "http://example.com")!) let dataTask = session.dataTaskWithRequest(request) + dataTask.taskDescription = "description" XCTAssert(dataTask is SessionDataTask) From 1f6907049dbca804159077a93cda662fdd6e4888 Mon Sep 17 00:00:00 2001 From: Nate Armstrong Date: Tue, 12 Apr 2016 16:37:11 -0600 Subject: [PATCH 08/11] Support task identifiers --- DVR/SessionDataTask.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DVR/SessionDataTask.swift b/DVR/SessionDataTask.swift index b836f80..743d9cc 100644 --- a/DVR/SessionDataTask.swift +++ b/DVR/SessionDataTask.swift @@ -26,6 +26,10 @@ class SessionDataTask: NSURLSessionDataTask { } } + override var taskIdentifier: Int { + return backingTask?.taskIdentifier ?? 0 + } + override var response: NSURLResponse? { return interaction?.response } From c4581273f89b28129ae7e9d8c35d8ea8316e11ac Mon Sep 17 00:00:00 2001 From: Nate Armstrong Date: Tue, 12 Apr 2016 16:39:16 -0600 Subject: [PATCH 09/11] Support upload task identifiers --- DVR/SessionUploadTask.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DVR/SessionUploadTask.swift b/DVR/SessionUploadTask.swift index cef0105..8d91883 100644 --- a/DVR/SessionUploadTask.swift +++ b/DVR/SessionUploadTask.swift @@ -15,6 +15,10 @@ class SessionUploadTask: NSURLSessionUploadTask { return dataTask.interaction?.response } + override var taskIdentifier: Int { + return dataTask.taskIdentifier + } + // MARK: - Initializers init(session: Session, request: NSURLRequest, completion: Completion? = nil) { From 92a70ecf5821f3ecc4c911d9b6ea205c1e812515 Mon Sep 17 00:00:00 2001 From: Nate Armstrong Date: Tue, 12 Apr 2016 16:46:13 -0600 Subject: [PATCH 10/11] Fix data task identifiers --- DVR/SessionDataTask.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DVR/SessionDataTask.swift b/DVR/SessionDataTask.swift index 743d9cc..891f56e 100644 --- a/DVR/SessionDataTask.swift +++ b/DVR/SessionDataTask.swift @@ -26,8 +26,9 @@ class SessionDataTask: NSURLSessionDataTask { } } + private var _taskIdentifier: Int? override var taskIdentifier: Int { - return backingTask?.taskIdentifier ?? 0 + return _taskIdentifier ?? 0 } override var response: NSURLResponse? { @@ -100,6 +101,8 @@ class SessionDataTask: NSURLSessionDataTask { this.interaction = Interaction(request: this.request, response: response, responseData: data) this.session.finishTask(this.backingTask ?? this, interaction: this.interaction!, playback: false) } + + _taskIdentifier = task.taskIdentifier task.resume() } } From 8c067131da8c5ebad7c8bc875b0c059092329f31 Mon Sep 17 00:00:00 2001 From: Nate Armstrong Date: Tue, 17 May 2016 15:14:15 -0600 Subject: [PATCH 11/11] Remove forced unwrap --- DVR/SessionDataTask.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DVR/SessionDataTask.swift b/DVR/SessionDataTask.swift index 891f56e..322228d 100644 --- a/DVR/SessionDataTask.swift +++ b/DVR/SessionDataTask.swift @@ -98,8 +98,9 @@ class SessionDataTask: NSURLSessionDataTask { } // Create interaction - this.interaction = Interaction(request: this.request, response: response, responseData: data) - this.session.finishTask(this.backingTask ?? this, interaction: this.interaction!, playback: false) + let interaction = Interaction(request: this.request, response: response, responseData: data) + this.interaction = interaction + this.session.finishTask(this.backingTask ?? this, interaction: interaction, playback: false) } _taskIdentifier = task.taskIdentifier