Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift Package Manager Support #122

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Xcode
#
build/
.build
*.pbxuser
!default.pbxuser
*.mode1v3
Expand Down
Binary file added .swiftpm/.DS_Store
Binary file not shown.
Binary file added .swiftpm/xcode/.DS_Store
Binary file not shown.
7 changes: 7 additions & 0 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 5 additions & 7 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Mockingjay Changelog

## TBD

### Enhancements

- Support for redirect responses (3xx).

## 3.0.0-alpha.1

### Breaking
Expand Down
2 changes: 1 addition & 1 deletion Mockingjay.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
39 changes: 39 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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")]
),
]
)
38 changes: 36 additions & 2 deletions Sources/Mockingjay/Builders.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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)
}
}
4 changes: 2 additions & 2 deletions Sources/Mockingjay/Mockingjay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
26 changes: 25 additions & 1 deletion Sources/Mockingjay/MockingjayProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ public class MockingjayProtocol: URLProtocol {
stubs.append(stub)

if !registered {
URLProtocol.registerClass(MockingjayProtocol.self)
MockingjayURLSessionConfiguration.swizzleDefaultSessionConfiguration()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the problem, that swizzling happens too lately. Previously it was called right after the app instance has been loaded into memory in [NSObject load]; method, and then app used swizzled version of URLSessionConfiguration.default in tests target. Have to move MockingjayURLSessionConfiguration.m file into MyProjectTests target and add @import Mockingjay;

URLProtocol.registerClass(MockingjayProtocol.self)
registered = true
}

return stub
Expand Down Expand Up @@ -109,13 +111,35 @@ 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 {
case .failure(let error):
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):
Expand Down
23 changes: 0 additions & 23 deletions Sources/Mockingjay/MockingjayURLSessionConfiguration.m

This file was deleted.

16 changes: 16 additions & 0 deletions Sources/Mockingjay/MockingjayURLSessionConfiguration.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
2 changes: 1 addition & 1 deletion Sources/Mockingjay/XCTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!)
}()
Expand Down
80 changes: 78 additions & 2 deletions Tests/MockingjayTests/BuildersTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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/")!)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
}
}
}
6 changes: 3 additions & 3 deletions Tests/MockingjayTests/MockingjayAsyncProtocolTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)])!
Expand Down Expand Up @@ -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)
Expand Down
Loading