ably-cocoa/Spec/TestUtilities.swift

1908 lines
65 KiB
Swift

import Ably
import Foundation
import XCTest
import Quick
import Nimble
import SwiftyJSON
import Aspects
import Ably.Private
typealias HookToken = AspectToken
let AblyTestsErrorDomain = "test.ably.io"
class CryptoTest {
private static let aes128 = "cipher+aes-128-cbc";
private static let aes256 = "cipher+aes-256-cbc";
public static let fixtures: [(
fileName: String,
expectedEncryptedEncoding: String,
keyLength: UInt
)] = [
("crypto-data-128", aes128, 128),
("crypto-data-256", aes256, 256),
];
}
// Swift isn't yet smart enough to do this automatically when bridging Objective-C APIs
extension ARTRealtimeChannels: Sequence {
public func makeIterator() -> NSFastEnumerationIterator {
return NSFastEnumerationIterator(self.iterate())
}
}
class Configuration : QuickConfiguration {
override class func configure(_ configuration: Quick.Configuration!) {
configuration.beforeSuite {
AsyncDefaults.timeout = testTimeout
}
}
}
typealias ARTDeviceId = String
func pathForTestResource(_ resourcePath: String) -> String {
let testBundle = Bundle(for: AblyTests.self)
return testBundle.path(forResource: resourcePath, ofType: "")!
}
let appSetupJson = JSON(parseJSON: try! String(contentsOfFile: pathForTestResource(testResourcesPath + "test-app-setup.json")))
let testTimeout = DispatchTimeInterval.seconds(20)
let testResourcesPath = "ably-common/test-resources/"
let echoServerAddress = "https://echo.ably.io/createJWT"
/// Common test utilities.
class AblyTests {
class func base64ToData(_ base64: String) -> Data {
return Data(base64Encoded: base64, options: NSData.Base64DecodingOptions(rawValue: 0))!
}
class func msgpackToJSON(_ data: Data) -> JSON {
let decoded = try! ARTMsgPackEncoder().decode(data)
let encoded = try! ARTJsonEncoder().encode(decoded)
return try! JSON(data: encoded)
}
class func checkError(_ errorInfo: ARTErrorInfo?, withAlternative message: String) {
if let error = errorInfo {
XCTFail("\((error ).code): \(error.message)")
}
else if !message.isEmpty {
XCTFail(message)
}
}
class func checkError(_ errorInfo: ARTErrorInfo?) {
checkError(errorInfo, withAlternative: "")
}
class var jsonRestOptions: ARTClientOptions {
get {
let options = AblyTests.clientOptions()
return options
}
}
class var authTokenCases: [String: (ARTAuthOptions) -> Void] {
get { return [
"useTokenAuth": { $0.useTokenAuth = true; $0.key = "fake:key" },
"authUrl": { $0.authUrl = URL(string: "http://test.com") },
"authCallback": { $0.authCallback = { _, _ in return } },
"tokenDetails": { $0.tokenDetails = ARTTokenDetails(token: "token") },
"token": { $0.token = "token" },
"key": { $0.tokenDetails = ARTTokenDetails(token: "token"); $0.key = "fake:key" }
]
}
}
static var testApplication: JSON?
static fileprivate var setupOptionsCounter = 0
struct QueueIdentity {
let label: String
}
static var queueIdentityKey = DispatchSpecificKey<QueueIdentity>()
static var queue: DispatchQueue = {
let queue = DispatchQueue(label: "io.ably.tests", qos: .userInitiated)
queue.setSpecific(key: queueIdentityKey, value: QueueIdentity(label: queue.label))
return queue
}()
static var userQueue: DispatchQueue = {
let queue = DispatchQueue(label: "io.ably.tests.callbacks", qos: .userInitiated)
queue.setSpecific(key: queueIdentityKey, value: QueueIdentity(label: queue.label))
return queue
}()
static var extraQueue: DispatchQueue = {
let queue = DispatchQueue(label: "io.ably.tests.extra", qos: .userInitiated)
queue.setSpecific(key: queueIdentityKey, value: QueueIdentity(label: queue.label))
return queue
}()
static func currentQueueLabel() -> String? {
return DispatchQueue.getSpecific(key: queueIdentityKey)?.label
}
class func setupOptions(_ options: ARTClientOptions, forceNewApp: Bool = false, debug: Bool = false) -> ARTClientOptions {
options.channelNamePrefix = "test-\(setupOptionsCounter)"
setupOptionsCounter += 1
if forceNewApp {
testApplication = nil
}
guard let app = testApplication else {
let request = NSMutableURLRequest(url: URL(string: "https://\(options.restHost):\(options.tlsPort)/apps")!)
request.httpMethod = "POST"
request.httpBody = try? appSetupJson["post_apps"].rawData()
request.allHTTPHeaderFields = [
"Accept" : "application/json",
"Content-Type" : "application/json"
]
let (responseData, responseError, _) = NSURLSessionServerTrustSync().get(request)
if let error = responseError {
fatalError(error.localizedDescription)
}
testApplication = try! JSON(data: responseData!)
if debug {
print(testApplication!)
}
return setupOptions(options, debug: debug)
}
let key = app["keys"][0]
options.key = key["keyStr"].stringValue
options.dispatchQueue = DispatchQueue.main
options.internalDispatchQueue = queue
if debug {
options.logLevel = .verbose
}
return options
}
class func commonAppSetup(_ debug: Bool = false) -> ARTClientOptions {
return AblyTests.setupOptions(AblyTests.jsonRestOptions, debug: debug)
}
class func clientOptions(_ debug: Bool = false, key: String? = nil, requestToken: Bool = false) -> ARTClientOptions {
let options = ARTClientOptions()
options.environment = getEnvironment()
options.logExceptionReportingUrl = nil
if debug {
options.logLevel = .debug
}
if let key = key {
options.key = key
}
if requestToken {
options.token = getTestToken()
}
options.dispatchQueue = DispatchQueue.main
options.internalDispatchQueue = queue
return options
}
class func newErrorProtocolMessage(message: String = "Fail test") -> ARTProtocolMessage {
let protocolMessage = ARTProtocolMessage()
protocolMessage.action = .error
protocolMessage.error = ARTErrorInfo.create(withCode: 0, message: message)
return protocolMessage
}
class func newPresenceProtocolMessage(_ channel: String, action: ARTPresenceAction, clientId: String) -> ARTProtocolMessage {
let protocolMessage = ARTProtocolMessage()
protocolMessage.action = .presence
protocolMessage.channel = channel
protocolMessage.timestamp = Date()
let presenceMessage = ARTPresenceMessage()
presenceMessage.action = action
presenceMessage.clientId = clientId
presenceMessage.timestamp = Date()
protocolMessage.presence = [presenceMessage]
return protocolMessage
}
class func newRealtime(_ options: ARTClientOptions) -> ARTRealtime {
let autoConnect = options.autoConnect
options.autoConnect = false
let realtime = ARTRealtime(options: options)
realtime.internal.setTransport(TestProxyTransport.self)
realtime.internal.setReachabilityClass(TestReachability.self)
if autoConnect {
options.autoConnect = true
realtime.connect()
}
return realtime
}
class func newRandomString() -> String {
return ProcessInfo.processInfo.globallyUniqueString
}
class func addMembersSequentiallyToChannel(_ channelName: String, members: Int = 1, startFrom: Int = 1, data: AnyObject? = nil, options: ARTClientOptions) -> ARTRealtime {
let client = ARTRealtime(options: options)
let channel = client.channels.get(channelName)
waitUntil(timeout: testTimeout) { done in
channel.attach() { _ in
done()
}
}
for i in startFrom..<startFrom+members {
waitUntil(timeout: testTimeout) { done in
channel.presence.enterClient("user\(i)", data: data) { _ in
done()
}
}
}
return client
}
class func addMembersSequentiallyToChannel(_ channelName: String, members: Int = 1, startFrom: Int = 1, data: AnyObject? = nil, options: ARTClientOptions, done: @escaping ()->()) -> ARTRealtime {
let client = ARTRealtime(options: options)
let channel = client.channels.get(channelName)
class Total {
static var count: Int = 0
}
Total.count = 0
channel.attach() { _ in
for i in startFrom..<startFrom+members {
channel.presence.enterClient("user\(i)", data: data) { _ in
Total.count += 1
if Total.count == members {
done()
}
}
}
}
return client
}
class func splitDone(_ howMany: Int, file: StaticString = #file, line: UInt = #line, done: @escaping () -> Void) -> (() -> Void) {
var left = howMany
return {
left -= 1
if left == 0 {
done()
} else if left < 0 {
XCTFail("splitDone called more than the expected \(howMany) times", file: file, line: line)
}
}
}
class func waitFor<T>(timeout: DispatchTimeInterval, file: FileString = #file, line: UInt = #line, f: @escaping (@escaping (T?) -> Void) -> Void) -> T? {
var value: T?
waitUntil(timeout: timeout, file: file, line: line) { done in
f() { v in
value = v
done()
}
}
return value
}
class func wait(for expectations: [XCTestExpectation], timeout dispatchInterval: DispatchTimeInterval = testTimeout, file: Nimble.FileString = #file, line: UInt = #line) {
let result = XCTWaiter.wait(
for: expectations,
timeout: dispatchInterval.toTimeInterval(),
enforceOrder: true
)
let title: String = "Waiter of expectations \(expectations.map({ $0.description }))"
switch result {
case .timedOut:
fail(title + " timed out (\(dispatchInterval)).", file: file, line: line)
case .invertedFulfillment:
fail(title + " shouldn't receive a fulfillment.", file: file, line: line)
case .interrupted:
fail(title + " got interrupted.", file: file, line: line)
case .incorrectOrder:
fail(title + " failed with incorrect order.", file: file, line: line)
case .completed:
break //completed successfully
default:
preconditionFailure("XCTWaiter.Result.\(String(describing: result)) not implemented")
}
}
// MARK: Crypto
struct CryptoTestItem {
struct TestMessage {
let name: String
let data: String
let encoding: String
}
let encoded: TestMessage
let encrypted: TestMessage
init(object: JSON) {
let encodedJson = object["encoded"]
encoded = TestMessage(name: encodedJson["name"].stringValue, data: encodedJson["data"].stringValue, encoding: encodedJson["encoding"].string ?? "")
let encryptedJson = object["encrypted"]
encrypted = TestMessage(name: encryptedJson["name"].stringValue, data: encryptedJson["data"].stringValue, encoding: encryptedJson["encoding"].stringValue)
}
}
class func loadCryptoTestData(_ fileName: String) -> (key: Data, iv: Data, items: [CryptoTestItem]) {
let file = testResourcesPath + fileName + ".json";
let json = JSON(parseJSON: try! String(contentsOfFile: pathForTestResource(file)))
let keyData = Data(base64Encoded: json["key"].stringValue, options: Data.Base64DecodingOptions(rawValue: 0))!
let ivData = Data(base64Encoded: json["iv"].stringValue, options: Data.Base64DecodingOptions(rawValue: 0))!
let items = json["items"].map{ $0.1 }.map(CryptoTestItem.init)
return (keyData, ivData, items)
}
}
class NSURLSessionServerTrustSync: NSObject, URLSessionDelegate, URLSessionTaskDelegate {
func get(_ request: NSMutableURLRequest) -> (Data?, NSError?, HTTPURLResponse?) {
var responseError: NSError?
var responseData: Data?
var httpResponse: HTTPURLResponse?;
var requestCompleted = false
let configuration = URLSessionConfiguration.default
let queue = OperationQueue()
queue.underlyingQueue = AblyTests.extraQueue
let session = Foundation.URLSession(configuration:configuration, delegate:self, delegateQueue:queue)
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
if let response = response as? HTTPURLResponse {
responseData = data
responseError = error as NSError?
httpResponse = response
}
else if let error = error {
responseError = error as NSError?
}
requestCompleted = true
})
task.resume()
while !requestCompleted {
CFRunLoopRunInMode(CFRunLoopMode.defaultMode, CFTimeInterval(0.1), Bool(truncating: 0))
}
return (responseData, responseError, httpResponse)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// Try to extract the server certificate for trust validation
if let serverTrust = challenge.protectionSpace.serverTrust {
// Server trust authentication
// Reference: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/URLLoadingSystem/Articles/AuthenticationChallenges.html
completionHandler(Foundation.URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: serverTrust))
}
else {
challenge.sender?.performDefaultHandling?(for: challenge)
XCTFail("Current authentication: \(challenge.protectionSpace.authenticationMethod)")
}
}
}
extension Date {
func isBefore(_ other: Date) -> Bool {
return self.compare(other) == ComparisonResult.orderedAscending
}
}
// MARK: ARTAuthOptions Equatable
func ==(lhs: ARTAuthOptions, rhs: ARTAuthOptions) -> Bool {
return lhs.token == rhs.token &&
lhs.authMethod == rhs.authMethod &&
lhs.authUrl == rhs.authUrl &&
lhs.key == rhs.key
}
func ==(lhs: ARTJsonCompatible?, rhs: ARTJsonCompatible?) -> Bool {
guard let lhs = lhs else {
return rhs == nil
}
guard let rhs = rhs else {
return false
}
do {
return NSDictionary(dictionary: try lhs.toJSON()).isEqual(to: try rhs.toJSON())
} catch {
return false
}
}
// MARK: Publish message class
class PublishTestMessage {
var completion: ((ARTErrorInfo?) -> Void)? = nil
var error: ARTErrorInfo? = nil
init(client: ARTRest, failOnError: Bool = true, completion: ((ARTErrorInfo?) -> Void)? = nil) {
client.channels.get("test").publish(nil, data: "message") { error in
self.error = error
if let callback = completion {
callback(error)
}
else if failOnError, let e = error {
XCTFail("Got error '\(e)'")
}
}
}
init(client: ARTRealtime, failOnError: Bool = true, completion: ((ARTErrorInfo?) -> Void)? = nil) {
let complete: (ARTErrorInfo?) -> Void = { errorInfo in
// ARTErrorInfo to NSError
self.error = errorInfo
if let callback = completion {
callback(self.error)
}
else if failOnError, let e = self.error {
XCTFail("Got error '\(e)'")
}
}
client.connection.on { stateChange in
let state = stateChange.current
if state == .connected {
let channel = client.channels.get("test")
channel.on { stateChange in
switch stateChange.current {
case .attached:
channel.publish(nil, data: "message") { errorInfo in
complete(errorInfo)
}
case .failed:
complete(stateChange.reason)
default:
break
}
}
channel.attach()
}
}
}
}
/// Rest - Publish message
@discardableResult func publishTestMessage(_ rest: ARTRest, completion: Optional<(ARTErrorInfo?)->()>) -> PublishTestMessage {
return PublishTestMessage(client: rest, failOnError: false, completion: completion)
}
@discardableResult func publishTestMessage(_ rest: ARTRest, failOnError: Bool = true) -> PublishTestMessage {
return PublishTestMessage(client: rest, failOnError: failOnError)
}
/// Realtime - Publish message with callback
/// (publishes if connection state changes to CONNECTED and channel state changes to ATTACHED)
@discardableResult func publishFirstTestMessage(_ realtime: ARTRealtime, completion: Optional<(ARTErrorInfo?)->()>) -> PublishTestMessage {
return PublishTestMessage(client: realtime, failOnError: false, completion: completion)
}
/// Realtime - Publish message
/// (publishes if connection state changes to CONNECTED and channel state changes to ATTACHED)
@discardableResult func publishFirstTestMessage(_ realtime: ARTRealtime, failOnError: Bool = true) -> PublishTestMessage {
return PublishTestMessage(client: realtime, failOnError: failOnError)
}
/// Access Token
func getTestToken(key: String? = nil, clientId: String? = nil, capability: String? = nil, ttl: TimeInterval? = nil, file: FileString = #file, line: UInt = #line) -> String {
return getTestTokenDetails(key: key, clientId: clientId, capability: capability, ttl: ttl, file: file, line: line)?.token ?? ""
}
func getTestToken(key: String? = nil, clientId: String? = nil, capability: String? = nil, ttl: TimeInterval? = nil, file: FileString = #file, line: UInt = #line, completion: @escaping (String) -> Void) {
getTestTokenDetails(key: key, clientId: clientId, capability: capability, ttl: ttl) { tokenDetails, error in
if let e = error {
fail(e.localizedDescription, file: file, line: line)
}
completion(tokenDetails?.token ?? "")
}
}
/// Access TokenDetails
func getTestTokenDetails(key: String? = nil, clientId: String? = nil, capability: String? = nil, ttl: TimeInterval? = nil, queryTime: Bool? = nil, completion: @escaping (ARTTokenDetails?, Error?) -> Void) {
let options: ARTClientOptions
if let key = key {
options = AblyTests.clientOptions()
options.key = key
}
else {
options = AblyTests.commonAppSetup()
}
if let queryTime = queryTime {
options.queryTime = queryTime
}
let client = ARTRest(options: options)
var tokenParams: ARTTokenParams? = nil
if let capability = capability {
tokenParams = ARTTokenParams()
tokenParams!.capability = capability
}
if let ttl = ttl {
if tokenParams == nil { tokenParams = ARTTokenParams() }
tokenParams!.ttl = NSNumber(value: ttl)
}
if let clientId = clientId {
if tokenParams == nil { tokenParams = ARTTokenParams() }
tokenParams!.clientId = clientId
}
client.auth.requestToken(tokenParams, with: nil) { details, error in
_ = client // Hold reference to client, since requestToken is async and will lose it.
completion(details, error)
}
}
func getTestTokenDetails(key: String? = nil, clientId: String? = nil, capability: String? = nil, ttl: TimeInterval? = nil, queryTime: Bool? = nil, file: FileString = #file, line: UInt = #line) -> ARTTokenDetails? {
guard let (tokenDetails, error) = (AblyTests.waitFor(timeout: testTimeout, file: file, line: line) { value in
getTestTokenDetails(key: key, clientId: clientId, capability: capability, ttl: ttl, queryTime: queryTime) { tokenDetails, error in
value((tokenDetails, error))
}
}) else {
return nil
}
if let e = error {
fail(e.localizedDescription, file: file, line: line)
}
return tokenDetails
}
func getJWTToken(invalid: Bool = false, expiresIn: Int = 3600, clientId: String = "testClientIDiOS", capability: String = "{\"*\":[\"*\"]}", jwtType: String = "", encrypted: Int = 0) -> String? {
let options = AblyTests.commonAppSetup()
guard let components = options.key?.components(separatedBy: ":"), let keyName = components.first, var keySecret = components.last else {
fail("Invalid API key: \(options.key ?? "nil")")
return nil
}
if (invalid) {
keySecret = "invalid"
}
var urlComponents = URLComponents(string: echoServerAddress)
urlComponents?.queryItems = [
URLQueryItem(name: "keyName", value: keyName),
URLQueryItem(name: "keySecret", value: keySecret),
URLQueryItem(name: "expiresIn", value: String(expiresIn)),
URLQueryItem(name: "clientId", value: clientId),
URLQueryItem(name: "capability", value: capability),
URLQueryItem(name: "jwtType", value: jwtType),
URLQueryItem(name: "encrypted", value: String(encrypted)),
URLQueryItem(name: "environment", value: getEnvironment())
]
let request = NSMutableURLRequest(url: urlComponents!.url!)
let (responseData, responseError, _) = NSURLSessionServerTrustSync().get(request)
if let error = responseError {
fail(error.localizedDescription)
return nil
}
return String(data: responseData!, encoding: String.Encoding.utf8)
}
func getKeys() -> Dictionary<String, String> {
let options = AblyTests.commonAppSetup()
guard let components = options.key?.components(separatedBy: ":"), let keyName = components.first, let keySecret = components.last else {
fatalError("Invalid API key)")
}
return ["keyName": keyName, "keySecret": keySecret]
}
public func delay(_ seconds: TimeInterval, closure: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: closure)
}
public func getEnvironment() -> String {
let b = Bundle(for: AblyTests.self)
guard let env = b.infoDictionary!["ABLY_ENV"] as? String, env.count > 0 else {
return "sandbox"
}
return env
}
public func buildMessagesThatExceedMaxMessageSize() -> [ARTMessage] {
var messages = [ARTMessage]()
for index in 0...5000 {
let m = ARTMessage(name: "name-\(index)", data: "data-\(index)")
messages.append(m)
}
return messages
}
public func buildStringThatExceedMaxMessageSize() -> String {
var name = ""
for index in 0...10000 {
name += "name-\(index)"
}
return name
}
class Box<T> {
let unbox: T
init(_ value: T) {
self.unbox = value
}
}
enum Result<T> {
case success(Box<T>)
case failure(String)
/// Constructs a success wrapping a `value`.
init(value: Box<T>) {
self = .success(value)
}
/// Constructs a failure wrapping an `error`.
init(error: String) {
self = .failure(error)
}
}
func extractURL(_ request: URLRequest?) -> Result<URL> {
guard let request = request
else { return Result(error: "No request found") }
guard let url = request.url
else { return Result(error: "Request has no URL defined") }
return Result.success(Box(url))
}
func extractBodyAsJSON(_ request: URLRequest?) -> Result<NSDictionary> {
guard let request = request
else { return Result(error: "No request found") }
guard let bodyData = request.httpBody
else { return Result(error: "No HTTPBody") }
guard let json = try? JSONSerialization.jsonObject(with: bodyData, options: .mutableLeaves)
else { return Result(error: "Invalid json") }
guard let httpBody = json as? NSDictionary
else { return Result(error: "HTTPBody has invalid format") }
return Result.success(Box(httpBody))
}
func extractBodyAsMsgPack(_ request: URLRequest?) -> Result<NSDictionary> {
guard let request = request
else { return Result(error: "No request found") }
guard let bodyData = request.httpBody
else { return Result(error: "No HTTPBody") }
let json = try! ARTMsgPackEncoder().decode(bodyData)
guard let httpBody = json as? NSDictionary
else { return Result(error: "expected dictionary, got \(type(of: (json) as AnyObject)): \(json)") }
return Result.success(Box(httpBody))
}
func extractBodyAsMessages(_ request: URLRequest?) -> Result<[NSDictionary]> {
guard let request = request
else { return Result(error: "No request found") }
guard let bodyData = request.httpBody
else { return Result(error: "No HTTPBody") }
let json = try! ARTMsgPackEncoder().decode(bodyData)
guard let httpBody = json as? NSArray
else { return Result(error: "expected array, got \(type(of: (json) as AnyObject)): \(json)") }
return Result.success(Box(httpBody.map{$0 as! NSDictionary}))
}
func extractURLQueryValue(_ url: URL?, key name: String) -> String? {
guard let query = url?.query else {
return nil
}
let queryItems = query.components(separatedBy: "&")
for item in queryItems {
let param = item.components(separatedBy: "=")
if param.first == name {
return param.last
}
}
return nil
}
enum FakeNetworkResponse {
case noInternet
case hostUnreachable
case requestTimeout(timeout: TimeInterval)
case hostInternalError(code: Int)
case host400BadRequest
var error: NSError {
switch self {
case .noInternet:
return NSError(domain: NSPOSIXErrorDomain, code: 50, userInfo: [NSLocalizedDescriptionKey: "network is down", NSLocalizedFailureReasonErrorKey: AblyTestsErrorDomain + ".FakeNetworkResponse"])
case .hostUnreachable:
return NSError(domain: kCFErrorDomainCFNetwork as String, code: 2, userInfo: [NSLocalizedDescriptionKey: "host unreachable", NSLocalizedFailureReasonErrorKey: AblyTestsErrorDomain + ".FakeNetworkResponse"])
case .requestTimeout:
return NSError(domain: "com.squareup.SocketRocket", code: 504, userInfo: [NSLocalizedDescriptionKey: "timed out", NSLocalizedFailureReasonErrorKey: AblyTestsErrorDomain + ".FakeNetworkResponse"])
case .hostInternalError(let code):
return NSError(domain: AblyTestsErrorDomain, code: code, userInfo: [NSLocalizedDescriptionKey: "internal error", NSLocalizedFailureReasonErrorKey: AblyTestsErrorDomain + ".FakeNetworkResponse"])
case .host400BadRequest:
return NSError(domain: AblyTestsErrorDomain, code: 400, userInfo: [NSLocalizedDescriptionKey: "bad request", NSLocalizedFailureReasonErrorKey: AblyTestsErrorDomain + ".FakeNetworkResponse"])
}
}
func transportError(for url: URL) -> ARTRealtimeTransportError {
switch self {
case .noInternet:
return ARTRealtimeTransportError(error: error, type: .noInternet, url: url)
case .hostUnreachable:
return ARTRealtimeTransportError(error: error, type: .hostUnreachable, url: url)
case .requestTimeout:
return ARTRealtimeTransportError(error: error, type: .timeout, url: url)
case .hostInternalError(let code):
return ARTRealtimeTransportError(error: error, badResponseCode: code, url: url)
case .host400BadRequest:
return ARTRealtimeTransportError(error: error, badResponseCode: 400, url: url)
}
}
}
class MockHTTP: ARTHttp {
enum Rule {
case host(name: String)
case resetAfter(numberOfRequests: Int)
}
private var networkState: FakeNetworkResponse?
private var rule: Rule?
private var count: Int = 0
init(logger: ARTLog) {
super.init(AblyTests.queue, logger: logger)
}
func setNetworkState(network: FakeNetworkResponse, resetAfter numberOfRequests: Int) {
queue.async {
self.networkState = network
self.rule = .resetAfter(numberOfRequests: numberOfRequests)
self.count = numberOfRequests
}
}
func setNetworkState(network: FakeNetworkResponse) {
queue.async {
self.networkState = network
self.rule = nil
}
}
func setNetworkState(network: FakeNetworkResponse, forHost host: String) {
queue.async {
self.networkState = network
self.rule = .host(name: host)
}
}
override public func execute(_ request: URLRequest, completion callback: ((HTTPURLResponse?, Data?, Error?) -> Void)? = nil) -> (ARTCancellable & NSObjectProtocol)? {
queue.async {
switch self.rule {
case .none:
self.performRequest(state: self.networkState, requestCallback: callback)
case .host(let name):
if request.url?.host == name {
self.performRequest(state: self.networkState, requestCallback: callback)
}
else {
self.performRequest(state: nil, requestCallback: callback)
}
case .resetAfter:
self.count -= 1
self.performRequest(state: self.networkState, requestCallback: callback)
if self.count == 0 {
self.networkState = nil
self.rule = nil
}
else if self.count < 0 {
fatalError("Out of sync")
}
}
}
return nil
}
func performRequest(state: FakeNetworkResponse?, requestCallback: ((HTTPURLResponse?, Data?, Error?) -> Void)? = nil) {
switch state {
case .none:
requestCallback?(nil, nil, nil)
case .noInternet:
requestCallback?(nil, nil, NSError(domain: NSURLErrorDomain, code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]))
case .hostUnreachable:
requestCallback?(nil, nil, NSError(domain: NSURLErrorDomain, code: -1003, userInfo: [NSLocalizedDescriptionKey: "A server with the specified hostname could not be found."]))
case .requestTimeout(let timeout):
self.queue.asyncAfter(deadline: .now() + timeout) {
requestCallback?(nil, nil, NSError(domain: NSURLErrorDomain, code: -1001, userInfo: [NSLocalizedDescriptionKey: "The request timed out."]))
}
case .hostInternalError(let code):
requestCallback?(HTTPURLResponse(url: URL(string: "http://cocoa.test.suite")!, statusCode: code, httpVersion: nil, headerFields: nil), nil, nil)
case .host400BadRequest:
requestCallback?(HTTPURLResponse(url: URL(string: "http://cocoa.test.suite")!, statusCode: 400, httpVersion: nil, headerFields: nil), nil, nil)
}
}
}
struct ErrorSimulator {
let value: Int
let description: String
let serverId = "server-test-suite"
var statusCode: Int = 401
var shouldPerformRequest: Bool = false
mutating func stubResponse(_ url: URL) -> HTTPURLResponse? {
return HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: [
"Content-Length": String(stubData?.count ?? 0),
"Content-Type": "application/json",
"X-Ably-Errorcode": String(value),
"X-Ably-Errormessage": description,
"X-Ably-Serverid": serverId,
]
)
}
lazy var stubData: Data? = {
let jsonObject = ["error": [
"statusCode": modf(Float(self.value)/100).0, //whole number part
"code": self.value,
"message": self.description,
"serverId": self.serverId,
]
]
return try? JSONSerialization.data(withJSONObject: jsonObject, options: JSONSerialization.WritingOptions.init(rawValue: 0))
}()
}
class MockHTTPExecutor: NSObject, ARTHTTPAuthenticatedExecutor {
fileprivate var errorSimulator: NSError?
var _logger = ARTLog()
var clientOptions = ARTClientOptions()
var encoder = ARTJsonLikeEncoder()
var requests: [URLRequest] = []
func logger() -> ARTLog {
return _logger
}
func options() -> ARTClientOptions {
return self.clientOptions
}
func defaultEncoder() -> ARTEncoder {
return self.encoder
}
func execute(_ request: NSMutableURLRequest, withAuthOption authOption: ARTAuthentication, completion callback: @escaping (HTTPURLResponse?, Data?, Error?) -> Void) -> (ARTCancellable & NSObjectProtocol)? {
self.requests.append(request as URLRequest)
if let simulatedError = errorSimulator, var _ = request.url {
defer { errorSimulator = nil }
callback(nil, nil, simulatedError)
return nil
}
callback(HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: ["X-Ably-HTTPExecutor": "MockHTTPExecutor"]), nil, nil)
return nil
}
func execute(_ request: URLRequest, completion callback: ((HTTPURLResponse?, Data?, Error?) -> Void)? = nil) -> (ARTCancellable & NSObjectProtocol)? {
self.requests.append(request)
if let simulatedError = errorSimulator, var _ = request.url {
defer { errorSimulator = nil }
callback?(nil, nil, simulatedError)
return nil
}
callback?(HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: ["X-Ably-HTTPExecutor": "MockHTTPExecutor"]), nil, nil)
return nil
}
func simulateIncomingErrorOnNextRequest(_ error: NSError) {
errorSimulator = error
}
func reset() {
requests.removeAll()
}
}
/// Records each request and response for test purpose.
class TestProxyHTTPExecutor: NSObject, ARTHTTPExecutor {
typealias HTTPExecutorCallback = (HTTPURLResponse?, Data?, Error?) -> Void
private(set) var http: ARTHttp
private var _logger: ARTLog!
private var errorSimulator: ErrorSimulator?
private var _requests: [URLRequest] = []
var requests: [URLRequest] {
var result: [URLRequest] = []
http.queue.sync {
result = self._requests
}
return result
}
private var _responses: [HTTPURLResponse] = []
var responses: [HTTPURLResponse] {
var result: [HTTPURLResponse] = []
http.queue.sync {
result = self._responses
}
return result
}
private var callbackBeforeRequest: ((URLRequest) -> Void)?
private var callbackAfterRequest: ((URLRequest) -> Void)?
private var callbackProcessingDataResponse: ((Data?) -> Data)?
init(_ logger: ARTLog) {
self._logger = logger
self.http = ARTHttp(AblyTests.queue, logger: logger)
}
init(http: ARTHttp, logger: ARTLog) {
self._logger = logger
self.http = http
}
func logger() -> ARTLog {
return self._logger
}
public func setHTTP(http: ARTHttp) {
self.http.queue.async {
self.http = http
}
}
func setListenerAfterRequest(_ callback: ((URLRequest) -> Void)?) {
http.queue.sync {
self.callbackAfterRequest = callback
}
}
func setListenerBeforeRequest(_ callback: ((URLRequest) -> Void)?) {
http.queue.sync {
self.callbackBeforeRequest = callback
}
}
func setListenerProcessingDataResponse(_ callback: ((Data?) -> Data)?) {
http.queue.sync {
self.callbackProcessingDataResponse = callback
}
}
public func execute(_ request: URLRequest, completion callback: HTTPExecutorCallback? = nil) -> (ARTCancellable & NSObjectProtocol)? {
self._requests.append(request)
if let performEvent = callbackBeforeRequest {
DispatchQueue.main.async {
performEvent(request)
}
}
if var simulatedError = errorSimulator, let requestURL = request.url {
defer {
errorSimulator = nil
}
if simulatedError.shouldPerformRequest {
return http.execute(request, completion: { response, data, error in
callback?(simulatedError.stubResponse(requestURL), simulatedError.stubData, nil)
})
}
else {
callback?(simulatedError.stubResponse(requestURL), simulatedError.stubData, nil)
return nil
}
}
let task = http.execute(request, completion: { response, data, error in
if let httpResponse = response {
DispatchQueue.main.async {
self._responses.append(httpResponse)
}
}
if let performEvent = self.callbackProcessingDataResponse {
DispatchQueue.main.async { [weak self] in
let result = performEvent(data)
self?.http.queue.async {
callback?(response, result, error)
}
}
}
else {
callback?(response, data, error)
}
})
if let performEvent = callbackAfterRequest {
DispatchQueue.main.async {
performEvent(request)
}
}
return task
}
func simulateIncomingServerErrorOnNextRequest(_ errorValue: Int, description: String) {
http.queue.sync {
errorSimulator = ErrorSimulator(value: errorValue, description: description, statusCode: 401, shouldPerformRequest: false, stubData: nil)
}
}
func simulateIncomingServerErrorOnNextRequest(_ error: ErrorSimulator) {
http.queue.sync {
errorSimulator = error
}
}
func simulateIncomingPayloadOnNextRequest(_ data: Data) {
http.queue.sync {
errorSimulator = ErrorSimulator(value: 0, description: "", statusCode: 200, shouldPerformRequest: false, stubData: data)
}
}
}
/// Records each message for test purpose.
class TestProxyTransport: ARTWebSocketTransport {
/// This will affect all WebSocketTransport instances.
/// Set it to nil after the test ends.
static var fakeNetworkResponse: FakeNetworkResponse?
static var networkConnectEvent: ((ARTRealtimeTransport, URL) -> Void)?
fileprivate(set) var lastUrl: URL?
private var _protocolMessagesReceived: [ARTProtocolMessage] = []
var protocolMessagesReceived: [ARTProtocolMessage] {
var result: [ARTProtocolMessage] = []
queue.sync {
result = self._protocolMessagesReceived
}
return result
}
private var _protocolMessagesSent: [ARTProtocolMessage] = []
var protocolMessagesSent: [ARTProtocolMessage] {
var result: [ARTProtocolMessage] = []
queue.sync {
result = self._protocolMessagesSent
}
return result
}
private var _protocolMessagesSentIgnored: [ARTProtocolMessage] = []
var protocolMessagesSentIgnored: [ARTProtocolMessage] {
var result: [ARTProtocolMessage] = []
queue.sync {
result = self._protocolMessagesSentIgnored
}
return result
}
fileprivate(set) var rawDataSent = [Data]()
fileprivate(set) var rawDataReceived = [Data]()
private var replacingAcksWithNacks: ARTErrorInfo?
var ignoreWebSocket = false
var ignoreSends = false
var actionsIgnored = [ARTProtocolMessageAction]()
var queue: DispatchQueue {
return websocket?.delegateDispatchQueue ?? AblyTests.queue
}
private var callbackBeforeProcessingIncomingMessage: ((ARTProtocolMessage) -> Void)?
private var callbackAfterProcessingIncomingMessage: ((ARTProtocolMessage) -> Void)?
private var callbackBeforeProcessingOutgoingMessage: ((ARTProtocolMessage) -> Void)?
private var callbackBeforeIncomingMessageModifier: ((ARTProtocolMessage) -> ARTProtocolMessage)?
private var callbackAfterIncomingMessageModifier: ((ARTProtocolMessage) -> ARTProtocolMessage)?
func setListenerBeforeProcessingIncomingMessage(_ callback: ((ARTProtocolMessage) -> Void)?) {
queue.sync {
self.callbackBeforeProcessingIncomingMessage = callback
}
}
func setListenerAfterProcessingIncomingMessage(_ callback: ((ARTProtocolMessage) -> Void)?) {
queue.sync {
self.callbackAfterProcessingIncomingMessage = callback
}
}
func setListenerBeforeProcessingOutgoingMessage(_ callback: ((ARTProtocolMessage) -> Void)?) {
queue.sync {
self.callbackBeforeProcessingOutgoingMessage = callback
}
}
/// The modifier will be used in the internal queue.
func setBeforeIncomingMessageModifier(_ callback: ((ARTProtocolMessage) -> ARTProtocolMessage)?) {
self.callbackBeforeIncomingMessageModifier = callback
}
/// The modifier will be used in the internal queue.
func setAfterIncomingMessageModifier(_ callback: ((ARTProtocolMessage) -> ARTProtocolMessage)?) {
self.callbackAfterIncomingMessageModifier = callback
}
func enableReplaceAcksWithNacks(with errorInfo: ARTErrorInfo) {
queue.sync {
self.replacingAcksWithNacks = errorInfo
}
}
func disableReplaceAcksWithNacks() {
queue.sync {
self.replacingAcksWithNacks = nil
}
}
// MARK: ARTWebSocket
override func connect(withKey key: String) {
if let fakeResponse = TestProxyTransport.fakeNetworkResponse {
setupFakeNetworkResponse(fakeResponse)
}
super.connect(withKey: key)
performNetworkConnectEvent()
}
override func connect(withToken token: String) {
if let fakeResponse = TestProxyTransport.fakeNetworkResponse {
setupFakeNetworkResponse(fakeResponse)
}
super.connect(withToken: token)
performNetworkConnectEvent()
}
private func setupFakeNetworkResponse(_ networkResponse: FakeNetworkResponse) {
var hook: AspectToken?
hook = ARTSRWebSocket.testSuite_replaceClassMethod(#selector(ARTSRWebSocket.open)) {
if TestProxyTransport.fakeNetworkResponse == nil {
return
}
func performFakeConnectionError(_ secondsForDelay: TimeInterval, error: ARTRealtimeTransportError) {
self.queue.asyncAfter(deadline: .now() + secondsForDelay) {
self.delegate?.realtimeTransportFailed(self, withError: error)
hook?.remove()
}
}
guard let url = self.lastUrl else {
fatalError("MockNetworkResponse: lastUrl should not be nil")
}
switch networkResponse {
case .noInternet,
.hostUnreachable,
.hostInternalError,
.host400BadRequest:
performFakeConnectionError(0.1, error: networkResponse.transportError(for: url))
case .requestTimeout(let timeout):
performFakeConnectionError(0.1 + timeout, error: networkResponse.transportError(for: url))
}
}
}
private func performNetworkConnectEvent() {
guard let networkConnectEventHandler = TestProxyTransport.networkConnectEvent else {
return
}
if let lastUrl = self.lastUrl {
networkConnectEventHandler(self, lastUrl)
}
else {
queue.asyncAfter(deadline: .now() + 0.1) {
// Repeat until `lastUrl` is assigned.
self.performNetworkConnectEvent()
}
}
}
override func setupWebSocket(_ params: [String: URLQueryItem], with options: ARTClientOptions, resumeKey: String?, connectionSerial: NSNumber?) -> URL {
let url = super.setupWebSocket(params, with: options, resumeKey: resumeKey, connectionSerial: connectionSerial)
lastUrl = url
return url
}
func send(_ message: ARTProtocolMessage) {
let data = try! encoder.encode(message)
send(data, withSource: message)
}
@discardableResult
override func send(_ data: Data, withSource decodedObject: Any?) -> Bool {
if let networkAnswer = TestProxyTransport.fakeNetworkResponse, let ws = self.websocket {
// Ignore it because it should fake a failure.
self.webSocket(ws, didFailWithError: networkAnswer.error)
return false
}
if let msg = decodedObject as? ARTProtocolMessage {
if ignoreSends {
_protocolMessagesSentIgnored.append(msg)
return false
}
_protocolMessagesSent.append(msg)
if let performEvent = callbackBeforeProcessingOutgoingMessage {
DispatchQueue.main.async {
performEvent(msg)
}
}
}
rawDataSent.append(data)
return super.send(data, withSource: decodedObject)
}
override func receive(_ original: ARTProtocolMessage) {
if original.action == .ack || original.action == .presence {
if let error = replacingAcksWithNacks {
original.action = .nack
original.error = error
}
}
_protocolMessagesReceived.append(original)
if actionsIgnored.contains(original.action) {
return
}
if let performEvent = callbackBeforeProcessingIncomingMessage {
DispatchQueue.main.async {
performEvent(original)
}
}
var msg = original
if let performEvent = callbackBeforeIncomingMessageModifier {
msg = performEvent(original)
}
super.receive(msg)
if let performEvent = callbackAfterIncomingMessageModifier {
msg = performEvent(msg)
}
if let performEvent = callbackAfterProcessingIncomingMessage {
DispatchQueue.main.async {
performEvent(msg)
}
}
}
override func receive(with data: Data) -> ARTProtocolMessage? {
rawDataReceived.append(data)
return super.receive(with: data)
}
override func webSocketDidOpen(_ webSocket: ARTWebSocket) {
if !ignoreWebSocket {
super.webSocketDidOpen(webSocket)
}
}
override func webSocket(_ webSocket: ARTWebSocket, didFailWithError error: Error) {
if !ignoreWebSocket {
super.webSocket(webSocket, didFailWithError: error)
}
}
override func webSocket(_ webSocket: ARTWebSocket, didReceiveMessage message: Any?) {
if let networkAnswer = TestProxyTransport.fakeNetworkResponse, let ws = self.websocket {
// Ignore it because it should fake a failure.
self.webSocket(ws, didFailWithError: networkAnswer.error)
return
}
if !ignoreWebSocket {
super.webSocket(webSocket, didReceiveMessage: message as Any)
}
}
override func webSocket(_ webSocket: ARTWebSocket, didCloseWithCode code: Int, reason: String?, wasClean: Bool) {
if !ignoreWebSocket {
super.webSocket(webSocket, didCloseWithCode: code, reason: reason, wasClean: wasClean)
}
}
// MARK: Helpers
func simulateTransportSuccess(clientId: String? = nil) {
self.ignoreWebSocket = true
let msg = ARTProtocolMessage()
msg.action = .connected
msg.connectionId = "x-xxxxxxxx"
msg.connectionKey = "xxxxxxx-xxxxxxxxxxxxxx-xxxxxxxx"
msg.connectionSerial = -1
msg.connectionDetails = ARTConnectionDetails(clientId: clientId, connectionKey: "a8c10!t-3D0O4ejwTdvLkl-b33a8c10", maxMessageSize: 16384, maxFrameSize: 262144, maxInboundRate: 250, connectionStateTtl: 60, serverId: "testServerId", maxIdleInterval: 15000)
super.receive(msg)
}
}
// MARK: - Extensions
extension Sequence where Iterator.Element == Data {
func toMsgPackArray<T>() -> [T] {
let msgPackEncoder = ARTMsgPackEncoder()
return map({ try! msgPackEncoder.decode($0) as! T })
}
}
func + <K,V> (left: Dictionary<K,V>, right: Dictionary<K,V>?) -> Dictionary<K,V> {
guard let right = right else { return left }
return left.reduce(right) {
var new = $0 as [K:V]
new.updateValue($1.1, forKey: $1.0)
return new
}
}
func += <K,V> (left: inout Dictionary<K,V>, right: Dictionary<K,V>?) {
guard let right = right else { return }
right.forEach { key, value in
left.updateValue(value, forKey: key)
}
}
extension Collection {
/// Returns the element at the specified index iff it is within bounds, otherwise nil.
public func at(_ i: Index) -> Iterator.Element? {
return (i >= startIndex && i < endIndex) ? self[i] : nil
}
}
extension Dictionary {
/// Returns the element at the specified index iff it is within bounds, otherwise nil.
public func at(_ key: Key) -> Iterator.Element? {
guard let index = index(forKey: key) else {
return nil
}
return at(index)
}
}
extension ARTMessage {
open override func isEqual(_ object: Any?) -> Bool {
if let other = object as? ARTMessage {
return self.name == other.name &&
self.encoding == other.encoding &&
self.data as! NSObject == other.data as! NSObject
}
return super.isEqual(object)
}
}
extension NSObject {
var toBase64: String {
return (try? JSONSerialization.data(withJSONObject: self, options: JSONSerialization.WritingOptions(rawValue: 0)).base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))) ?? ""
}
}
extension Data {
var toBase64: String {
return self.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
}
var toUTF8String: String {
return String(data: self, encoding: .utf8)!
}
var bytes: [UInt8]{
return [UInt8](self)
}
var hexString: String {
var result = ""
for byte in bytes {
result += String(format: "%02x", UInt(byte))
}
return result.uppercased()
}
}
extension NSData {
var hexString: String {
var result = ""
var bytes = [UInt8](repeating: 0, count: length)
getBytes(&bytes, length: length)
for byte in bytes {
result += String(format: "%02x", UInt(byte))
}
return result.uppercased()
}
}
extension JSON {
var asArray: NSArray? {
return object as? NSArray
}
var asDictionary: NSDictionary? {
return object as? NSDictionary
}
}
extension NSRegularExpression {
class func match(_ value: String?, pattern: String) -> Bool {
guard let value = value else {
return false
}
let options = NSRegularExpression.Options()
let regex = try! NSRegularExpression(pattern: pattern, options: options)
let range = NSMakeRange(0, value.lengthOfBytes(using: String.Encoding.utf8))
return regex.rangeOfFirstMatch(in: value, options: [], range: range).location != NSNotFound
}
class func extract(_ value: String?, pattern: String) -> String? {
guard let value = value else {
return nil
}
let options = NSRegularExpression.Options()
let regex = try! NSRegularExpression(pattern: pattern, options: options)
let range = NSMakeRange(0, value.lengthOfBytes(using: String.Encoding.utf8))
let result = regex.firstMatch(in: value, options: [], range: range)
guard let textRange = result?.range(at: 0) else { return nil }
let convertedRange = value.index(value.startIndex, offsetBy: textRange.location)..<value.index(value.startIndex, offsetBy: textRange.location+textRange.length)
return String(value[convertedRange.lowerBound..<convertedRange.upperBound])
}
}
extension String {
func replace(_ value: String, withString string: String) -> String {
return self.replacingOccurrences(of: value, with: string, options: NSString.CompareOptions.literal, range: nil)
}
}
extension ARTRealtime {
func simulateLostConnectionAndState() {
//1. Abruptly disconnect
//2. Change the `Connection#id` and `Connection#key` before the client
// library attempts to reconnect and resume the connection
self.connection.internal.setId("lost")
self.connection.internal.setKey("xxxxx!xxxxxxx-xxxxxxxx-xxxxxxxx")
self.internal.onDisconnected()
}
func simulateSuspended(beforeSuspension beforeSuspensionCallback: @escaping (_ done: @escaping () -> ()) -> Void) {
waitUntil(timeout: testTimeout) { done in
self.connection.once(.disconnected) { _ in
beforeSuspensionCallback(done)
self.internal.onSuspended()
}
self.internal.onDisconnected()
}
}
func simulateNoInternetConnection() {
guard let reachability = self.internal.reachability as? TestReachability else {
fatalError("Expected test reachability")
}
AblyTests.queue.async {
TestProxyTransport.fakeNetworkResponse = .noInternet
reachability.simulate(false)
}
}
func simulateRestoreInternetConnection(after seconds: TimeInterval? = nil) {
guard let reachability = self.internal.reachability as? TestReachability else {
fatalError("Expected test reachability")
}
AblyTests.queue.asyncAfter(deadline: .now() + (seconds ?? 0)) {
TestProxyTransport.fakeNetworkResponse = nil
reachability.simulate(true)
}
}
func overrideConnectionStateTTL(_ ttl: TimeInterval) -> HookToken {
return self.internal.testSuite_injectIntoMethod(before: NSSelectorFromString("connectionStateTtl")) {
self.internal.connectionStateTtl = ttl
}
}
func dispose() {
let names = self.channels.map({ ($0 as! ARTRealtimeChannel).name })
for name in names {
self.channels.release(name)
}
self.connection.off()
}
}
extension ARTWebSocketTransport {
func simulateIncomingNormalClose() {
let CLOSE_NORMAL = 1000
self.setState(ARTRealtimeTransportState.closing)
let webSocketDelegate = self as ARTWebSocketDelegate
webSocketDelegate.webSocket(self.websocket!, didCloseWithCode: CLOSE_NORMAL, reason: "", wasClean: true)
}
func simulateIncomingAbruptlyClose() {
let CLOSE_ABNORMAL = 1006
let webSocketDelegate = self as ARTWebSocketDelegate
webSocketDelegate.webSocket(self.websocket!, didCloseWithCode: CLOSE_ABNORMAL, reason: "connection was closed abnormally", wasClean: false)
}
func simulateIncomingError() {
let error = NSError(domain: ARTAblyErrorDomain, code: 0, userInfo: [NSLocalizedDescriptionKey:"Fail test"])
let webSocketDelegate = self as ARTWebSocketDelegate
webSocketDelegate.webSocket(self.websocket!, didFailWithError: error)
}
}
extension ARTAuthInternal {
func testSuite_forceTokenToExpire(_ file: StaticString = #file, line: UInt = #line) {
guard let tokenDetails = self.tokenDetails else {
XCTFail("TokenDetails is nil", file: file, line: line)
return
}
self.setTokenDetails(ARTTokenDetails(
token: tokenDetails.token,
expires: Date().addingTimeInterval(-1.0),
issued: Date().addingTimeInterval(-1.0),
capability: tokenDetails.capability,
clientId: tokenDetails.clientId
)
)
}
}
extension ARTPresenceMessage {
convenience init(clientId: String, action: ARTPresenceAction, connectionId: String, id: String, timestamp: Date = Date()) {
self.init()
self.action = action
self.clientId = clientId
self.connectionId = connectionId
self.id = id
self.timestamp = timestamp
}
}
extension ARTMessage {
convenience init(id: String, name: String? = nil, data: Any) {
self.init(name: name, data: data)
self.id = id
}
}
extension ARTRealtimeConnectionState : CustomStringConvertible {
public var description : String {
return ARTRealtimeConnectionStateToStr(self)
}
}
extension ARTRealtimeConnectionEvent : CustomStringConvertible {
public var description : String {
return ARTRealtimeConnectionEventToStr(self)
}
}
extension ARTProtocolMessageAction : CustomStringConvertible {
public var description : String {
return ARTProtocolMessageActionToStr(self)
}
}
extension ARTRealtimeChannelState : CustomStringConvertible {
public var description : String {
return ARTRealtimeChannelStateToStr(self)
}
}
extension ARTChannelEvent : CustomStringConvertible {
public var description : String {
return ARTChannelEventToStr(self)
}
}
extension ARTPresenceAction : CustomStringConvertible {
public var description : String {
return ARTPresenceActionToStr(self)
}
}
// MARK: - Custom Nimble Matchers
/// A Nimble matcher that succeeds when two dates are quite the same.
public func beCloseTo(_ expectedValue: Date) -> Predicate<Date> {
let errorMessage = "be close to <\(expectedValue)> (within 0.5)"
return Predicate.simple(errorMessage) { actualExpression in
guard let actualValue = try actualExpression.evaluate() else {
return .fail
}
if abs(actualValue.timeIntervalSince1970 - expectedValue.timeIntervalSince1970) < 0.5 {
return .matches
}
return .doesNotMatch
}
}
/// A Nimble matcher that succeeds when a param exists.
public func haveParam(_ key: String, withValue expectedValue: String? = nil) -> Predicate<String> {
let errorMessage = "param <\(key)=\(expectedValue ?? "nil")> exists"
return Predicate.simple(errorMessage) { actualExpression in
guard let actualValue = try actualExpression.evaluate() else {
return .fail
}
let queryItems = actualValue.components(separatedBy: "&")
for item in queryItems {
let param = item.components(separatedBy: "=")
if let currentKey = param.first, let currentValue = param.last, currentKey == key && currentValue == expectedValue {
return .matches
}
}
return .doesNotMatch
}
}
/// A Nimble matcher that succeeds when a param value starts with a particular string.
public func haveParam(_ key: String, hasPrefix expectedValue: String) -> Predicate<String> {
let errorMessage = "param <\(key)> has prefix \(expectedValue)"
return Predicate.simple(errorMessage) { actualExpression in
guard let actualValue = try actualExpression.evaluate() else {
return .fail
}
let queryItems = actualValue.components(separatedBy: "&")
for item in queryItems {
let param = item.components(separatedBy: "=")
if let currentKey = param.first, let currentValue = param.last, currentKey == key && currentValue.hasPrefix(expectedValue) {
return .matches
}
}
return .doesNotMatch
}
}
// http://stackoverflow.com/a/26502285/818420
extension String {
/// Create `NSData` from hexadecimal string representation
///
/// This takes a hexadecimal representation and creates a `NSData` object. Note, if the string has any spaces or non-hex characters (e.g. starts with '<' and with a '>'), those are ignored and only hex characters are processed.
///
/// - returns: Data represented by this hexadecimal string.
func dataFromHexadecimalString() -> Data? {
let data = NSMutableData(capacity: self.count / 2)
let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive)
regex.enumerateMatches(in: self, options: [], range: NSMakeRange(0, self.count)) { match, flags, stop in
let byteString = (self as NSString).substring(with: match!.range)
var num = UInt8(byteString, radix: 16)
data?.append(&num, length: 1)
}
return data as Data?
}
}
@objc class TestReachability : NSObject, ARTReachability {
var host: String?
var callback: ((Bool) -> Void)?
var queue: DispatchQueue
required init(logger: ARTLog, queue: DispatchQueue) {
self.queue = queue
}
func listen(forHost host: String, callback: @escaping (Bool) -> Void) {
self.host = host
self.callback = callback
}
func off() {
self.host = nil
self.callback = nil
}
func simulate(_ reachable: Bool) {
self.queue.async {
self.callback!(reachable)
}
}
}
extension HTTPURLResponse {
/**
This is broken since Swift 3. The access is now case-sensitive.
Regression: HTTPURLResponse allHeaderFields is now case-sensitive
https://bugs.swift.org/browse/SR-2429
- Returns: A dictionary containing all the HTTP header fields of the
receiver.
*/
var objc_allHeaderFields: NSDictionary {
// Disables bridging and calls the Objective-C implementation
//of the private NSDictionary subclass in CFNetwork directly
return allHeaderFields as NSDictionary
}
/**
Don't use 'allHeaderFields' property.
It's not case-insensitive.
Please use `value(forHTTPHeaderField:)` method.
- Warning: Don't use 'allHeaderFields' property. See discussion.
*/
@available(*, deprecated, message: "Don't use 'allHeaderFields'. It's not case-insensitive. Please use 'value(forHTTPHeaderField:)' method")
open var _allHeaderFields: [AnyHashable : Any] { return [:] }
/**
The value which corresponds to the given header
field. Note that, in keeping with the HTTP RFC, HTTP header field
names are case-insensitive.
- Parameter field: the header field name to use for the lookup (case-insensitive).
*/
func value(forHTTPHeaderField field: String) -> String? {
return objc_allHeaderFields.object(forKey: field) as? String
}
}
extension ARTHTTPPaginatedResponse {
var headers: NSDictionary {
return response.objc_allHeaderFields
}
}
protocol ARTHasInternal {
associatedtype Internal
func unwrapAsync(_: @escaping (Internal) -> ())
}
extension ARTRealtime: ARTHasInternal {
typealias Internal = ARTRealtimeInternal
func unwrapAsync(_ use: @escaping (Internal) -> ()) {
self.internalAsync(use)
}
}
extension DispatchTimeInterval {
/// Convert dispatch time interval to older style time interval for use with XCTest APIs.
func toTimeInterval() -> TimeInterval {
// Based on: https://stackoverflow.com/a/47716381/392847
switch self {
case .seconds(let value):
return Double(value)
case .milliseconds(let value):
return Double(value) * 0.001
case .microseconds(let value):
return Double(value) * 0.000001
case .nanoseconds(let value):
return Double(value) * 0.000000001
case .never:
return Double.greatestFiniteMagnitude;
@unknown default:
fatalError("Unhandled DispatchTimeInterval unit.")
}
}
/// Return a new dispatch time interval computed from this one, multipled by the supplied amount.
func multiplied(by multiplier: Double) -> DispatchTimeInterval {
switch self {
case .seconds(let value):
return .seconds(Int(Double(value) * multiplier))
case .milliseconds(let value):
return .milliseconds(Int(Double(value) * multiplier))
case .microseconds(let value):
return .microseconds(Int(Double(value) * multiplier))
case .nanoseconds(let value):
return .nanoseconds(Int(Double(value) * multiplier))
case .never:
return .never
@unknown default:
fatalError("Unhandled DispatchTimeInterval unit.")
}
}
/// Return a new dispatch time interval computed from this one, incremented by the supplied amount, to no less than millisecond precision.
func incremented(by interval: TimeInterval) -> DispatchTimeInterval {
// interval is a TimeInterval which is a Double which is in SECONDS
switch self {
case .seconds(let value):
// rounding to millisecond precision, which is fine for the purposes of our test needs
return .milliseconds(Int(1000.0 * (Double(value) + interval)))
case .milliseconds(let value):
let millisecondIncrement = interval * 1000.0
return .milliseconds(Int(Double(value) + millisecondIncrement))
case .microseconds(let value):
let microsecondIncrement = interval * 1000000.0
return .microseconds(Int(Double(value) + microsecondIncrement))
case .nanoseconds(let value):
let nanosecondIncrement = interval * 1000000000.0
return .nanoseconds(Int(Double(value) + nanosecondIncrement))
case .never:
return .never
@unknown default:
fatalError("Unhandled DispatchTimeInterval unit.")
}
}
}
extension ARTErrorCode {
var intValue: NSInteger {
return NSInteger(rawValue)
}
}