diff --git a/Mockingjay.xcodeproj/project.pbxproj b/Mockingjay.xcodeproj/project.pbxproj index 7d9bf9a..148d7e3 100644 --- a/Mockingjay.xcodeproj/project.pbxproj +++ b/Mockingjay.xcodeproj/project.pbxproj @@ -23,6 +23,8 @@ 27703A631CE2560600194732 /* MockingjayURLSessionConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 27703A621CE2560600194732 /* MockingjayURLSessionConfiguration.m */; }; 444EA6091C5261DE000C3A9F /* MockingjayAsyncProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 444EA6081C5261DE000C3A9F /* MockingjayAsyncProtocolTests.swift */; }; 444EA60C1C52666D000C3A9F /* TestAudio.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 444EA60B1C52666D000C3A9F /* TestAudio.m4a */; }; + 9B2889531DF4DCF5005036B5 /* HttpBodyHack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2889521DF4DCF5005036B5 /* HttpBodyHack.swift */; }; + 9B2889591DF4E4DF005036B5 /* HttpBodyHackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2889581DF4E4DF005036B5 /* HttpBodyHackTests.swift */; }; A1E3C5701AA4EA130069C998 /* XCTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2743679B1AA28D4D0030C97B /* XCTest.swift */; }; /* End PBXBuildFile section */ @@ -88,6 +90,8 @@ 27703A621CE2560600194732 /* MockingjayURLSessionConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MockingjayURLSessionConfiguration.m; sourceTree = ""; }; 444EA6081C5261DE000C3A9F /* MockingjayAsyncProtocolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockingjayAsyncProtocolTests.swift; sourceTree = ""; }; 444EA60B1C52666D000C3A9F /* TestAudio.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestAudio.m4a; sourceTree = ""; }; + 9B2889521DF4DCF5005036B5 /* HttpBodyHack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpBodyHack.swift; sourceTree = ""; }; + 9B2889581DF4E4DF005036B5 /* HttpBodyHackTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpBodyHackTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -153,6 +157,7 @@ 274367931AA27AAD0030C97B /* MockingjayProtocol.swift */, 2743679B1AA28D4D0030C97B /* XCTest.swift */, 274367C51AA35FD00030C97B /* NSURLSessionConfiguration.swift */, + 9B2889521DF4DCF5005036B5 /* HttpBodyHack.swift */, 27703A621CE2560600194732 /* MockingjayURLSessionConfiguration.m */, 2746CDC21A702F7800719B66 /* Supporting Files */, ); @@ -172,6 +177,7 @@ children = ( 2746CDD11A702F7800719B66 /* MockingjayTests.swift */, 274367951AA27B170030C97B /* MockingjayProtocolTests.swift */, + 9B2889581DF4E4DF005036B5 /* HttpBodyHackTests.swift */, 444EA6081C5261DE000C3A9F /* MockingjayAsyncProtocolTests.swift */, 274367991AA28B0D0030C97B /* MatcherTests.swift */, 274367B11AA29E620030C97B /* BuildersTests.swift */, @@ -357,6 +363,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9B2889531DF4DCF5005036B5 /* HttpBodyHack.swift in Sources */, 274367981AA28AFC0030C97B /* Matchers.swift in Sources */, 27703A631CE2560600194732 /* MockingjayURLSessionConfiguration.m in Sources */, 274367921AA27A7C0030C97B /* Mockingjay.swift in Sources */, @@ -373,6 +380,7 @@ files = ( 2743679A1AA28B0D0030C97B /* MatcherTests.swift in Sources */, 274367961AA27B170030C97B /* MockingjayProtocolTests.swift in Sources */, + 9B2889591DF4E4DF005036B5 /* HttpBodyHackTests.swift in Sources */, 444EA6091C5261DE000C3A9F /* MockingjayAsyncProtocolTests.swift in Sources */, 274367B21AA29E620030C97B /* BuildersTests.swift in Sources */, 2705946B1C4FA7A6002A3AA9 /* MockingjayXCTestTests.swift in Sources */, diff --git a/Mockingjay/HttpBodyHack.swift b/Mockingjay/HttpBodyHack.swift new file mode 100644 index 0000000..cd407da --- /dev/null +++ b/Mockingjay/HttpBodyHack.swift @@ -0,0 +1,120 @@ +// +// HttpBodyHack.swift +// Mockingjay +// +// Created by Alexey Kozhevnikov on 05/12/2016. +// Copyright © 2016 Cocode. All rights reserved. +// + +import Foundation + +/* + * Use this hack to retrieve HTTP body from inside Mockingjay matcher. Now HTTP body is always nil due to Apple bug. + * See https://github.com/kylef/Mockingjay/issues/32 + * + * Usage: + * + * func test() { + * let hack = HttpBodyHack() + * + * let matcher: Matcher = { request in + * let data = hack.body(request) + * // use data + * } + * + * // make request + * + * waitForExpectationsWithTimeout(1) { _ in } + * } + */ + +private let httpBodyHackHeaderName = "HTTPBodyHack" + +private let httpBodyHackLock = NSLock() +private var httpBodyHackValues = [String: Data]() + +extension URLRequest { + fileprivate func httpBodyHack() -> Data? { + if let key = allHTTPHeaderFields?[httpBodyHackHeaderName] { + httpBodyHackLock.lock() + defer { + httpBodyHackLock.unlock() + } + return httpBodyHackValues[key] + } else { + return nil + } + } +} + +extension NSMutableURLRequest { + fileprivate class func httpBodyHackSwizzle() { + let setHttpBody = class_getInstanceMethod(self, #selector(setter: NSMutableURLRequest.httpBody)) + let httpBodyHackSetHttpBody = class_getInstanceMethod(self, #selector(NSMutableURLRequest.httpBodyHackSetHttpBody(_:))) + method_exchangeImplementations(setHttpBody, httpBodyHackSetHttpBody) + } + + func httpBodyHackSetHttpBody(_ body: Data?) { + // Don't allow stripping of request + if body == nil { + return + } + var headers = allHTTPHeaderFields ?? [:] + let key = UUID().uuidString + headers[httpBodyHackHeaderName] = key + allHTTPHeaderFields = headers + + httpBodyHackLock.lock() + defer { + httpBodyHackLock.unlock() + } + httpBodyHackValues[key] = body + } +} + +public class HttpBodyHack { + private static var instanceCount = 0 + private static let lock = NSLock() + + public init() { + HttpBodyHack.lock.lock() + defer { + HttpBodyHack.lock.unlock() + } + if HttpBodyHack.instanceCount == 0 { + NSMutableURLRequest.httpBodyHackSwizzle() + } + HttpBodyHack.instanceCount += 1 + } + + public func body(_ request: URLRequest) -> Data? { + return request.httpBodyHack() + } + + public func headers(_ request: URLRequest) -> [String: String]? { + guard let headers = request.allHTTPHeaderFields else { + return nil + } + var result = headers + result.removeValue(forKey: httpBodyHackHeaderName) + return result + } + + deinit { + HttpBodyHack.lock.lock() + defer { + HttpBodyHack.lock.unlock() + } + HttpBodyHack.instanceCount -= 1 + if HttpBodyHack.instanceCount == 0 { + // Unswizzle + NSMutableURLRequest.httpBodyHackSwizzle() + + httpBodyHackLock.lock() + defer { + httpBodyHackLock.unlock() + } + httpBodyHackValues.removeAll() + } + } +} diff --git a/MockingjayTests/HttpBodyHackTests.swift b/MockingjayTests/HttpBodyHackTests.swift new file mode 100644 index 0000000..43e5cfa --- /dev/null +++ b/MockingjayTests/HttpBodyHackTests.swift @@ -0,0 +1,46 @@ +// +// HttpBodyHackTests.swift +// Mockingjay +// +// Created by Alexey Kozhevnikov on 05/12/2016. +// Copyright © 2016 Cocode. All rights reserved. +// + +import Foundation +import XCTest +@testable import Mockingjay + +class HttpBodyHackTests: XCTestCase { + var urlSession:URLSession! + + override func setUp() { + super.setUp() + urlSession = URLSession(configuration: URLSessionConfiguration.default) + } + + override func tearDown() { + urlSession = nil + super.tearDown() + } + + func test() { + let data = "some data".data(using: .utf8) + + let bodyHack = HttpBodyHack() + let matcher: Matcher = { request in + XCTAssertEqual(bodyHack.body(request), data) + return true + } + stub(matcher, http()) + + let expectation = self.expectation(description: "completion called") + var request = URLRequest(url: URL(string: "https://kylefuller.co.uk/")!) + request.httpBody = data + let dataTask = urlSession.dataTask(with: request) { _ in + expectation.fulfill() + } + dataTask.resume() + + waitForExpectations(timeout: 2.0, handler: nil) + } +}