diff --git a/.gitignore b/.gitignore index e0ae851..cea0fd7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Xcode # build/ +.build *.pbxuser !default.pbxuser *.mode1v3 diff --git a/.swiftpm/.DS_Store b/.swiftpm/.DS_Store new file mode 100644 index 0000000..84eb2cd Binary files /dev/null and b/.swiftpm/.DS_Store differ diff --git a/.swiftpm/xcode/.DS_Store b/.swiftpm/xcode/.DS_Store new file mode 100644 index 0000000..0998a6f Binary files /dev/null and b/.swiftpm/xcode/.DS_Store differ diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/.travis.yml b/.travis.yml index ad100f7..dad9388 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ -matrix: - include: - - os: osx - osx_image: xcode10.2 +language: swift +osx_image: xcode12u script: - - cd URITemplate && swift package generate-xcodeproj && cd .. - - xcodebuild -project Mockingjay.xcodeproj -scheme Mockingjay test - - pod lib lint --quick +- swift --version +- swift package update +- swift test \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 32dc9bc..cbd402f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Mockingjay Changelog +## TBD + +### Enhancements + +- Support for redirect responses (3xx). + ## 3.0.0-alpha.1 ### Breaking diff --git a/Mockingjay.podspec b/Mockingjay.podspec index 0ae36a8..668eadf 100644 --- a/Mockingjay.podspec +++ b/Mockingjay.podspec @@ -18,7 +18,7 @@ Pod::Spec.new do |spec| 'Sources/Mockingjay/MockingjayProtocol.swift', 'Sources/Mockingjay/{Matchers,Builders}.swift', 'Sources/Mockingjay/NSURLSessionConfiguration.swift', - 'Sources/Mockingjay/MockingjayURLSessionConfiguration.m' + 'Sources/Mockingjay/MockingjayURLSessionConfiguration.{m,swift}' end spec.subspec 'XCTest' do |xctest_spec| diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..9400eef --- /dev/null +++ b/Package.swift @@ -0,0 +1,39 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Mockingjay", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "Mockingjay", + targets: ["Mockingjay"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(name: "URITemplate", url: "https://github.com/kylef/URITemplate.swift.git", .branch("master")), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "Mockingjay", + dependencies: [ + .product(name: "URITemplate", package: "URITemplate"), + ], + exclude: ["Info.plist"] + ), + + .testTarget( + name: "MockingjayTests", + dependencies: [ + "Mockingjay", + .product(name: "URITemplate", package: "URITemplate"), + ], + exclude: ["Info.plist"], + resources: [.process("TestAudio.m4a")] + ), + ] +) diff --git a/Sources/Mockingjay/Builders.swift b/Sources/Mockingjay/Builders.swift index d390a9b..e04b958 100644 --- a/Sources/Mockingjay/Builders.swift +++ b/Sources/Mockingjay/Builders.swift @@ -10,9 +10,17 @@ import Foundation // Collection of generic builders +internal struct MockingjayFailure: Error {} + /// Generic builder for returning a failure -public func failure(_ error: NSError) -> (_ request: URLRequest) -> Response { - return { _ in return .failure(error) } +public func failure(_ error: Error? = nil) -> (_ request: URLRequest) -> Response { + return { _ in + if let error = error { + return .failure(error) + } + + return .failure(MockingjayFailure()) + } } public func http(_ status:Int = 200, headers:[String:String]? = nil, download:Download=nil) -> (_ request: URLRequest) -> Response { @@ -24,6 +32,22 @@ public func http(_ status:Int = 200, headers:[String:String]? = nil, download:Do return .failure(NSError(domain: NSExceptionName.internalInconsistencyException.rawValue, code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to construct response for stub."])) } } +public func content(_ data: Data, headers: [String:String]? = nil) -> (_ request: URLRequest) -> Response { + return http(200, headers: headers, download: .content(data)) +} + +public func text(_ body: String, using encoding: String.Encoding, status: Int = 200, headers: [String: String]? = nil) -> (_ request: URLRequest) -> Response { + var headers = headers ?? [String:String]() + if headers["Content-Type"] == nil && encoding == .utf8 { + headers["Content-Type"] = "text/plain; charset=utf-8" + } + + if let data = body.data(using: encoding) { + return http(status, headers: headers, download: .content(data)) + } + + return failure() +} public func json(_ body: Any, status:Int = 200, headers:[String:String]? = nil) -> (_ request: URLRequest) -> Response { return { (request:URLRequest) in @@ -46,3 +70,13 @@ public func jsonData(_ data: Data, status: Int = 200, headers: [String:String]? return http(status, headers: headers, download: .content(data))(request) } } + +public func redirect(to url: URL, status: Int = 301, headers: [String:String]? = nil) -> (_ request: URLRequest) -> Response { + var headers = headers ?? [:] + headers["Location"] = url.absoluteString + + let resoponse = HTTPURLResponse(url: url, statusCode: status, httpVersion: nil, headerFields: headers)! + return { request in + return .success(resoponse, .noContent) + } +} diff --git a/Sources/Mockingjay/Mockingjay.swift b/Sources/Mockingjay/Mockingjay.swift index 4bd8e17..b833723 100644 --- a/Sources/Mockingjay/Mockingjay.swift +++ b/Sources/Mockingjay/Mockingjay.swift @@ -38,13 +38,13 @@ public func ==(lhs:Download, rhs:Download) -> Bool { public enum Response : Equatable { case success(URLResponse, Download) - case failure(NSError) + case failure(Error) } public func ==(lhs:Response, rhs:Response) -> Bool { switch (lhs, rhs) { case let (.failure(lhsError), .failure(rhsError)): - return lhsError == rhsError + return (lhsError as NSError) == (rhsError as NSError) case let (.success(lhsResponse, lhsDownload), .success(rhsResponse, rhsDownload)): return lhsResponse == rhsResponse && lhsDownload == rhsDownload default: diff --git a/Sources/Mockingjay/MockingjayProtocol.swift b/Sources/Mockingjay/MockingjayProtocol.swift index 0d224e9..6cf6ee5 100644 --- a/Sources/Mockingjay/MockingjayProtocol.swift +++ b/Sources/Mockingjay/MockingjayProtocol.swift @@ -40,7 +40,9 @@ public class MockingjayProtocol: URLProtocol { stubs.append(stub) if !registered { - URLProtocol.registerClass(MockingjayProtocol.self) + MockingjayURLSessionConfiguration.swizzleDefaultSessionConfiguration() + URLProtocol.registerClass(MockingjayProtocol.self) + registered = true } return stub @@ -109,6 +111,23 @@ public class MockingjayProtocol: URLProtocol { } // MARK: Private Methods + + func isRedirect(request: URLRequest, response: URLResponse) -> URLRequest? { + guard + let response = response as? HTTPURLResponse, + response.statusCode == 301 || response.statusCode == 302 || response.statusCode == 303 || response.statusCode == 307, + let location = response.allHeaderFields["Location"] as? String, + let locationURL = URL(string: location, relativeTo: response.url) + else { return nil } + + if response.statusCode == 307 { + var redirectRequest = request + redirectRequest.url = locationURL + return redirectRequest + } + + return URLRequest(url: locationURL) + } fileprivate func sendResponse(_ response: Response) { switch response { @@ -116,6 +135,11 @@ public class MockingjayProtocol: URLProtocol { client?.urlProtocol(self, didFailWithError: error) case .success(var response, let download): let headers = self.request.allHTTPHeaderFields + + if let redirectRequest = isRedirect(request: self.request, response: response) { + client?.urlProtocol(self, wasRedirectedTo: redirectRequest, redirectResponse: response) + return + } switch(download) { case .content(var data): diff --git a/Sources/Mockingjay/MockingjayURLSessionConfiguration.m b/Sources/Mockingjay/MockingjayURLSessionConfiguration.m deleted file mode 100644 index f83735a..0000000 --- a/Sources/Mockingjay/MockingjayURLSessionConfiguration.m +++ /dev/null @@ -1,23 +0,0 @@ -// -// MockingjayURLSessionConfiguration.m -// Mockingjay -// -// Created by Kyle Fuller on 10/05/2016. -// Copyright © 2016 Cocode. All rights reserved. -// - -#import -#import - - -@interface MockingjayURLConfiguration : NSObject - -@end - -@implementation MockingjayURLConfiguration - -+ (void)load { - [NSURLSessionConfiguration mockingjaySwizzleDefaultSessionConfiguration]; -} - -@end \ No newline at end of file diff --git a/Sources/Mockingjay/MockingjayURLSessionConfiguration.swift b/Sources/Mockingjay/MockingjayURLSessionConfiguration.swift new file mode 100644 index 0000000..a7a44f4 --- /dev/null +++ b/Sources/Mockingjay/MockingjayURLSessionConfiguration.swift @@ -0,0 +1,16 @@ +// +// MockingjayURLSessionConfiguration.m +// Mockingjay +// +// Created by Kyle Fuller on 10/05/2016. +// Copyright © 2016 Cocode. All rights reserved. +// + +import Foundation + +public class MockingjayURLSessionConfiguration: NSObject { + + public class func swizzleDefaultSessionConfiguration() { + URLSessionConfiguration.mockingjaySwizzleDefaultSessionConfiguration() + } +} diff --git a/Sources/Mockingjay/XCTest.swift b/Sources/Mockingjay/XCTest.swift index 8e2905a..ad0e2f7 100644 --- a/Sources/Mockingjay/XCTest.swift +++ b/Sources/Mockingjay/XCTest.swift @@ -10,7 +10,7 @@ import ObjectiveC import XCTest let swizzleTearDown: Void = { - let tearDown = class_getInstanceMethod(XCTest.self, #selector(XCTest.tearDown)) + let tearDown = class_getInstanceMethod(XCTest.self, #selector(XCTest.tearDown as (XCTest) -> () -> Void)) let mockingjayTearDown = class_getInstanceMethod(XCTest.self, #selector(XCTest.mockingjayTearDown)) method_exchangeImplementations(tearDown!, mockingjayTearDown!) }() diff --git a/Tests/MockingjayTests/BuildersTests.swift b/Tests/MockingjayTests/BuildersTests.swift index 6b97f97..a440471 100644 --- a/Tests/MockingjayTests/BuildersTests.swift +++ b/Tests/MockingjayTests/BuildersTests.swift @@ -20,6 +20,19 @@ class FailureBuilderTests : XCTestCase { XCTAssertEqual(response, Response.failure(error)) } + + func testUnspecifiedFailure() { + let request = URLRequest(url: URL(string: "http://test.com/")!) + + let response = failure()(request) + + switch response { + case .success(_, _): + XCTFail("Unexpected success") + case .failure(_): + break + } + } func testHTTP() { let request = URLRequest(url: URL(string: "http://test.com/")!) @@ -51,10 +64,35 @@ class FailureBuilderTests : XCTestCase { XCTFail("Test Failure") } case let .failure(error): - XCTFail("Test Failure: " + error.debugDescription) + XCTFail("Test Failure: " + (error as NSError).debugDescription) } } - + + func testText() { + let request = URLRequest(url: URL(string: "http://test.com/")!) + let response = text("Hello World", using: .utf8)(request) + + switch response { + case let .success(response, download): + switch download { + case .content(let data): + if let response = response as? HTTPURLResponse { + XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual(response.mimeType, "text/plain") + XCTAssertEqual(response.textEncodingName, "utf-8") + let body = NSString(data:data, encoding: String.Encoding.utf8.rawValue) + XCTAssertEqual(body, "Hello World") + } else { + XCTFail("Test Failure") + } + default: + XCTFail("Test Failure") + } + default: + XCTFail("Test Failure") + } + } + func testJSON() { let request = URLRequest(url: URL(string: "http://test.com/")!) let response = json(["A"])(request) @@ -107,4 +145,42 @@ class FailureBuilderTests : XCTestCase { XCTFail("Test Failure") } } + + func testRedirect() { + let request = URLRequest(url: URL(string: "http://example.com")!) + let response = redirect(to: URL(string: "https://example.com")!)(request) + + switch response { + case let .success(response, _): + guard let response = response as? HTTPURLResponse else { + XCTFail("Test Failure") + return + } + + + XCTAssertEqual(response.statusCode, 301) + XCTAssertEqual(response.allHeaderFields["Location"] as? String, "https://example.com") + default: + XCTFail("Test Failure") + } + } + + func testRelativeRedirect() { + let request = URLRequest(url: URL(string: "https://example.com")!) + let response = redirect(to: URL(string: "/authorize")!)(request) + + switch response { + case let .success(response, _): + guard let response = response as? HTTPURLResponse else { + XCTFail("Test Failure") + return + } + + + XCTAssertEqual(response.statusCode, 301) + XCTAssertEqual(response.allHeaderFields["Location"] as? String, "/authorize") + default: + XCTFail("Test Failure") + } + } } diff --git a/Tests/MockingjayTests/MockingjayAsyncProtocolTests.swift b/Tests/MockingjayTests/MockingjayAsyncProtocolTests.swift index 555e56d..e5fe8dd 100644 --- a/Tests/MockingjayTests/MockingjayAsyncProtocolTests.swift +++ b/Tests/MockingjayTests/MockingjayAsyncProtocolTests.swift @@ -61,7 +61,7 @@ class MockingjayAsyncProtocolTests: XCTestCase, URLSessionDataDelegate { func testDownloadOfAudioFileInChunks() { let request = URLRequest(url: URL(string: "https://fuller.li/")!) - let path = Bundle(for: self.classForCoder).path(forResource: "TestAudio", ofType: "m4a") + let path = Bundle.module.path(forResource: "TestAudio", ofType: "m4a") let data = try! Data(contentsOf: URL(fileURLWithPath: path!)) let stubResponse = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "1.1", headerFields: ["Content-Length" : String(data.count)])! @@ -91,14 +91,14 @@ class MockingjayAsyncProtocolTests: XCTestCase, URLSessionDataDelegate { let length = 100000 var request = URLRequest(url: URL(string: "https://fuller.li/")!) request.addValue("bytes=50000-149999", forHTTPHeaderField: "Range") - let path = Bundle(for: self.classForCoder).path(forResource: "TestAudio", ofType: "m4a") + let path = Bundle.module.path(forResource: "TestAudio", ofType: "m4a") let data = try! Data(contentsOf: URL(fileURLWithPath: path!)) let stubResponse = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "1.1", headerFields: ["Content-Length" : String(length)])! MockingjayProtocol.addStub(matcher: { (requestedRequest) -> (Bool) in return true }) { (request) -> (Response) in - return Response.success(stubResponse, .streamContent(data: data, inChunksOf: 2000)) + return Response.success(stubResponse, .streamContent(data: data, inChunksOf: 2000)) } let urlSession = Foundation.URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.current) diff --git a/Tests/MockingjayTests/MockingjayProtocolTests.swift b/Tests/MockingjayTests/MockingjayProtocolTests.swift index 43960ec..871c8f3 100644 --- a/Tests/MockingjayTests/MockingjayProtocolTests.swift +++ b/Tests/MockingjayTests/MockingjayProtocolTests.swift @@ -165,5 +165,60 @@ class MockingjayProtocolTests : XCTestCase { XCTAssert(startDate.addingTimeInterval(0.95).compare(Date()) == .orderedAscending) } - + + func testRedirect() { + let request = URLRequest(url: URL(string: "http://example.com")!) + + MockingjayProtocol.addStub( + matcher: { $0.url?.absoluteString == "https://example.com" }, + builder: http(204, download: .noContent) + ) + + MockingjayProtocol.addStub( + matcher: { $0.url?.absoluteString == "http://example.com" }, + builder: http(301, headers: ["Location": "https://example.com"], download: .noContent) + ) + + let expectation = self.expectation(description: #function) + + urlSession = URLSession(configuration: URLSessionConfiguration.default) + let dataTask = urlSession.dataTask(with: request) { _, response, _ in + XCTAssertEqual(response?.url?.absoluteString, "https://example.com") + expectation.fulfill() + } + dataTask.resume() + + waitForExpectations(timeout: 2.0, handler: nil) + } + + func testRedirect307() { + var request = URLRequest(url: URL(string: "http://example.com")!) + request.httpMethod = "PUT" + request.setValue("text/plain", forHTTPHeaderField: "Content-Type") + + MockingjayProtocol.addStub( + matcher: { $0.url?.absoluteString == "https://example.com" }, + builder: http(204, download: .noContent) + ) + + MockingjayProtocol.addStub( + matcher: { $0.url?.absoluteString == "http://example.com" }, + builder: { request in + XCTAssertEqual(request.httpMethod, "PUT") + XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "text/plain") + return http(307, headers: ["Location": "https://example.com"], download: .noContent)(request) + } + ) + + let expectation = self.expectation(description: #function) + + urlSession = URLSession(configuration: URLSessionConfiguration.default) + let dataTask = urlSession.dataTask(with: request) { _, response, _ in + XCTAssertEqual(response?.url?.absoluteString, "https://example.com") + expectation.fulfill() + } + dataTask.resume() + + waitForExpectations(timeout: 2.0, handler: nil) + } }