4503 lines
221 KiB
Swift
4503 lines
221 KiB
Swift
import Ably
|
||
import Ably.Private
|
||
import Nimble
|
||
import Quick
|
||
import Aspects
|
||
|
||
class Auth : QuickSpec {
|
||
override func spec() {
|
||
|
||
struct ExpectedTokenParams {
|
||
static let clientId = "client_from_params"
|
||
static let ttl = 1.0
|
||
static let capability = "{\"cansubscribe:*\":[\"subscribe\"]}"
|
||
}
|
||
|
||
var testHTTPExecutor: TestProxyHTTPExecutor!
|
||
|
||
describe("Basic") {
|
||
|
||
// RSA1
|
||
it("should work over HTTPS only") {
|
||
let clientOptions = AblyTests.setupOptions(AblyTests.jsonRestOptions)
|
||
clientOptions.tls = false
|
||
|
||
expect{ ARTRest(options: clientOptions) }.to(raiseException())
|
||
}
|
||
|
||
// RSA11
|
||
it("should send the API key in the Authorization header") {
|
||
let options = AblyTests.setupOptions(AblyTests.jsonRestOptions)
|
||
let client = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
client.internal.httpExecutor = testHTTPExecutor
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.channels.get("test").publish(nil, data: "message") { error in
|
||
done()
|
||
}
|
||
}
|
||
|
||
let key64 = "\(client.internal.options.key!)"
|
||
.data(using: .utf8)!
|
||
.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0))
|
||
|
||
let expectedAuthorization = "Basic \(key64)"
|
||
|
||
guard let request = testHTTPExecutor.requests.first else {
|
||
fail("No request found")
|
||
return
|
||
}
|
||
|
||
let authorization = request.allHTTPHeaderFields?["Authorization"]
|
||
|
||
expect(authorization).to(equal(expectedAuthorization))
|
||
}
|
||
|
||
// RSA2
|
||
it("should be default when an API key is set") {
|
||
let client = ARTRest(options: ARTClientOptions(key: "fake:key"))
|
||
|
||
expect(client.auth.internal.method).to(equal(ARTAuthMethod.basic))
|
||
}
|
||
}
|
||
|
||
describe("Token") {
|
||
|
||
// RSA3
|
||
context("token auth") {
|
||
// RSA3a
|
||
it("should work over HTTP") {
|
||
let options = AblyTests.clientOptions(requestToken: true)
|
||
options.tls = false
|
||
let clientHTTP = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
clientHTTP.internal.httpExecutor = testHTTPExecutor
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
clientHTTP.channels.get("test").publish(nil, data: "message") { error in
|
||
done()
|
||
}
|
||
}
|
||
|
||
guard let request = testHTTPExecutor.requests.first else {
|
||
fail("No request found")
|
||
return
|
||
}
|
||
guard let url = request.url else {
|
||
fail("Request is invalid")
|
||
return
|
||
}
|
||
expect(url.scheme).to(equal("http"), description: "No HTTP support")
|
||
}
|
||
|
||
it("should work over HTTPS") {
|
||
let options = AblyTests.clientOptions(requestToken: true)
|
||
options.tls = true
|
||
let clientHTTPS = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
clientHTTPS.internal.httpExecutor = testHTTPExecutor
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
clientHTTPS.channels.get("test").publish(nil, data: "message") { error in
|
||
done()
|
||
}
|
||
}
|
||
|
||
guard let request = testHTTPExecutor.requests.first else {
|
||
fail("No request found")
|
||
return
|
||
}
|
||
guard let url = request.url else {
|
||
fail("Request is invalid")
|
||
return
|
||
}
|
||
expect(url.scheme).to(equal("https"), description: "No HTTPS support")
|
||
}
|
||
|
||
// RSA3b
|
||
it("should send the token in the Authorization header") {
|
||
let options = AblyTests.clientOptions()
|
||
options.token = getTestToken()
|
||
|
||
let client = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
client.internal.httpExecutor = testHTTPExecutor
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.channels.get("test").publish(nil, data: "message") { error in
|
||
done()
|
||
}
|
||
}
|
||
|
||
guard let currentToken = client.internal.options.token else {
|
||
fail("No access token")
|
||
return
|
||
}
|
||
|
||
let expectedAuthorization = "Bearer \(currentToken)"
|
||
|
||
guard let request = testHTTPExecutor.requests.first else {
|
||
fail("No request found")
|
||
return
|
||
}
|
||
|
||
let authorization = request.allHTTPHeaderFields?["Authorization"]
|
||
|
||
expect(authorization).to(equal(expectedAuthorization))
|
||
}
|
||
|
||
// RSA3c
|
||
it("should send the token in the Authorization header") {
|
||
let options = AblyTests.clientOptions()
|
||
options.token = getTestToken()
|
||
options.autoConnect = false
|
||
|
||
let client = ARTRealtime(options: options)
|
||
defer { client.dispose(); client.close() }
|
||
client.internal.setTransport(TestProxyTransport.self)
|
||
client.connect()
|
||
|
||
if let transport = client.internal.transport as? TestProxyTransport, let query = transport.lastUrl?.query {
|
||
expect(query).to(haveParam("accessToken", withValue: client.auth.tokenDetails?.token ?? ""))
|
||
}
|
||
else {
|
||
XCTFail("MockTransport is not working")
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA4
|
||
context("authentication method") {
|
||
for (caseName, caseSetter) in AblyTests.authTokenCases {
|
||
it("should be default auth method when \(caseName) is set") {
|
||
let options = ARTClientOptions()
|
||
caseSetter(options)
|
||
|
||
let client = ARTRest(options: options)
|
||
|
||
expect(client.auth.internal.method).to(equal(ARTAuthMethod.token))
|
||
}
|
||
}
|
||
|
||
// RSA4a
|
||
it("should indicate an error and not retry the request when the server responds with a token error and there is no way to renew the token") {
|
||
let options = AblyTests.clientOptions()
|
||
options.token = getTestToken()
|
||
|
||
let rest = ARTRest(options: options)
|
||
// No means to renew the token is provided
|
||
expect(rest.internal.options.key).to(beNil())
|
||
expect(rest.internal.options.authCallback).to(beNil())
|
||
expect(rest.internal.options.authUrl).to(beNil())
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
rest.internal.httpExecutor = testHTTPExecutor
|
||
|
||
let channel = rest.channels.get("test")
|
||
|
||
testHTTPExecutor.simulateIncomingServerErrorOnNextRequest(ARTErrorCode.tokenRevoked.intValue, description: "token revoked")
|
||
waitUntil(timeout: testTimeout) { done in
|
||
channel.publish("message", data: nil) { error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(UInt(error.code)).to(equal(ARTState.requestTokenFailed.rawValue))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA4a
|
||
it("should transition the connection to the FAILED state when the server responds with a token error and there is no way to renew the token") {
|
||
let options = AblyTests.clientOptions()
|
||
options.tokenDetails = getTestTokenDetails(ttl: 0.1)
|
||
options.autoConnect = false
|
||
|
||
// Token will expire, expecting 40142
|
||
waitUntil(timeout: testTimeout) { done in
|
||
delay(0.2) { done() }
|
||
}
|
||
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
// No means to renew the token is provided
|
||
expect(realtime.internal.options.key).to(beNil())
|
||
expect(realtime.internal.options.authCallback).to(beNil())
|
||
expect(realtime.internal.options.authUrl).to(beNil())
|
||
realtime.internal.setTransport(TestProxyTransport.self)
|
||
|
||
let channel = realtime.channels.get("test")
|
||
|
||
waitUntil(timeout: testTimeout.multiplied(by: 2)) { done in
|
||
realtime.connect()
|
||
channel.publish("message", data: nil) { error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(error.code).to(equal(ARTErrorCode.tokenExpired.intValue))
|
||
expect(realtime.connection.state).to(equal(ARTRealtimeConnectionState.failed))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA4b
|
||
it("on token error, reissues token and retries REST requests") {
|
||
var authCallbackCalled = 0
|
||
|
||
let options = AblyTests.commonAppSetup()
|
||
options.authCallback = { _, callback in
|
||
authCallbackCalled += 1
|
||
getTestTokenDetails { token, err in
|
||
callback(token, err)
|
||
}
|
||
}
|
||
|
||
let rest = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
rest.internal.httpExecutor = testHTTPExecutor
|
||
|
||
let channel = rest.channels.get("test")
|
||
|
||
testHTTPExecutor.simulateIncomingServerErrorOnNextRequest(ARTErrorCode.tokenRevoked.intValue, description: "token revoked")
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
channel.publish("message", data: nil) { error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
// First request and a second attempt
|
||
expect(testHTTPExecutor.requests).to(haveCount(2))
|
||
|
||
// First token issue, and then reissue on token error.
|
||
expect(authCallbackCalled).to(equal(2))
|
||
}
|
||
|
||
// RSA4b
|
||
it("in REST, if the token creation failed or the subsequent request with the new token failed due to a token error, then the request should result in an error") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.useTokenAuth = true
|
||
|
||
let rest = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
rest.internal.httpExecutor = testHTTPExecutor
|
||
|
||
let channel = rest.channels.get("test")
|
||
|
||
testHTTPExecutor.setListenerAfterRequest({ _ in
|
||
testHTTPExecutor.simulateIncomingServerErrorOnNextRequest(ARTErrorCode.tokenRevoked.intValue, description: "token revoked")
|
||
})
|
||
|
||
testHTTPExecutor.simulateIncomingServerErrorOnNextRequest(ARTErrorCode.tokenRevoked.intValue, description: "token revoked")
|
||
waitUntil(timeout: testTimeout) { done in
|
||
channel.publish("message", data: nil) { error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(error.code).to(equal(ARTErrorCode.tokenRevoked.intValue))
|
||
done()
|
||
}
|
||
}
|
||
|
||
// First request and a second attempt
|
||
expect(testHTTPExecutor.requests).to(haveCount(2))
|
||
}
|
||
|
||
// RSA4b
|
||
it("in Realtime, if the token creation failed then the connection should move to the DISCONNECTED state and reports the error") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.authCallback = { tokenParams, completion in
|
||
completion(nil, NSError(domain: NSURLErrorDomain, code: -1003, userInfo: [NSLocalizedDescriptionKey: "A server with the specified hostname could not be found."]))
|
||
}
|
||
options.autoConnect = false
|
||
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.failed) { _ in
|
||
fail("Should not reach Failed state"); done(); return
|
||
}
|
||
realtime.connection.once(.disconnected) { stateChange in
|
||
guard let errorInfo = stateChange.reason else {
|
||
fail("ErrorInfo is nil"); done(); return
|
||
}
|
||
expect(errorInfo.message).to(contain("server with the specified hostname could not be found"))
|
||
done()
|
||
}
|
||
realtime.connect()
|
||
}
|
||
}
|
||
|
||
// RSA4b
|
||
it("in Realtime, if the connection fails due to a terminal token error, then the connection should move to the FAILED state and reports the error") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.authCallback = { tokenParams, completion in
|
||
getTestToken() { token in
|
||
let invalidToken = String(token.reversed())
|
||
completion(invalidToken as ARTTokenDetailsCompatible?, nil)
|
||
}
|
||
}
|
||
options.autoConnect = false
|
||
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.failed) { stateChange in
|
||
guard let errorInfo = stateChange.reason else {
|
||
fail("ErrorInfo is nil"); done(); return
|
||
}
|
||
expect(errorInfo.message).to(contain("No application found with id"))
|
||
done()
|
||
}
|
||
realtime.connection.once(.disconnected) { _ in
|
||
fail("Should not reach Disconnected state"); done(); return
|
||
}
|
||
realtime.connect()
|
||
}
|
||
}
|
||
|
||
// RSA4b1
|
||
context("local token validity check") {
|
||
it("should be done if queryTime is true and local time is in sync with server") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let testKey = options.key!
|
||
|
||
let tokenDetails = getTestTokenDetails(key: testKey, ttl: 5.0, queryTime: true)
|
||
|
||
options.queryTime = true
|
||
options.tokenDetails = tokenDetails
|
||
options.key = nil
|
||
|
||
let rest = ARTRest(options: options)
|
||
let proxyHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
|
||
// Sync server time offset
|
||
let authOptions = ARTAuthOptions(key: testKey)
|
||
authOptions.queryTime = true
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(nil, options: authOptions, callback: { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenRequest).toNot(beNil())
|
||
done()
|
||
})
|
||
}
|
||
|
||
// Let the token expire
|
||
waitUntil(timeout: testTimeout) { done in
|
||
delay(5.0) {
|
||
done()
|
||
}
|
||
}
|
||
|
||
expect(rest.auth.internal.timeOffset).toNot(beNil())
|
||
|
||
rest.internal.httpExecutor = proxyHTTPExecutor
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.channels.get("foo").history { _, error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect((error ).code).to(equal(Int(ARTState.requestTokenFailed.rawValue)))
|
||
expect(error.message).to(contain("no means to renew the token is provided"))
|
||
|
||
expect(proxyHTTPExecutor.requests.count).to(equal(0))
|
||
done()
|
||
}
|
||
}
|
||
|
||
expect(rest.auth.tokenDetails).toNot(beNil())
|
||
}
|
||
|
||
it("should NOT be done if queryTime is false and local time is NOT in sync with server") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let testKey = options.key!
|
||
|
||
let tokenDetails = getTestTokenDetails(key: testKey, ttl: 5.0, queryTime: true)
|
||
|
||
options.queryTime = false
|
||
options.tokenDetails = tokenDetails
|
||
options.key = nil
|
||
|
||
let rest = ARTRest(options: options)
|
||
let proxyHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
rest.internal.httpExecutor = proxyHTTPExecutor
|
||
|
||
// No server time offset
|
||
rest.auth.internal.clearTimeOffset()
|
||
|
||
// Let the token expire
|
||
waitUntil(timeout: testTimeout) { done in
|
||
delay(5.0) {
|
||
done()
|
||
}
|
||
}
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.channels.get("foo").history { _, error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect((error ).code).to(equal(Int(ARTState.requestTokenFailed.rawValue)))
|
||
expect(error.message).to(contain("no means to renew the token is provided"))
|
||
expect(proxyHTTPExecutor.requests.count).to(equal(1))
|
||
expect(proxyHTTPExecutor.responses.count).to(equal(1))
|
||
guard let response = proxyHTTPExecutor.responses.first else {
|
||
fail("Response is nil"); done(); return
|
||
}
|
||
expect(response.value(forHTTPHeaderField: "X-Ably-Errorcode")).to(equal("\(ARTErrorCode.tokenExpired.intValue)"))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA4d
|
||
it("if a request by a realtime client to an authUrl results in an HTTP 403 the client library should transition to the FAILED state") {
|
||
let options = AblyTests.clientOptions()
|
||
options.autoConnect = false
|
||
options.authUrl = URL(string: "https://echo.ably.io/respondwith?status=403")!
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.failed) { stateChange in
|
||
expect(stateChange.reason?.code).to(equal(ARTErrorCode.authConfiguredProviderFailure.intValue))
|
||
expect(stateChange.reason?.statusCode).to(equal(403))
|
||
done()
|
||
}
|
||
realtime.connect()
|
||
}
|
||
}
|
||
|
||
// RSA4d
|
||
it("if an authCallback results in an HTTP 403 the client library should transition to the FAILED state") {
|
||
let options = AblyTests.clientOptions()
|
||
options.autoConnect = false
|
||
var authCallbackHasBeenInvoked = false
|
||
options.authCallback = { tokenParams, completion in
|
||
authCallbackHasBeenInvoked = true
|
||
completion(nil, ARTErrorInfo(domain: "io.ably.cocoa", code: ARTErrorCode.forbidden.intValue, userInfo: ["ARTErrorInfoStatusCode": 403]))
|
||
}
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.failed) { stateChange in
|
||
expect(authCallbackHasBeenInvoked).to(beTrue())
|
||
expect(stateChange.reason?.code).to(equal(ARTErrorCode.authConfiguredProviderFailure.intValue))
|
||
expect(stateChange.reason?.statusCode).to(equal(403))
|
||
done()
|
||
}
|
||
realtime.connect()
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA14
|
||
context("options") {
|
||
// Cases:
|
||
// - useTokenAuth is specified and thus a key is not provided
|
||
// - authCallback and authUrl are both specified
|
||
let cases: [String: (ARTAuthOptions) -> ()] = [
|
||
"useTokenAuth and no key":{ $0.useTokenAuth = true },
|
||
"authCallback and authUrl":{ $0.authCallback = { params, callback in /*nothing*/ }; $0.authUrl = URL(string: "http://auth.ably.io") }
|
||
]
|
||
|
||
for (caseName, caseSetter) in cases {
|
||
it("should stop client when \(caseName) occurs") {
|
||
let options = ARTClientOptions()
|
||
caseSetter(options)
|
||
|
||
expect{ ARTRest(options: options) }.to(raiseException())
|
||
}
|
||
}
|
||
|
||
// RSA4c
|
||
context("if an attempt by the realtime client library to authenticate is made using the authUrl or authCallback") {
|
||
|
||
context("the request to authUrl fails") {
|
||
|
||
// RSA4c1 & RSA4c2
|
||
it("if the connection is CONNECTING, then the connection attempt should be treated as unsuccessful") {
|
||
let options = AblyTests.clientOptions()
|
||
options.autoConnect = false
|
||
options.authUrl = URL(string: "http://echo.ably.io")!
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.disconnected) { stateChange in
|
||
expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.connecting))
|
||
guard let errorInfo = stateChange.reason else {
|
||
fail("ErrorInfo is nil"); done(); return
|
||
}
|
||
expect(errorInfo.code) == ARTErrorCode.authConfiguredProviderFailure.intValue
|
||
done()
|
||
}
|
||
realtime.connect()
|
||
}
|
||
|
||
guard let errorInfo = realtime.connection.errorReason else {
|
||
fail("ErrorInfo is empty"); return
|
||
}
|
||
expect(errorInfo.code) == ARTErrorCode.authConfiguredProviderFailure.intValue
|
||
expect(errorInfo.message).to(contain("body param is required"))
|
||
}
|
||
|
||
// RSA4c3
|
||
it("if the connection is CONNECTED, then the connection should remain CONNECTED") {
|
||
let token = getTestToken()
|
||
let options = AblyTests.clientOptions()
|
||
options.authUrl = URL(string: "http://echo.ably.io")!
|
||
options.authParams = [URLQueryItem]()
|
||
options.authParams?.append(URLQueryItem(name: "type", value: "text"))
|
||
options.authParams?.append(URLQueryItem(name: "body", value: token))
|
||
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.connected) { stateChange in
|
||
expect(stateChange.reason).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
// Token reauth will fail
|
||
realtime.internal.options.authParams = [URLQueryItem]()
|
||
|
||
// Inject AUTH
|
||
let authMessage = ARTProtocolMessage()
|
||
authMessage.action = ARTProtocolMessageAction.auth
|
||
realtime.internal.transport?.receive(authMessage)
|
||
|
||
expect(realtime.connection.errorReason).toEventuallyNot(beNil(), timeout: testTimeout)
|
||
guard let errorInfo = realtime.connection.errorReason else {
|
||
fail("ErrorInfo is empty"); return
|
||
}
|
||
expect(errorInfo.code) == ARTErrorCode.authConfiguredProviderFailure.intValue
|
||
expect(errorInfo.message).to(contain("body param is required"))
|
||
|
||
expect(realtime.connection.state).to(equal(ARTRealtimeConnectionState.connected))
|
||
}
|
||
}
|
||
|
||
context("the request to authCallback fails") {
|
||
|
||
// RSA4c1 & RSA4c2
|
||
it("if the connection is CONNECTING, then the connection attempt should be treated as unsuccessful") {
|
||
let options = AblyTests.clientOptions()
|
||
options.autoConnect = false
|
||
options.authCallback = { tokenParams, completion in
|
||
completion(nil, NSError(domain: NSURLErrorDomain, code: -1003, userInfo: [NSLocalizedDescriptionKey: "A server with the specified hostname could not be found."]))
|
||
}
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.disconnected) { stateChange in
|
||
expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.connecting))
|
||
guard let errorInfo = stateChange.reason else {
|
||
fail("ErrorInfo is nil"); done(); return
|
||
}
|
||
expect(errorInfo.code) == ARTErrorCode.authConfiguredProviderFailure.intValue
|
||
done()
|
||
}
|
||
realtime.connect()
|
||
}
|
||
|
||
expect(realtime.connection.state).toEventually(equal(ARTRealtimeConnectionState.disconnected), timeout: testTimeout)
|
||
guard let errorInfo = realtime.connection.errorReason else {
|
||
fail("ErrorInfo is empty"); return
|
||
}
|
||
expect(errorInfo.code) == ARTErrorCode.authConfiguredProviderFailure.intValue
|
||
expect(errorInfo.message).to(contain("hostname could not be found"))
|
||
}
|
||
|
||
// RSA4c3
|
||
it("if the connection is CONNECTED, then the connection should remain CONNECTED") {
|
||
let options = AblyTests.clientOptions()
|
||
options.authCallback = { tokenParams, completion in
|
||
getTestTokenDetails(completion: completion)
|
||
}
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.connected) { stateChange in
|
||
expect(stateChange.reason).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
// Token should renew and fail
|
||
realtime.internal.options.authCallback = { tokenParams, completion in
|
||
completion(nil, NSError(domain: NSURLErrorDomain, code: -1003, userInfo: [NSLocalizedDescriptionKey: "A server with the specified hostname could not be found."]))
|
||
}
|
||
|
||
// Inject AUTH
|
||
let authMessage = ARTProtocolMessage()
|
||
authMessage.action = ARTProtocolMessageAction.auth
|
||
realtime.internal.transport?.receive(authMessage)
|
||
|
||
expect(realtime.connection.errorReason).toEventuallyNot(beNil(), timeout: testTimeout)
|
||
guard let errorInfo = realtime.connection.errorReason else {
|
||
fail("ErrorInfo is empty"); return
|
||
}
|
||
expect(errorInfo.code) == ARTErrorCode.authConfiguredProviderFailure.intValue
|
||
expect(errorInfo.message).to(contain("hostname could not be found"))
|
||
|
||
expect(realtime.connection.state).to(equal(ARTRealtimeConnectionState.connected))
|
||
}
|
||
}
|
||
|
||
context("the provided token is in an invalid format") {
|
||
|
||
// RSA4c1 & RSA4c2
|
||
it("if the connection is CONNECTING, then the connection attempt should be treated as unsuccessful") {
|
||
let options = AblyTests.clientOptions()
|
||
options.autoConnect = false
|
||
options.authUrl = URL(string: "http://echo.ably.io")!
|
||
options.authParams = [URLQueryItem]()
|
||
options.authParams?.append(URLQueryItem(name: "type", value: "json"))
|
||
let invalidTokenFormat = "{secret_token:xxx}"
|
||
options.authParams?.append(URLQueryItem(name: "body", value: invalidTokenFormat))
|
||
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.disconnected) { stateChange in
|
||
expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.connecting))
|
||
guard let errorInfo = stateChange.reason else {
|
||
fail("ErrorInfo is nil"); done(); return
|
||
}
|
||
expect(errorInfo.code) == ARTErrorCode.authConfiguredProviderFailure.intValue
|
||
done()
|
||
}
|
||
realtime.connect()
|
||
}
|
||
|
||
guard let errorInfo = realtime.connection.errorReason else {
|
||
fail("ErrorInfo is empty"); return
|
||
}
|
||
expect(errorInfo.code) == ARTErrorCode.authConfiguredProviderFailure.intValue
|
||
expect(errorInfo.message).to(contain("content response cannot be used for token request"))
|
||
|
||
expect(realtime.connection.state).toEventually(equal(ARTRealtimeConnectionState.disconnected), timeout: testTimeout)
|
||
}
|
||
|
||
// RSA4c3
|
||
it("if the connection is CONNECTED, then the connection should remain CONNECTED") {
|
||
let options = AblyTests.clientOptions()
|
||
options.authUrl = URL(string: "http://echo.ably.io")!
|
||
options.authParams = [URLQueryItem]()
|
||
options.authParams?.append(URLQueryItem(name: "type", value: "text"))
|
||
|
||
let token = getTestToken()
|
||
options.authParams?.append(URLQueryItem(name: "body", value: token))
|
||
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.connected) { stateChange in
|
||
expect(stateChange.reason).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
// Token should renew and fail
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.unwrapAsync { realtime in
|
||
realtime.options.authParams = [URLQueryItem]()
|
||
realtime.options.authParams?.append(URLQueryItem(name: "type", value: "json"))
|
||
let invalidTokenFormat = "{secret_token:xxx}"
|
||
realtime.options.authParams?.append(URLQueryItem(name: "body", value: invalidTokenFormat))
|
||
done()
|
||
}
|
||
}
|
||
|
||
realtime.connection.on() { stateChange in
|
||
if stateChange.current != .connected {
|
||
fail("Connection should remain connected")
|
||
}
|
||
}
|
||
|
||
// Inject AUTH
|
||
let authMessage = ARTProtocolMessage()
|
||
authMessage.action = ARTProtocolMessageAction.auth
|
||
realtime.internal.transport?.receive(authMessage)
|
||
|
||
expect(realtime.connection.errorReason).toEventuallyNot(beNil(), timeout: testTimeout)
|
||
guard let errorInfo = realtime.connection.errorReason else {
|
||
fail("ErrorInfo is empty"); return
|
||
}
|
||
expect(errorInfo.code) == ARTErrorCode.authConfiguredProviderFailure.intValue
|
||
expect(errorInfo.message).to(contain("content response cannot be used for token request"))
|
||
|
||
expect(realtime.connection.state).to(equal(ARTRealtimeConnectionState.connected))
|
||
}
|
||
}
|
||
|
||
context("the attempt times out after realtimeRequestTimeout") {
|
||
// RSA4c1 & RSA4c2
|
||
it("if the connection is CONNECTING, then the connection attempt should be treated as unsuccessful") {
|
||
let previousRealtimeRequestTimeout = ARTDefault.realtimeRequestTimeout()
|
||
defer { ARTDefault.setRealtimeRequestTimeout(previousRealtimeRequestTimeout) }
|
||
ARTDefault.setRealtimeRequestTimeout(0.5)
|
||
|
||
let options = AblyTests.clientOptions()
|
||
options.autoConnect = false
|
||
options.authCallback = { tokenParams, completion in
|
||
// Ignore `completion` closure to force a time out
|
||
}
|
||
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.disconnected) { stateChange in
|
||
guard let errorInfo = stateChange.reason else {
|
||
fail("ErrorInfo is nil"); done(); return
|
||
}
|
||
expect(errorInfo.code) == ARTErrorCode.authConfiguredProviderFailure.intValue
|
||
done()
|
||
}
|
||
realtime.connect()
|
||
}
|
||
|
||
guard let errorInfo = realtime.connection.errorReason else {
|
||
fail("ErrorInfo is empty"); return
|
||
}
|
||
expect(errorInfo.code) == ARTErrorCode.authConfiguredProviderFailure.intValue
|
||
expect(errorInfo.message).to(contain("timed out"))
|
||
|
||
expect(realtime.connection.state).toEventually(equal(ARTRealtimeConnectionState.disconnected), timeout: testTimeout)
|
||
}
|
||
|
||
// RSA4c3
|
||
it("if the connection is CONNECTED, then the connection should remain CONNECTED") {
|
||
let options = AblyTests.clientOptions()
|
||
options.autoConnect = false
|
||
options.authCallback = { tokenParams, completion in
|
||
getTestTokenDetails(completion: completion)
|
||
}
|
||
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.connected) { stateChange in
|
||
expect(stateChange.reason).to(beNil())
|
||
done()
|
||
}
|
||
realtime.connect()
|
||
}
|
||
|
||
let previousRealtimeRequestTimeout = ARTDefault.realtimeRequestTimeout()
|
||
defer { ARTDefault.setRealtimeRequestTimeout(previousRealtimeRequestTimeout) }
|
||
ARTDefault.setRealtimeRequestTimeout(0.5)
|
||
|
||
// Token should renew and fail
|
||
realtime.internal.options.authCallback = { tokenParams, completion in
|
||
// Ignore `completion` closure to force a time out
|
||
}
|
||
|
||
// Inject AUTH
|
||
let authMessage = ARTProtocolMessage()
|
||
authMessage.action = ARTProtocolMessageAction.auth
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.unwrapAsync { realtime in
|
||
realtime.transport?.receive(authMessage)
|
||
done()
|
||
}
|
||
}
|
||
|
||
expect(realtime.connection.errorReason).toEventuallyNot(beNil(), timeout: testTimeout)
|
||
guard let errorInfo = realtime.connection.errorReason else {
|
||
fail("ErrorInfo is empty"); return
|
||
}
|
||
expect(errorInfo.code) == ARTErrorCode.authConfiguredProviderFailure.intValue
|
||
expect(errorInfo.message).to(contain("timed out"))
|
||
|
||
expect(realtime.connection.state).to(equal(ARTRealtimeConnectionState.connected))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA15
|
||
context("token auth and clientId") {
|
||
// RSA15a
|
||
context("should check clientId consistency") {
|
||
|
||
it("on rest") {
|
||
let expectedClientId = "client_string"
|
||
let options = AblyTests.commonAppSetup()
|
||
options.useTokenAuth = true
|
||
options.clientId = expectedClientId
|
||
|
||
let client = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
client.internal.httpExecutor = testHTTPExecutor
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
// Token
|
||
client.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(client.auth.internal.method).to(equal(ARTAuthMethod.token))
|
||
guard let tokenDetails = tokenDetails else {
|
||
fail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.clientId).to(equal(expectedClientId))
|
||
done()
|
||
}
|
||
}
|
||
|
||
switch extractBodyAsMsgPack(testHTTPExecutor.requests.first) {
|
||
case .failure(let error):
|
||
XCTFail(error)
|
||
case .success(let httpBody):
|
||
guard let requestedClientId = httpBody.unbox["clientId"] as? String else { XCTFail("No clientId field in HTTPBody"); return }
|
||
expect(requestedClientId).to(equal(expectedClientId))
|
||
}
|
||
}
|
||
|
||
it("on realtime") {
|
||
let expectedClientId = "client_string"
|
||
let options = AblyTests.setupOptions(AblyTests.jsonRestOptions)
|
||
options.clientId = expectedClientId
|
||
options.autoConnect = false
|
||
|
||
let client = ARTRealtime(options: options)
|
||
defer { client.dispose(); client.close() }
|
||
client.internal.setTransport(TestProxyTransport.self)
|
||
client.connect()
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.connection.on { stateChange in
|
||
let state = stateChange.current
|
||
let error = stateChange.reason
|
||
if state == .connected && error == nil {
|
||
let currentChannel = client.channels.get("test")
|
||
currentChannel.subscribe({ message in
|
||
done()
|
||
})
|
||
currentChannel.publish(nil, data: "ping", callback:nil)
|
||
}
|
||
}
|
||
}
|
||
|
||
guard let transport = client.internal.transport as? TestProxyTransport else {
|
||
fail("Transport is nil"); return
|
||
}
|
||
guard let connectedMessage = transport.protocolMessagesReceived.filter({ $0.action == .connected }).last else {
|
||
XCTFail("No CONNECTED protocol action received"); return
|
||
}
|
||
|
||
// CONNECTED ProtocolMessage
|
||
expect(connectedMessage.connectionDetails!.clientId).to(equal(expectedClientId))
|
||
}
|
||
|
||
it("with wildcard") {
|
||
let options = AblyTests.setupOptions(AblyTests.jsonRestOptions)
|
||
options.clientId = "*"
|
||
expect{ ARTRest(options: options) }.to(raiseException())
|
||
expect{ ARTRealtime(options: options) }.to(raiseException())
|
||
}
|
||
}
|
||
|
||
// RSA15b
|
||
it("should permit to be unauthenticated") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.clientId = nil
|
||
|
||
let clientBasic = ARTRest(options: options)
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
// Basic
|
||
clientBasic.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(clientBasic.auth.clientId).to(beNil())
|
||
options.tokenDetails = tokenDetails
|
||
done()
|
||
}
|
||
}
|
||
|
||
let clientToken = ARTRest(options: options)
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
// Last TokenDetails
|
||
clientToken.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(clientToken.auth.clientId).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA15c
|
||
context("Incompatible client") {
|
||
|
||
it("with Realtime, it should change the connection state to FAILED and emit an error") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let wrongTokenDetails = getTestTokenDetails(clientId: "wrong")
|
||
|
||
options.clientId = "john"
|
||
options.autoConnect = false
|
||
options.authCallback = { tokenParams, completion in
|
||
completion(wrongTokenDetails, nil)
|
||
}
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.failed) { stateChange in
|
||
expect(stateChange.reason?.code).to(equal(ARTErrorCode.invalidCredentials.intValue))
|
||
done()
|
||
}
|
||
realtime.connect()
|
||
}
|
||
}
|
||
|
||
it("with Rest, it should result in an appropriate error response") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.clientId = "john"
|
||
let rest = ARTRest(options: options)
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken(ARTTokenParams(clientId: "wrong"), with: nil) { tokenDetails, error in
|
||
let error = error as! ARTErrorInfo
|
||
expect(error.code).to(equal(ARTErrorCode.incompatibleCredentials.intValue))
|
||
expect(tokenDetails).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA5
|
||
it("TTL should default to be omitted") {
|
||
let tokenParams = ARTTokenParams()
|
||
expect(tokenParams.ttl).to(beNil())
|
||
}
|
||
|
||
it("should URL query be correctly encoded") {
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.capability = "{\"*\":[\"*\"]}"
|
||
|
||
if #available(iOS 10.0, *) {
|
||
let dateFormatter = ISO8601DateFormatter()
|
||
tokenParams.timestamp = dateFormatter.date(from: "2016-10-08T22:31:00Z")
|
||
}
|
||
else {
|
||
let dateFormatter = DateFormatter()
|
||
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm zzz"
|
||
tokenParams.timestamp = dateFormatter.date(from: "2016/10/08 22:31 GMT")
|
||
}
|
||
|
||
let options = ARTClientOptions()
|
||
options.authUrl = URL(string: "https://ably-test-suite.io")
|
||
let rest = ARTRest(options: options)
|
||
let request = rest.auth.internal.buildRequest(options, with: tokenParams)
|
||
|
||
if let query = request.url?.query {
|
||
expect(query).to(haveParam("capability", withValue: "%7B%22*%22:%5B%22*%22%5D%7D"))
|
||
expect(query).to(haveParam("timestamp", withValue: "1475965860000"))
|
||
}
|
||
else {
|
||
fail("URL is empty")
|
||
}
|
||
}
|
||
|
||
// RSA6
|
||
it("should omit capability field if it is not specified") {
|
||
let tokenParams = ARTTokenParams()
|
||
expect(tokenParams.capability).to(beNil())
|
||
|
||
let options = AblyTests.setupOptions(AblyTests.jsonRestOptions)
|
||
let rest = ARTRest(options: options)
|
||
let testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
rest.internal.httpExecutor = testHTTPExecutor
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
// Token
|
||
rest.auth.requestToken(tokenParams, with: options) { tokenDetails, error in
|
||
if let e = error {
|
||
fail(e.localizedDescription); done(); return
|
||
}
|
||
expect(tokenParams.capability).to(beNil())
|
||
expect(tokenDetails?.capability).to(equal("{\"*\":[\"*\"]}"))
|
||
done()
|
||
}
|
||
}
|
||
|
||
switch extractBodyAsMsgPack(testHTTPExecutor.requests.first) {
|
||
case .failure(let error):
|
||
fail(error)
|
||
case .success(let httpBody):
|
||
expect(httpBody.unbox["capability"]).to(beNil())
|
||
}
|
||
}
|
||
|
||
// RSA6
|
||
it("should add capability field if the user specifies it") {
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.capability = "{\"*\":[\"*\"]}"
|
||
|
||
let options = AblyTests.setupOptions(AblyTests.jsonRestOptions)
|
||
let rest = ARTRest(options: options)
|
||
let testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
rest.internal.httpExecutor = testHTTPExecutor
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
// Token
|
||
rest.auth.requestToken(tokenParams, with: options) { tokenDetails, error in
|
||
if let e = error {
|
||
fail(e.localizedDescription); done(); return
|
||
}
|
||
expect(tokenDetails?.capability).to(equal(tokenParams.capability))
|
||
done()
|
||
}
|
||
}
|
||
|
||
switch extractBodyAsMsgPack(testHTTPExecutor.requests.first) {
|
||
case .failure(let error):
|
||
fail(error)
|
||
case .success(let httpBody):
|
||
expect(httpBody.unbox["capability"] as? String).to(equal("{\"*\":[\"*\"]}"))
|
||
}
|
||
}
|
||
|
||
// RSA7
|
||
context("clientId and authenticated clients") {
|
||
|
||
// RSA7a1
|
||
it("should not pass clientId with published message") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.clientId = "mary"
|
||
let rest = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
rest.internal.httpExecutor = testHTTPExecutor
|
||
let channel = rest.channels.get("RSA7a1")
|
||
waitUntil(timeout: testTimeout) { done in
|
||
channel.publish("foo", data: nil) { error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
switch extractBodyAsMsgPack(testHTTPExecutor.requests.last) {
|
||
case .failure(let error):
|
||
fail(error)
|
||
case .success(let httpBody):
|
||
let message = httpBody.unbox
|
||
expect(message["clientId"]).to(beNil())
|
||
expect(message["name"] as? String).to(equal("foo"))
|
||
}
|
||
}
|
||
|
||
// RSA7a2
|
||
it("should obtain a token if clientId is assigned") {
|
||
let options = AblyTests.setupOptions(AblyTests.jsonRestOptions)
|
||
options.clientId = "client_string"
|
||
|
||
let client = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
client.internal.httpExecutor = testHTTPExecutor
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.channels.get("test").publish(nil, data: "message") { error in
|
||
if let e = error {
|
||
XCTFail((e ).localizedDescription)
|
||
}
|
||
done()
|
||
}
|
||
}
|
||
|
||
let authorization = testHTTPExecutor.requests.last?.allHTTPHeaderFields?["Authorization"] ?? ""
|
||
|
||
expect(authorization).toNot(equal(""))
|
||
}
|
||
|
||
// RSA7a3
|
||
it("should convenience clientId return a string") {
|
||
let clientOptions = AblyTests.setupOptions(AblyTests.jsonRestOptions)
|
||
clientOptions.clientId = "String"
|
||
|
||
expect(ARTRest(options: clientOptions).internal.options.clientId).to(equal("String"))
|
||
}
|
||
|
||
// RSA7a4
|
||
it("ClientOptions#clientId takes precendence when a clientId value is provided in both ClientOptions#clientId and ClientOptions#defaultTokenParams") {
|
||
let options = AblyTests.clientOptions()
|
||
options.clientId = "john"
|
||
options.authCallback = { tokenParams, completion in
|
||
expect(tokenParams.clientId).to(equal(options.clientId))
|
||
getTestToken(clientId: tokenParams.clientId) { token in
|
||
completion(token as ARTTokenDetailsCompatible?, nil)
|
||
}
|
||
}
|
||
options.defaultTokenParams = ARTTokenParams(clientId: "tester")
|
||
let client = ARTRest(options: options)
|
||
let channel = client.channels.get("test")
|
||
|
||
expect(client.auth.clientId).to(equal("john"))
|
||
waitUntil(timeout: testTimeout) { done in
|
||
channel.publish(nil, data: "message") { error in
|
||
expect(error).to(beNil())
|
||
channel.history() { paginatedResult, error in
|
||
guard let result = paginatedResult else {
|
||
fail("PaginatedResult is empty"); done(); return
|
||
}
|
||
guard let message = result.items.first else {
|
||
fail("First message does not exist"); done(); return
|
||
}
|
||
expect(message.clientId).to(equal("john"))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA12
|
||
context("Auth#clientId attribute is null") {
|
||
|
||
// RSA12a
|
||
it("identity should be anonymous for all operations") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.autoConnect = false
|
||
let realtime = AblyTests.newRealtime(options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
expect(realtime.auth.clientId).to(beNil())
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.connected) { stateChange in
|
||
expect(stateChange.reason).to(beNil())
|
||
expect(realtime.auth.clientId).to(beNil())
|
||
done()
|
||
}
|
||
realtime.connect()
|
||
|
||
let transport = realtime.internal.transport as! TestProxyTransport
|
||
transport.setBeforeIncomingMessageModifier({ message in
|
||
if message.action == .connected {
|
||
if let details = message.connectionDetails {
|
||
details.clientId = nil
|
||
}
|
||
}
|
||
return message
|
||
})
|
||
}
|
||
}
|
||
|
||
// RSA12b
|
||
it("identity may change and become identified") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.autoConnect = false
|
||
options.token = getTestToken(clientId: "tester")
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
expect(realtime.auth.clientId).to(beNil())
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.connecting) { stateChange in
|
||
expect(stateChange.reason).to(beNil())
|
||
expect(realtime.auth.clientId).to(beNil())
|
||
}
|
||
realtime.connection.once(.connected) { stateChange in
|
||
expect(stateChange.reason).to(beNil())
|
||
expect(realtime.auth.clientId).to(equal("tester"))
|
||
done()
|
||
}
|
||
realtime.connect()
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// RSA7b
|
||
context("auth.clientId not null") {
|
||
// RSA7b1
|
||
it("when clientId attribute is assigned on client options") {
|
||
let clientOptions = AblyTests.setupOptions(AblyTests.jsonRestOptions)
|
||
clientOptions.clientId = "Exist"
|
||
|
||
expect(ARTRest(options: clientOptions).auth.clientId).to(equal("Exist"))
|
||
}
|
||
|
||
// RSA7b2
|
||
it("when tokenRequest or tokenDetails has clientId not null or wildcard string") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.clientId = "client_string"
|
||
options.useTokenAuth = true
|
||
|
||
let client = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
client.internal.httpExecutor = testHTTPExecutor
|
||
|
||
// TokenDetails
|
||
waitUntil(timeout: testTimeout) { done in
|
||
// Token
|
||
client.auth.authorize(nil, options: nil) { token, error in
|
||
expect(error).to(beNil())
|
||
expect(client.auth.internal.method).to(equal(ARTAuthMethod.token))
|
||
expect(client.auth.clientId).to(equal(options.clientId))
|
||
done()
|
||
}
|
||
}
|
||
|
||
// TokenRequest
|
||
switch extractBodyAsMsgPack(testHTTPExecutor.requests.last) {
|
||
case .failure(let error):
|
||
XCTFail(error)
|
||
case .success(let httpBody):
|
||
guard let requestedClientId = httpBody.unbox["clientId"] as? String else { XCTFail("No clientId field in HTTPBody"); return }
|
||
expect(client.auth.clientId).to(equal(requestedClientId))
|
||
}
|
||
}
|
||
|
||
// RSA7b3
|
||
it("should CONNECTED ProtocolMessages contain a clientId") {
|
||
let options = AblyTests.clientOptions()
|
||
options.token = getTestToken(clientId: "john")
|
||
expect(options.clientId).to(beNil())
|
||
options.autoConnect = false
|
||
let realtime = AblyTests.newRealtime(options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.connected) { stateChange in
|
||
expect(stateChange.reason).to(beNil())
|
||
expect(realtime.auth.clientId).to(equal("john"))
|
||
|
||
let transport = realtime.internal.transport as! TestProxyTransport
|
||
let connectedProtocolMessage = transport.protocolMessagesReceived.filter{ $0.action == .connected }[0]
|
||
expect(connectedProtocolMessage.connectionDetails!.clientId).to(equal("john"))
|
||
done()
|
||
}
|
||
realtime.connect()
|
||
}
|
||
}
|
||
|
||
// RSA7b4
|
||
it("client does not have an identity when a wildcard string '*' is present") {
|
||
let options = AblyTests.clientOptions()
|
||
options.token = getTestToken(clientId: "*")
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.on(.connected) { _ in
|
||
expect(realtime.auth.clientId).to(equal("*"))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// RSA7c
|
||
it("should clientId be null or string") {
|
||
let clientOptions = AblyTests.setupOptions(AblyTests.jsonRestOptions)
|
||
clientOptions.clientId = "*"
|
||
|
||
expect{ ARTRest(options: clientOptions) }.to(raiseException())
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA8
|
||
describe("requestToken") {
|
||
context("arguments") {
|
||
// RSA8e
|
||
it("should not merge with the configured params and options but instead replace all corresponding values, even when @null@") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.clientId = "сlientId"
|
||
let rest = ARTRest(options: options)
|
||
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.ttl = 2000
|
||
tokenParams.capability = "{\"cansubscribe:*\":[\"subscribe\"]}"
|
||
|
||
let precedenceOptions = AblyTests.commonAppSetup()
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken(tokenParams, with: precedenceOptions) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
expect(tokenDetails!.capability).to(equal("{\"cansubscribe:*\":[\"subscribe\"]}"))
|
||
expect(tokenDetails!.clientId).to(beNil())
|
||
expect(tokenDetails!.expires!.timeIntervalSince1970 - tokenDetails!.issued!.timeIntervalSince1970).to(equal(tokenParams.ttl as? Double))
|
||
done()
|
||
}
|
||
}
|
||
|
||
let options2 = AblyTests.commonAppSetup()
|
||
options2.clientId = nil
|
||
let rest2 = ARTRest(options: options2)
|
||
|
||
let precedenceOptions2 = AblyTests.commonAppSetup()
|
||
precedenceOptions2.clientId = nil
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest2.auth.requestToken(nil, with: precedenceOptions2) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("tokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.clientId).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA8e
|
||
it("should use configured defaults if the object arguments are omitted") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.clientId = "tester"
|
||
let rest = ARTRest(options: options)
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken(nil, with: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
expect(tokenDetails!.capability).to(equal("{\"*\":[\"*\"]}"))
|
||
expect(tokenDetails!.clientId).to(equal("tester"))
|
||
done()
|
||
}
|
||
}
|
||
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.ttl = 2000
|
||
tokenParams.capability = "{\"cansubscribe:*\":[\"subscribe\"]}"
|
||
tokenParams.clientId = nil
|
||
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.key = options.key
|
||
|
||
// Provide TokenParams and Options
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken(tokenParams, with: authOptions) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
expect(tokenDetails!.capability).to(equal("{\"cansubscribe:*\":[\"subscribe\"]}"))
|
||
expect(tokenDetails!.clientId).to(beNil())
|
||
expect(tokenDetails!.expires!.timeIntervalSince1970 - tokenDetails!.issued!.timeIntervalSince1970).to(equal(tokenParams.ttl as? Double))
|
||
done()
|
||
}
|
||
}
|
||
|
||
// Provide TokenParams as null
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken(nil, with: authOptions) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
expect(tokenDetails!.capability).to(equal("{\"*\":[\"*\"]}"))
|
||
expect(tokenDetails!.clientId).to(equal("tester"))
|
||
expect(tokenDetails!.expires!.timeIntervalSince1970 - tokenDetails!.issued!.timeIntervalSince1970).to(equal(ARTDefault.ttl()))
|
||
done()
|
||
}
|
||
}
|
||
|
||
// Omit arguments
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
expect(tokenDetails!.capability).to(equal("{\"*\":[\"*\"]}"))
|
||
expect(tokenDetails!.clientId).to(equal("tester"))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA8c
|
||
context("authUrl") {
|
||
|
||
it("query will provide a token string") {
|
||
let testToken = getTestToken()
|
||
|
||
let options = AblyTests.clientOptions()
|
||
options.authUrl = URL(string: "http://echo.ably.io")
|
||
expect(options.authUrl).toNot(beNil())
|
||
// Plain text
|
||
options.authParams = [URLQueryItem]()
|
||
options.authParams!.append(URLQueryItem(name: "type", value: "text"))
|
||
options.authParams!.append(URLQueryItem(name: "body", value: testToken))
|
||
|
||
let rest = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
rest.internal.httpExecutor = testHTTPExecutor
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken(nil, with: nil, callback: { tokenDetails, error in
|
||
expect(testHTTPExecutor.requests.last?.url?.host).to(equal("echo.ably.io"))
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
expect(tokenDetails?.token).to(equal(testToken))
|
||
done()
|
||
})
|
||
}
|
||
}
|
||
|
||
it("query will provide a TokenDetails") {
|
||
guard let testTokenDetails = getTestTokenDetails(clientId: "tester") else {
|
||
fail("TokenDetails is empty")
|
||
return
|
||
}
|
||
|
||
let encoder = ARTJsonLikeEncoder()
|
||
encoder.delegate = ARTJsonEncoder()
|
||
guard let jsonTokenDetails = try? encoder.encode(testTokenDetails) else {
|
||
fail("Invalid TokenDetails")
|
||
return
|
||
}
|
||
|
||
let options = ARTClientOptions()
|
||
options.authUrl = URL(string: "http://echo.ably.io")
|
||
expect(options.authUrl).toNot(beNil())
|
||
// JSON with TokenDetails
|
||
options.authParams = [URLQueryItem]()
|
||
options.authParams?.append(URLQueryItem(name: "type", value: "json"))
|
||
options.authParams?.append(URLQueryItem(name: "body", value: jsonTokenDetails.toUTF8String))
|
||
|
||
let rest = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
rest.internal.httpExecutor = testHTTPExecutor
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken(nil, with: nil, callback: { tokenDetails, error in
|
||
expect(testHTTPExecutor.requests.last?.url?.host).to(equal("echo.ably.io"))
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
expect(tokenDetails?.clientId) == testTokenDetails.clientId
|
||
expect(tokenDetails?.capability) == testTokenDetails.capability
|
||
expect(tokenDetails?.issued).toNot(beNil())
|
||
expect(tokenDetails?.expires).toNot(beNil())
|
||
if let issued = tokenDetails?.issued, let testIssued = testTokenDetails.issued {
|
||
expect(issued.compare(testIssued)) == ComparisonResult.orderedSame
|
||
}
|
||
if let expires = tokenDetails?.expires, let testExpires = testTokenDetails.expires {
|
||
expect(expires.compare(testExpires)) == ComparisonResult.orderedSame
|
||
}
|
||
done()
|
||
})
|
||
}
|
||
}
|
||
|
||
it("query will provide a TokenRequest") {
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.capability = "{\"test\":[\"subscribe\"]}"
|
||
|
||
let options = AblyTests.commonAppSetup()
|
||
options.authUrl = URL(string: "http://echo.ably.io")
|
||
expect(options.authUrl).toNot(beNil())
|
||
|
||
var rest = ARTRest(options: options)
|
||
|
||
var tokenRequest: ARTTokenRequest?
|
||
waitUntil(timeout: testTimeout) { done in
|
||
// Sandbox and valid TokenRequest
|
||
rest.auth.createTokenRequest(tokenParams, options: nil, callback: { newTokenRequest, error in
|
||
expect(error).to(beNil())
|
||
tokenRequest = newTokenRequest
|
||
done()
|
||
})
|
||
}
|
||
|
||
guard let testTokenRequest = tokenRequest else {
|
||
fail("TokenRequest is empty")
|
||
return
|
||
}
|
||
|
||
let encoder = ARTJsonLikeEncoder()
|
||
encoder.delegate = ARTJsonEncoder()
|
||
guard let jsonTokenRequest = try? encoder.encode(testTokenRequest) else {
|
||
fail("Invalid TokenRequest")
|
||
return
|
||
}
|
||
|
||
// JSON with TokenRequest
|
||
options.authParams = [URLQueryItem]()
|
||
options.authParams?.append(URLQueryItem(name: "type", value: "json"))
|
||
options.authParams?.append(URLQueryItem(name: "body", value: jsonTokenRequest.toUTF8String))
|
||
|
||
rest = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
rest.internal.httpExecutor = testHTTPExecutor
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken(nil, with: nil, callback: { tokenDetails, error in
|
||
expect(testHTTPExecutor.requests.first?.url?.host).to(equal("echo.ably.io"))
|
||
expect(testHTTPExecutor.requests.last?.url?.host).toNot(equal("echo.ably.io"))
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
fail("TokenDetails is empty"); done()
|
||
return
|
||
}
|
||
expect(tokenDetails.token).toNot(beNil())
|
||
expect(tokenDetails.capability) == tokenParams.capability
|
||
done()
|
||
})
|
||
}
|
||
}
|
||
|
||
context("parameters") {
|
||
// RSA8c1a
|
||
it("should be added to the URL when auth method is GET") {
|
||
let clientOptions = ARTClientOptions()
|
||
clientOptions.authUrl = URL(string: "http://auth.ably.io")
|
||
var authParams = [
|
||
"param1": "value",
|
||
"param2": "value",
|
||
"clientId": "should not be overwritten",
|
||
]
|
||
clientOptions.authParams = authParams.map {
|
||
URLQueryItem(name: $0, value: $1)
|
||
}
|
||
clientOptions.authHeaders = ["X-Header-1": "foo", "X-Header-2": "bar"]
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.clientId = "test"
|
||
|
||
let rest = ARTRest(options: clientOptions)
|
||
let request = rest.auth.internal.buildRequest(clientOptions, with: tokenParams)
|
||
|
||
for (header, expectedValue) in clientOptions.authHeaders! {
|
||
if let value = request.allHTTPHeaderFields?[header] {
|
||
expect(value).to(equal(expectedValue))
|
||
} else {
|
||
fail("Missing header in request: \(header), expected: \(expectedValue)")
|
||
}
|
||
}
|
||
|
||
guard let url = request.url else {
|
||
fail("Request is invalid")
|
||
return
|
||
}
|
||
guard let urlComponents = NSURLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
||
fail("invalid URL: \(url)")
|
||
return
|
||
}
|
||
expect(urlComponents.scheme).to(equal("http"))
|
||
expect(urlComponents.host).to(equal("auth.ably.io"))
|
||
guard let queryItems = urlComponents.queryItems else {
|
||
fail("URL without query: \(url)")
|
||
return
|
||
}
|
||
for queryItem in queryItems {
|
||
if var expectedValue = authParams[queryItem.name] {
|
||
if queryItem.name == "clientId" {
|
||
expectedValue = "test"
|
||
}
|
||
expect(queryItem.value!).to(equal(expectedValue))
|
||
authParams.removeValue(forKey: queryItem.name)
|
||
}
|
||
}
|
||
expect(authParams).to(beEmpty())
|
||
}
|
||
|
||
// RSA8c1b
|
||
it("should added on the body request when auth method is POST") {
|
||
let clientOptions = ARTClientOptions()
|
||
clientOptions.authUrl = URL(string: "http://auth.ably.io")
|
||
clientOptions.authParams = [
|
||
URLQueryItem(name: "identifier", value: "123")
|
||
]
|
||
clientOptions.authMethod = "POST"
|
||
clientOptions.authHeaders = ["X-Header-1": "foo", "X-Header-2": "bar"]
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.ttl = 2000
|
||
tokenParams.capability = "{\"cansubscribe:*\":[\"subscribe\"]}"
|
||
|
||
let rest = ARTRest(options: clientOptions)
|
||
|
||
let request = rest.auth.internal.buildRequest(clientOptions, with: tokenParams)
|
||
|
||
guard let httpBodyData = request.httpBody else {
|
||
fail("Body is missing"); return
|
||
}
|
||
guard let httpBodyString = String(data: httpBodyData, encoding: .utf8) else {
|
||
fail("Body should be a string"); return
|
||
}
|
||
|
||
let expectedFormEncoding = "capability=%7B%22cansubscribe%3A%2A%22%3A%5B%22subscribe%22%5D%7D&identifier=123&ttl=2000"
|
||
|
||
expect(httpBodyString).to(equal(expectedFormEncoding))
|
||
|
||
expect(request.value(forHTTPHeaderField: "Content-Type")).to(equal("application/x-www-form-urlencoded"))
|
||
|
||
expect(request.value(forHTTPHeaderField: "Content-Length")).to(equal("89"))
|
||
|
||
for (header, expectedValue) in clientOptions.authHeaders! {
|
||
if let value = request.value(forHTTPHeaderField: header) {
|
||
expect(value).to(equal(expectedValue))
|
||
} else {
|
||
fail("Missing header in request: \(header), expected: \(expectedValue)")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA8c2
|
||
it("TokenParams should take precedence over any configured authParams when a name conflict occurs") {
|
||
let options = ARTClientOptions()
|
||
options.clientId = "john"
|
||
options.authUrl = URL(string: "http://auth.ably.io")
|
||
options.authMethod = "GET"
|
||
options.authHeaders = ["X-Header-1": "foo1", "X-Header-2": "foo2"]
|
||
let authParams = [
|
||
"key": "secret",
|
||
"clientId": "should be overridden"
|
||
]
|
||
options.authParams = authParams.map { URLQueryItem(name: $0, value: $1) }
|
||
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.clientId = "tester"
|
||
|
||
let client = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
client.internal.httpExecutor = testHTTPExecutor
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.auth.requestToken(tokenParams, with: nil) { tokenDetails, error in
|
||
let query = testHTTPExecutor.requests[0].url!.query
|
||
expect(query).to(haveParam("clientId", withValue: tokenParams.clientId!))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA8c3
|
||
it("should override previously configured parameters") {
|
||
let clientOptions = ARTClientOptions()
|
||
clientOptions.authUrl = URL(string: "http://auth.ably.io")
|
||
let rest = ARTRest(options: clientOptions)
|
||
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.authUrl = URL(string: "http://auth.ably.io")
|
||
authOptions.authParams = [URLQueryItem(name: "ttl", value: "invalid")]
|
||
authOptions.authParams = [URLQueryItem(name: "test", value: "1")]
|
||
let url = rest.auth.internal.buildURL(authOptions, with: ARTTokenParams())
|
||
expect(url.absoluteString).to(contain(URL(string: "http://auth.ably.io")?.absoluteString ?? ""))
|
||
}
|
||
}
|
||
|
||
// RSA8a
|
||
it("implicitly creates a TokenRequest and requests a token") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
|
||
var createTokenRequestMethodWasCalled = false
|
||
|
||
// Adds a block of code after `createTokenRequest` is triggered
|
||
let token = rest.auth.internal.testSuite_injectIntoMethod(after: NSSelectorFromString("_createTokenRequest:options:callback:")) {
|
||
createTokenRequestMethodWasCalled = true
|
||
}
|
||
defer { token.remove() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken(nil, with: nil, callback: { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails?.token).toNot(beEmpty())
|
||
done()
|
||
})
|
||
}
|
||
|
||
expect(createTokenRequestMethodWasCalled).to(beTrue())
|
||
}
|
||
|
||
// RSA8b
|
||
context("should support all TokenParams") {
|
||
|
||
let currentClientId = "client_string"
|
||
|
||
var options: ARTClientOptions!
|
||
var rest: ARTRest!
|
||
|
||
func setupDependencies() {
|
||
if (options == nil) {
|
||
options = AblyTests.commonAppSetup()
|
||
options.clientId = currentClientId
|
||
rest = ARTRest(options: options)
|
||
}
|
||
}
|
||
|
||
it("using defaults") {
|
||
setupDependencies()
|
||
|
||
// Default values
|
||
let defaultTokenParams = ARTTokenParams(clientId: currentClientId)
|
||
defaultTokenParams.ttl = ARTDefault.ttl() as NSNumber // Set by the server.
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken(nil, with: nil, callback: { tokenDetails, error in
|
||
expect(tokenDetails?.clientId).to(equal(defaultTokenParams.clientId))
|
||
expect(defaultTokenParams.capability).to(beNil())
|
||
expect(tokenDetails?.capability).to(equal("{\"*\":[\"*\"]}")) //Ably supplied capabilities of the underlying key
|
||
expect(tokenDetails?.issued).toNot(beNil())
|
||
expect(tokenDetails?.expires).toNot(beNil())
|
||
if let issued = tokenDetails?.issued, let expires = tokenDetails?.expires {
|
||
expect(expires.timeIntervalSince(issued)).to(equal(defaultTokenParams.ttl as? TimeInterval))
|
||
}
|
||
done()
|
||
})
|
||
}
|
||
}
|
||
|
||
it("overriding defaults") {
|
||
setupDependencies()
|
||
|
||
// Custom values
|
||
let expectedTtl = 4800.0
|
||
let expectedCapability = "{\"canpublish:*\":[\"publish\"]}"
|
||
|
||
let tokenParams = ARTTokenParams(clientId: currentClientId)
|
||
tokenParams.ttl = NSNumber(value: expectedTtl)
|
||
tokenParams.capability = expectedCapability
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken(tokenParams, with: nil, callback: { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails?.clientId).to(equal(options.clientId))
|
||
expect(tokenDetails?.capability).to(equal(expectedCapability))
|
||
expect(tokenDetails?.issued).toNot(beNil())
|
||
expect(tokenDetails?.expires).toNot(beNil())
|
||
if let issued = tokenDetails?.issued, let expires = tokenDetails?.expires {
|
||
expect(expires.timeIntervalSince(issued)).to(equal(expectedTtl))
|
||
}
|
||
done()
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA8d
|
||
context("When authCallback option is set, it will invoke the callback") {
|
||
|
||
it("with a token string") {
|
||
let options = AblyTests.clientOptions()
|
||
let expectedTokenParams = ARTTokenParams()
|
||
|
||
options.authCallback = { tokenParams, completion in
|
||
expect(tokenParams.clientId).to(beNil())
|
||
completion("token_string" as ARTTokenDetailsCompatible?, nil)
|
||
}
|
||
let rest = ARTRest(options: options)
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken(expectedTokenParams, with: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails!.token).to(equal("token_string"))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("with a TokenDetails") {
|
||
let expectedTokenParams = ARTTokenParams()
|
||
|
||
let options = AblyTests.clientOptions()
|
||
options.authCallback = { tokenParams, completion in
|
||
expect(tokenParams.clientId).to(beNil())
|
||
completion(ARTTokenDetails(token: "token_from_details"), nil)
|
||
}
|
||
let rest = ARTRest(options: options)
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken(expectedTokenParams, with: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails!.token).to(equal("token_from_details"))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("with a TokenRequest") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let expectedTokenParams = ARTTokenParams()
|
||
expectedTokenParams.clientId = "foo"
|
||
var rest: ARTRest!
|
||
|
||
options.authCallback = { tokenParams, completion in
|
||
expect(tokenParams.clientId).to(beIdenticalTo(expectedTokenParams.clientId))
|
||
rest.auth.createTokenRequest(tokenParams, options: options) { tokenRequest, error in
|
||
completion(tokenRequest, error)
|
||
}
|
||
}
|
||
|
||
rest = ARTRest(options: options)
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken(expectedTokenParams, with: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
fail("tokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.clientId).to(equal(expectedTokenParams.clientId))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA8f1
|
||
it("ensure the message published does not have a clientId") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.token = getTestToken(clientId: nil)
|
||
let rest = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
rest.internal.httpExecutor = testHTTPExecutor
|
||
let channel = rest.channels.get("test")
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
let message = ARTMessage(name: nil, data: "message without an explicit clientId")
|
||
expect(message.clientId).to(beNil())
|
||
channel.publish([message]) { error in
|
||
expect(error).to(beNil())
|
||
switch extractBodyAsMessages(testHTTPExecutor.requests.first) {
|
||
case .failure(let error):
|
||
fail(error)
|
||
case .success(let httpBody):
|
||
expect(httpBody.unbox.first!["clientId"]).to(beNil())
|
||
}
|
||
channel.history { page, error in
|
||
expect(error).to(beNil())
|
||
guard let page = page else {
|
||
fail("Result is empty"); done(); return
|
||
}
|
||
expect(page.items).to(haveCount(1))
|
||
expect((page.items[0] ).clientId).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
expect(rest.auth.clientId).to(beNil())
|
||
}
|
||
|
||
// RSA8f2
|
||
it("ensure that the message is rejected") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.token = getTestToken(clientId: nil)
|
||
let rest = ARTRest(options: options)
|
||
let channel = rest.channels.get("test")
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
let message = ARTMessage(name: nil, data: "message with an explicit clientId", clientId: "john")
|
||
channel.publish([message]) { error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(error.message).to(contain("mismatched clientId"))
|
||
done()
|
||
}
|
||
}
|
||
expect(rest.auth.clientId).to(beNil())
|
||
}
|
||
|
||
// RSA8f3
|
||
it("ensure the message published with a wildcard '*' does not have a clientId") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let rest = ARTRest(options: options)
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(ARTTokenParams(clientId: "*"), options: nil) { _, error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
rest.internal.httpExecutor = testHTTPExecutor
|
||
let channel = rest.channels.get("test")
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
let message = ARTMessage(name: nil, data: "no client")
|
||
expect(message.clientId).to(beNil())
|
||
channel.publish([message]) { error in
|
||
expect(error).to(beNil())
|
||
switch extractBodyAsMessages(testHTTPExecutor.requests.first) {
|
||
case .failure(let error):
|
||
fail(error)
|
||
case .success(let httpBody):
|
||
expect(httpBody.unbox.first!["clientId"]).to(beNil())
|
||
}
|
||
channel.history { page, error in
|
||
guard let page = page else {
|
||
fail("Page is empty"); done(); return
|
||
}
|
||
expect(error).to(beNil())
|
||
expect(page.items).to(haveCount(1))
|
||
expect(page.items[0].clientId).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
expect(rest.auth.clientId).to(equal("*"))
|
||
}
|
||
|
||
// RSA8f4
|
||
it("ensure the message published with a wildcard '*' has the provided clientId") {
|
||
let options = AblyTests.commonAppSetup()
|
||
// Request a token with a wildcard '*' value clientId
|
||
options.token = getTestToken(clientId: "*")
|
||
let rest = ARTRest(options: options)
|
||
let channel = rest.channels.get("test")
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
let message = ARTMessage(name: nil, data: "message with an explicit clientId", clientId: "john")
|
||
channel.publish([message]) { error in
|
||
expect(error).to(beNil())
|
||
channel.history { page, error in
|
||
expect(error).to(beNil())
|
||
guard let page = page else {
|
||
fail("Page is empty"); done(); return
|
||
}
|
||
guard let item = page.items.first else {
|
||
fail("First item does not exist"); done(); return
|
||
}
|
||
expect(item.clientId).to(equal("john"))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
expect(rest.auth.clientId).to(beNil())
|
||
}
|
||
}
|
||
|
||
// RSA9
|
||
describe("createTokenRequest") {
|
||
|
||
// RSA9h
|
||
it("should not merge with the configured params and options but instead replace all corresponding values, even when @null@") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.clientId = "client_string"
|
||
let rest = ARTRest(options: options)
|
||
|
||
let tokenParams = ARTTokenParams()
|
||
let defaultCapability = tokenParams.capability
|
||
expect(defaultCapability).to(beNil())
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(nil, options: nil) { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
XCTFail("tokenRequest is nil"); done(); return
|
||
}
|
||
expect(tokenRequest.clientId).to(equal(options.clientId))
|
||
expect(tokenRequest.ttl).to(beNil())
|
||
expect(tokenRequest.capability).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
tokenParams.ttl = NSNumber(value: ExpectedTokenParams.ttl)
|
||
tokenParams.capability = ExpectedTokenParams.capability
|
||
tokenParams.clientId = nil
|
||
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.queryTime = true
|
||
authOptions.key = options.key
|
||
|
||
let mockServerDate = Date().addingTimeInterval(120)
|
||
rest.auth.internal.testSuite_returnValue(for: NSSelectorFromString("handleServerTime:"), with: mockServerDate)
|
||
|
||
var serverTimeRequestCount = 0
|
||
let hook = rest.internal.testSuite_injectIntoMethod(after: #selector(rest.internal._time(_:))) {
|
||
serverTimeRequestCount += 1
|
||
}
|
||
defer { hook.remove() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(tokenParams, options: authOptions) { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
XCTFail("tokenRequest is nil"); done(); return
|
||
}
|
||
expect(tokenRequest.clientId).to(beNil())
|
||
expect(tokenRequest.timestamp).to(beCloseTo(mockServerDate))
|
||
expect(serverTimeRequestCount) == 1
|
||
expect(tokenRequest.ttl).to(equal(ExpectedTokenParams.ttl as NSNumber))
|
||
expect(tokenRequest.capability).to(equal(ExpectedTokenParams.capability))
|
||
done()
|
||
}
|
||
}
|
||
|
||
tokenParams.clientId = "newClientId"
|
||
tokenParams.ttl = 2000
|
||
tokenParams.capability = "{ \"test:*\":[\"test\"] }"
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(tokenParams, options: authOptions) { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
XCTFail("tokenRequest is nil"); done(); return
|
||
}
|
||
expect(tokenRequest.clientId).to(equal("newClientId"))
|
||
expect(tokenRequest.ttl).to(equal(2000))
|
||
expect(tokenRequest.capability).to(equal("{ \"test:*\":[\"test\"] }"))
|
||
done()
|
||
}
|
||
}
|
||
|
||
tokenParams.clientId = nil
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(tokenParams, options: authOptions) { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
XCTFail("tokenRequest is nil"); done(); return
|
||
}
|
||
expect(tokenRequest.clientId).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("should override defaults if AuthOptions provided") {
|
||
let defaultOptions = AblyTests.commonAppSetup()
|
||
defaultOptions.authCallback = { tokenParams, completion in
|
||
fail("Should not be called")
|
||
}
|
||
|
||
var testTokenRequest: ARTTokenRequest?
|
||
let rest = ARTRest(options: defaultOptions)
|
||
rest.auth.createTokenRequest(nil, options: nil, callback: { tokenRequest, error in
|
||
testTokenRequest = tokenRequest
|
||
})
|
||
expect(testTokenRequest).toEventuallyNot(beNil(), timeout: testTimeout)
|
||
|
||
var customCallbackCalled = false
|
||
let customOptions = ARTAuthOptions()
|
||
customOptions.authCallback = { tokenParams, completion in
|
||
customCallbackCalled = true
|
||
completion(testTokenRequest, nil)
|
||
}
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: customOptions) { _, error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
expect(customCallbackCalled).to(beTrue())
|
||
}
|
||
|
||
it("should use defaults if no AuthOptions is provided") {
|
||
var currentTokenRequest: ARTTokenRequest? = nil
|
||
var callbackCalled = false
|
||
|
||
let defaultOptions = AblyTests.commonAppSetup()
|
||
defaultOptions.authCallback = { tokenParams, completion in
|
||
callbackCalled = true
|
||
guard let tokenRequest = currentTokenRequest else {
|
||
fail("tokenRequest is nil"); return
|
||
}
|
||
completion(tokenRequest, nil)
|
||
}
|
||
|
||
let rest = ARTRest(options: defaultOptions)
|
||
rest.auth.createTokenRequest(nil, options: nil, callback: { tokenRequest, error in
|
||
currentTokenRequest = tokenRequest
|
||
})
|
||
expect(currentTokenRequest).toEventuallyNot(beNil(), timeout: testTimeout)
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { _, error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
expect(callbackCalled).to(beTrue())
|
||
}
|
||
|
||
it("should replace defaults if `nil` option's field passed") {
|
||
let defaultOptions = AblyTests.commonAppSetup()
|
||
let rest = ARTRest(options: defaultOptions)
|
||
|
||
let customOptions = ARTAuthOptions()
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(nil, options: customOptions) { tokenRequest, error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(error.localizedDescription).to(contain("no key provided for signing token requests"))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA9h
|
||
it("should use configured defaults if the object arguments are omitted") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let rest = ARTRest(options: options)
|
||
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.clientId = "tester"
|
||
tokenParams.ttl = 2000
|
||
tokenParams.capability = "{\"foo:*\":[\"publish\"]}"
|
||
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.queryTime = true
|
||
authOptions.key = options.key
|
||
|
||
var serverTimeRequestCount = 0
|
||
let hook = rest.internal.testSuite_injectIntoMethod(after: #selector(rest.internal._time(_:))) {
|
||
serverTimeRequestCount += 1
|
||
}
|
||
defer { hook.remove() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(tokenParams, options: authOptions) { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
XCTFail("TokenRequest is nil"); done(); return
|
||
}
|
||
expect(tokenRequest.clientId) == tokenParams.clientId
|
||
expect(tokenRequest.ttl) == tokenParams.ttl
|
||
expect(tokenRequest.capability) == tokenParams.capability
|
||
done()
|
||
}
|
||
}
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
XCTFail("TokenRequest is nil"); done(); return
|
||
}
|
||
expect(tokenRequest.clientId).to(beNil())
|
||
expect(tokenRequest.ttl).to(beNil())
|
||
expect(tokenRequest.capability).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
expect(serverTimeRequestCount) == 1
|
||
}
|
||
|
||
// RSA9a
|
||
it("should create and sign a TokenRequest") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
let expectedClientId = "client_string"
|
||
let tokenParams = ARTTokenParams(clientId: expectedClientId)
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(tokenParams, options: nil, callback: { tokenRequest, error in
|
||
defer { done() }
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
XCTFail("TokenRequest is nil"); return
|
||
}
|
||
expect(tokenRequest).to(beAnInstanceOf(ARTTokenRequest.self))
|
||
expect(tokenRequest.clientId).to(equal(expectedClientId))
|
||
expect(tokenRequest.mac).toNot(beNil())
|
||
expect(tokenRequest.nonce).toNot(beNil())
|
||
})
|
||
}
|
||
}
|
||
|
||
// RSA9b
|
||
it("should support AuthOptions") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
let auth: ARTAuth = rest.auth
|
||
|
||
let authOptions = ARTAuthOptions(key: "key:secret")
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
auth.createTokenRequest(nil, options: authOptions, callback: { tokenRequest, error in
|
||
defer { done() }
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
XCTFail("TokenRequest is nil"); return
|
||
}
|
||
expect(tokenRequest.keyName).to(equal("key"))
|
||
})
|
||
}
|
||
}
|
||
|
||
// RSA9c
|
||
it("should generate a unique 16+ character nonce if none is provided") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
// First
|
||
rest.auth.createTokenRequest(nil, options: nil, callback: { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest1 = tokenRequest else {
|
||
XCTFail("TokenRequest1 is nil"); done(); return
|
||
}
|
||
expect(tokenRequest1.nonce).to(haveCount(16))
|
||
|
||
// Second
|
||
rest.auth.createTokenRequest(nil, options: nil, callback: { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest2 = tokenRequest else {
|
||
XCTFail("TokenRequest2 is nil"); done(); return
|
||
}
|
||
expect(tokenRequest2.nonce).to(haveCount(16))
|
||
|
||
// Uniqueness
|
||
expect(tokenRequest1.nonce).toNot(equal(tokenRequest2.nonce))
|
||
done()
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
// RSA9d
|
||
context("should generate a timestamp") {
|
||
|
||
it("from current time if not provided") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(nil, options: nil, callback: { tokenRequest, error in
|
||
defer { done() }
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
XCTFail("TokenRequest is nil"); return
|
||
}
|
||
expect(tokenRequest.timestamp).to(beCloseTo(Date(), within: 1.0))
|
||
})
|
||
}
|
||
}
|
||
|
||
it("will retrieve the server time if queryTime is true") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
|
||
var serverTimeRequestWasMade = false
|
||
let block: @convention(block) (AspectInfo) -> Void = { _ in
|
||
serverTimeRequestWasMade = true
|
||
}
|
||
|
||
let hook = ARTRestInternal.aspect_hook(rest.internal)
|
||
// Adds a block of code after `time` is triggered
|
||
let _ = try? hook(#selector(ARTRestInternal._time(_:)), .positionBefore, unsafeBitCast(block, to: ARTRestInternal.self))
|
||
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.queryTime = true
|
||
authOptions.key = AblyTests.commonAppSetup().key
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(nil, options: authOptions, callback: { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
XCTFail("tokenRequest is nil"); done(); return
|
||
}
|
||
expect(tokenRequest.timestamp).toNot(beNil())
|
||
expect(serverTimeRequestWasMade).to(beTrue())
|
||
done()
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA9e
|
||
context("TTL") {
|
||
|
||
it("should be optional") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(nil, options: nil, callback: { tokenRequest, error in
|
||
defer { done() }
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
XCTFail("TokenRequest is nil"); return
|
||
}
|
||
//In Seconds because TTL property is a NSTimeInterval but further it does the conversion to milliseconds
|
||
expect(tokenRequest.ttl).to(beNil())
|
||
})
|
||
}
|
||
|
||
let tokenParams = ARTTokenParams()
|
||
expect(tokenParams.ttl).to(beNil())
|
||
|
||
let expectedTtl = TimeInterval(10)
|
||
tokenParams.ttl = NSNumber(value: expectedTtl)
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(tokenParams, options: nil, callback: { tokenRequest, error in
|
||
defer { done() }
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
XCTFail("TokenRequest is nil"); return
|
||
}
|
||
expect(tokenRequest.ttl as? TimeInterval).to(equal(expectedTtl))
|
||
})
|
||
}
|
||
}
|
||
|
||
it("should be specified in milliseconds") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
|
||
let params = ARTTokenParams()
|
||
params.ttl = NSNumber(value: 42)
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(params, options: nil, callback: { tokenRequest, error in
|
||
defer { done() }
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
XCTFail("TokenRequest is nil"); return
|
||
}
|
||
expect(tokenRequest.ttl as? TimeInterval).to(equal(42))
|
||
|
||
// Check if the encoder changes the TTL to milliseconds
|
||
let encoder = rest.internal.defaultEncoder as! ARTJsonLikeEncoder
|
||
let data = try! encoder.encode(tokenRequest)
|
||
let jsonObject = (try! encoder.delegate!.decode(data)) as! NSDictionary
|
||
let ttl = jsonObject["ttl"] as! NSNumber
|
||
expect(ttl as? Int64).to(equal(42 * 1000))
|
||
|
||
// Make sure it comes back the same.
|
||
let decoded = try! encoder.decodeTokenRequest(data)
|
||
expect(decoded.ttl as? TimeInterval).to(equal(42))
|
||
})
|
||
}
|
||
}
|
||
|
||
it("should be valid to request a token for 24 hours") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
let tokenParams = ARTTokenParams()
|
||
let dayInSeconds = TimeInterval(24 * 60 * 60)
|
||
tokenParams.ttl = dayInSeconds as NSNumber
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.requestToken(tokenParams, with: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.expires!.timeIntervalSince(tokenDetails.issued!)).to(beCloseTo(dayInSeconds))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// RSA9f
|
||
it("should provide capability has json text") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.capability = "{ - }"
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(tokenParams, options: nil, callback: { tokenRequest, error in
|
||
defer { done() }
|
||
guard let error = error else {
|
||
XCTFail("Error is nil"); return
|
||
}
|
||
expect(error.localizedDescription).to(contain("Capability"))
|
||
expect(tokenRequest?.capability).to(beNil())
|
||
})
|
||
}
|
||
|
||
let expectedCapability = "{ \"cansubscribe:*\":[\"subscribe\"] }"
|
||
tokenParams.capability = expectedCapability
|
||
|
||
rest.auth.createTokenRequest(tokenParams, options: nil, callback: { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
XCTFail("TokenRequest is nil"); return
|
||
}
|
||
expect(tokenRequest.capability).to(equal(expectedCapability))
|
||
})
|
||
}
|
||
|
||
// RSA9g
|
||
it("should generate a valid HMAC") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
|
||
let tokenParams = ARTTokenParams(clientId: "client_string")
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(tokenParams, options: nil, callback: { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest1 = tokenRequest else {
|
||
XCTFail("TokenRequest is nil"); done(); return
|
||
}
|
||
let signed = tokenParams.sign(rest.internal.options.key!, withNonce: tokenRequest1.nonce)
|
||
expect(tokenRequest1.mac).to(equal(signed?.mac))
|
||
|
||
rest.auth.createTokenRequest(tokenParams, options: nil, callback: { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest2 = tokenRequest else {
|
||
XCTFail("TokenRequest is nil"); done(); return
|
||
}
|
||
expect(tokenRequest2.nonce).toNot(equal(tokenRequest1.nonce))
|
||
expect(tokenRequest2.mac).toNot(equal(tokenRequest1.mac))
|
||
done()
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
// RSA9i
|
||
it("should respect all requirements") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
let expectedClientId = "client_string"
|
||
let tokenParams = ARTTokenParams(clientId: expectedClientId)
|
||
let expectedTtl = 6.0
|
||
tokenParams.ttl = NSNumber(value: expectedTtl)
|
||
let expectedCapability = "{}"
|
||
tokenParams.capability = expectedCapability
|
||
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.queryTime = true
|
||
authOptions.key = AblyTests.commonAppSetup().key
|
||
|
||
var serverTime: Date?
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.time({ date, error in
|
||
serverTime = date
|
||
done()
|
||
})
|
||
}
|
||
expect(serverTime).toNot(beNil(), description: "Server time is nil")
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(tokenParams, options: authOptions, callback: { tokenRequest, error in
|
||
defer { done() }
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
XCTFail("TokenRequest is nil"); return
|
||
}
|
||
expect(tokenRequest.clientId).to(equal(expectedClientId))
|
||
expect(tokenRequest.mac).toNot(beNil())
|
||
expect(tokenRequest.nonce).to(haveCount(16))
|
||
expect(tokenRequest.ttl as? TimeInterval).to(equal(expectedTtl))
|
||
expect(tokenRequest.capability).to(equal(expectedCapability))
|
||
expect(tokenRequest.timestamp).to(beCloseTo(serverTime!, within: 6.0))
|
||
})
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// RSA10
|
||
describe("authorize") {
|
||
|
||
// RSA10a
|
||
it("should always create a token") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.useTokenAuth = true
|
||
let rest = ARTRest(options: options)
|
||
let channel = rest.channels.get("test")
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
channel.publish(nil, data: "first check") { error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
// Check that token exists
|
||
expect(rest.auth.internal.method).to(equal(ARTAuthMethod.token))
|
||
guard let firstTokenDetails = rest.auth.tokenDetails else {
|
||
fail("TokenDetails is nil"); return
|
||
}
|
||
expect(firstTokenDetails.token).toNot(beNil())
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
channel.publish(nil, data: "second check") { error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
// Check that token has not changed
|
||
expect(rest.auth.internal.method).to(equal(ARTAuthMethod.token))
|
||
guard let secondTokenDetails = rest.auth.tokenDetails else {
|
||
fail("TokenDetails is nil"); return
|
||
}
|
||
expect(firstTokenDetails).to(beIdenticalTo(secondTokenDetails))
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil, callback: { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("TokenDetails is nil"); done(); return
|
||
}
|
||
// Check that token has changed
|
||
expect(tokenDetails.token).toNot(equal(firstTokenDetails.token))
|
||
|
||
channel.publish(nil, data: "third check") { error in
|
||
expect(error).to(beNil())
|
||
guard let thirdTokenDetails = rest.auth.tokenDetails else {
|
||
fail("TokenDetails is nil"); return
|
||
}
|
||
expect(thirdTokenDetails.token).to(equal(tokenDetails.token))
|
||
done()
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// RSA10a
|
||
it("should create a new token if one already exist and ensure Token Auth is used for all future requests") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let testToken = getTestToken()
|
||
options.token = testToken
|
||
let rest = ARTRest(options: options)
|
||
|
||
expect(rest.auth.tokenDetails?.token).toNot(beNil())
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil, callback: { tokenDetails, error in
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.token).toNot(equal(testToken))
|
||
expect(rest.auth.internal.method).to(equal(ARTAuthMethod.token))
|
||
|
||
publishTestMessage(rest, completion: { error in
|
||
expect(error).to(beNil())
|
||
expect(rest.auth.internal.method).to(equal(ARTAuthMethod.token))
|
||
expect(rest.auth.tokenDetails?.token).to(equal(tokenDetails.token))
|
||
done()
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
// RSA10a
|
||
it("should create a token immediately and ensures Token Auth is used for all future requests") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let rest = ARTRest(options: options)
|
||
|
||
expect(rest.auth.tokenDetails?.token).to(beNil())
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil, callback: { tokenDetails, error in
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.token).toNot(beNil())
|
||
expect(rest.auth.internal.method).to(equal(ARTAuthMethod.token))
|
||
|
||
publishTestMessage(rest, completion: { error in
|
||
expect(error).to(beNil())
|
||
expect(rest.auth.internal.method).to(equal(ARTAuthMethod.token))
|
||
expect(rest.auth.tokenDetails?.token).to(equal(tokenDetails.token))
|
||
done()
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
// RSA10b
|
||
it("should supports all TokenParams and AuthOptions") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(ARTTokenParams(), options: ARTAuthOptions(), callback: { tokenDetails, error in
|
||
guard let error = error as? ARTErrorInfo else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(error.localizedDescription).to(contain("no means to renew the token is provided"))
|
||
done()
|
||
})
|
||
}
|
||
}
|
||
|
||
// RSA10e
|
||
it("should use the requestToken implementation") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
|
||
var requestMethodWasCalled = false
|
||
let block: @convention(block) (AspectInfo) -> Void = { _ in
|
||
requestMethodWasCalled = true
|
||
}
|
||
|
||
let hook = ARTAuthInternal.aspect_hook(rest.auth.internal)
|
||
// Adds a block of code after `requestToken` is triggered
|
||
let token = try? hook(#selector(ARTAuthInternal._requestToken(_:with:callback:)), [], unsafeBitCast(block, to: ARTAuthInternal.self))
|
||
|
||
expect(token).toNot(beNil())
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil, callback: { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.token).toNot(beEmpty())
|
||
done()
|
||
})
|
||
}
|
||
|
||
expect(requestMethodWasCalled).to(beTrue())
|
||
}
|
||
|
||
// RSA10f
|
||
it("should return TokenDetails with valid token metadata") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.clientId = "client_string"
|
||
let rest = ARTRest(options: options)
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails).to(beAnInstanceOf(ARTTokenDetails.self))
|
||
expect(tokenDetails.token).toNot(beEmpty())
|
||
expect(tokenDetails.expires!.timeIntervalSinceNow).to(beGreaterThan(tokenDetails.issued!.timeIntervalSinceNow))
|
||
expect(tokenDetails.clientId).to(equal(options.clientId))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA10g
|
||
context("on subsequent authorisations") {
|
||
|
||
it("should store the AuthOptions with authUrl") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let rest = ARTRest(options: options)
|
||
testHTTPExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
rest.internal.httpExecutor = testHTTPExecutor
|
||
let auth = rest.auth
|
||
|
||
let token = getTestToken()
|
||
let authOptions = ARTAuthOptions()
|
||
// Use authUrl for authentication with plain text token response
|
||
authOptions.authUrl = URL(string: "http://echo.ably.io")!
|
||
authOptions.authParams = [URLQueryItem]()
|
||
authOptions.authParams?.append(URLQueryItem(name: "type", value: "text"))
|
||
authOptions.authParams?.append(URLQueryItem(name: "body", value: token))
|
||
authOptions.authHeaders = ["X-Ably":"Test"]
|
||
authOptions.queryTime = true
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
auth.authorize(nil, options: authOptions) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.token).to(equal(token))
|
||
|
||
auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(testHTTPExecutor.requests.last?.url?.host).to(equal("echo.ably.io"))
|
||
expect(auth.internal.options.authUrl!.host).to(equal("echo.ably.io"))
|
||
expect(auth.internal.options.authHeaders!["X-Ably"]).to(equal("Test"))
|
||
expect(tokenDetails.token).to(equal(token))
|
||
expect(auth.internal.options.queryTime).to(beFalse())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
it("should store the AuthOptions with authCallback") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
let auth = rest.auth
|
||
|
||
var authCallbackHasBeenInvoked = false
|
||
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.authCallback = { tokenParams, completion in
|
||
authCallbackHasBeenInvoked = true
|
||
completion(ARTTokenDetails(token: "token"), nil)
|
||
}
|
||
authOptions.useTokenAuth = true
|
||
authOptions.queryTime = true
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
auth.authorize(nil, options: authOptions) { tokenDetails, error in
|
||
expect(authCallbackHasBeenInvoked).to(beTrue())
|
||
|
||
authCallbackHasBeenInvoked = false
|
||
let authOptions2 = ARTAuthOptions()
|
||
|
||
auth.internal.testSuite_forceTokenToExpire()
|
||
|
||
auth.authorize(nil, options: authOptions2) { tokenDetails, error in
|
||
expect(authCallbackHasBeenInvoked).to(beFalse())
|
||
expect(auth.internal.options.useTokenAuth).to(beFalse())
|
||
expect(auth.internal.options.queryTime).to(beFalse())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
it("should not store queryTime") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let rest = ARTRest(options: options)
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.key = options.key
|
||
authOptions.queryTime = true
|
||
|
||
var serverTimeRequestWasMade = false
|
||
let hook = rest.internal.testSuite_injectIntoMethod(after: #selector(rest.internal._time(_:))) {
|
||
serverTimeRequestWasMade = true
|
||
}
|
||
defer { hook.remove() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
// First time
|
||
rest.auth.authorize(nil, options: authOptions) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
expect(serverTimeRequestWasMade).to(beTrue())
|
||
expect(rest.auth.internal.options.queryTime).to(beFalse())
|
||
serverTimeRequestWasMade = false
|
||
|
||
// Second time
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
expect(serverTimeRequestWasMade).to(beFalse())
|
||
expect(rest.auth.internal.options.queryTime).to(beFalse())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
it("should store the TokenParams") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.clientId = ExpectedTokenParams.clientId
|
||
tokenParams.ttl = ExpectedTokenParams.ttl as NSNumber
|
||
tokenParams.capability = ExpectedTokenParams.capability
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(tokenParams, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
delay(tokenParams.ttl as! TimeInterval + 1.0) {
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.clientId).to(equal(ExpectedTokenParams.clientId))
|
||
expect(tokenDetails.issued!.addingTimeInterval(ExpectedTokenParams.ttl)).to(beCloseTo(tokenDetails.expires!))
|
||
expect(tokenDetails.capability).to(equal(ExpectedTokenParams.capability))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
it("should use configured defaults if the object arguments are omitted") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let rest = ARTRest(options: options)
|
||
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.clientId = ExpectedTokenParams.clientId
|
||
tokenParams.ttl = ExpectedTokenParams.ttl as NSNumber
|
||
tokenParams.capability = ExpectedTokenParams.capability
|
||
|
||
let authOptions = ARTAuthOptions()
|
||
var authCallbackCalled = 0
|
||
authOptions.authCallback = { tokenParams, completion in
|
||
expect(tokenParams.clientId) == ExpectedTokenParams.clientId
|
||
expect(tokenParams.ttl as? TimeInterval) == ExpectedTokenParams.ttl
|
||
expect(tokenParams.capability) == ExpectedTokenParams.capability
|
||
authCallbackCalled += 1
|
||
getTestTokenDetails(key: options.key, completion: completion)
|
||
}
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(tokenParams, options: authOptions) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
expect(authCallbackCalled) == 2
|
||
}
|
||
|
||
}
|
||
|
||
// RSA10h
|
||
it("should use the configured Auth#clientId, if not null, by default") {
|
||
let options = AblyTests.commonAppSetup()
|
||
var rest = ARTRest(options: options)
|
||
|
||
// ClientId null
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.clientId).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
options.clientId = "client_string"
|
||
rest = ARTRest(options: options)
|
||
|
||
// ClientId not null
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.clientId).to(equal(options.clientId))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA10i
|
||
context("should adhere to all requirements relating to") {
|
||
|
||
it("TokenParams") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.clientId = "client_string"
|
||
let rest = ARTRest(options: options)
|
||
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.clientId = ExpectedTokenParams.clientId
|
||
tokenParams.ttl = ExpectedTokenParams.ttl as NSNumber
|
||
tokenParams.capability = ExpectedTokenParams.capability
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(tokenParams, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails).to(beAnInstanceOf(ARTTokenDetails.self))
|
||
expect(tokenDetails.token).toNot(beEmpty())
|
||
expect(tokenDetails.clientId).to(equal(ExpectedTokenParams.clientId))
|
||
expect(tokenDetails.issued!.addingTimeInterval(ExpectedTokenParams.ttl)).to(beCloseTo(tokenDetails.expires!))
|
||
expect(tokenDetails.capability).to(equal(ExpectedTokenParams.capability))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("authCallback") {
|
||
var currentTokenRequest: ARTTokenRequest? = nil
|
||
|
||
var rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
rest.auth.createTokenRequest(nil, options: nil, callback: { tokenRequest, error in
|
||
currentTokenRequest = tokenRequest
|
||
})
|
||
expect(currentTokenRequest).toEventuallyNot(beNil(), timeout: testTimeout)
|
||
|
||
if currentTokenRequest == nil {
|
||
return
|
||
}
|
||
|
||
let options = AblyTests.clientOptions()
|
||
options.authCallback = { tokenParams, completion in
|
||
completion(currentTokenRequest!, nil)
|
||
}
|
||
|
||
rest = ARTRest(options: options)
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails).to(beAnInstanceOf(ARTTokenDetails.self))
|
||
expect(tokenDetails.token).toNot(beEmpty())
|
||
expect(tokenDetails.expires!.timeIntervalSinceNow).to(beGreaterThan(tokenDetails.issued!.timeIntervalSinceNow))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("authUrl") {
|
||
let options = ARTClientOptions()
|
||
options.authUrl = URL(string: "http://echo.ably.io")!
|
||
|
||
let rest = ARTRest(options: options)
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
guard let error = error as? ARTErrorInfo else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(error.statusCode).to(equal(400)) //Bad request
|
||
expect(tokenDetails).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("authUrl with json") {
|
||
guard let tokenDetails = getTestTokenDetails() else {
|
||
XCTFail("TokenDetails is empty")
|
||
return
|
||
}
|
||
|
||
let encoder = ARTJsonLikeEncoder()
|
||
encoder.delegate = ARTJsonEncoder()
|
||
guard let tokenDetailsJSON = String(data: try! encoder.encode(tokenDetails), encoding: .utf8) else {
|
||
XCTFail("JSON TokenDetails is empty")
|
||
return
|
||
}
|
||
|
||
let options = ARTClientOptions()
|
||
// Use authUrl for authentication with JSON TokenDetails response
|
||
options.authUrl = URL(string: "http://echo.ably.io")!
|
||
options.authParams = [URLQueryItem]()
|
||
options.authParams?.append(URLQueryItem(name: "type", value: "json"))
|
||
options.authParams?.append(URLQueryItem(name: "body", value: "[]"))
|
||
var rest = ARTRest(options: options)
|
||
|
||
// Invalid TokenDetails
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect((error as! ARTErrorInfo).code).to(equal(Int(ARTState.authUrlIncompatibleContent.rawValue)))
|
||
expect(tokenDetails).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
options.authParams?.removeLast()
|
||
options.authParams?.append(URLQueryItem(name: "body", value: tokenDetailsJSON))
|
||
rest = ARTRest(options: options)
|
||
|
||
// Valid token
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
// https://github.com/ably/ably-cocoa/issues/618
|
||
it("authUrl returning TokenRequest decodes TTL as expected") {
|
||
let options = AblyTests.commonAppSetup()
|
||
|
||
var rest = ARTRest(options: options)
|
||
var tokenRequest: ARTTokenRequest!
|
||
waitUntil(timeout: testTimeout) { done in
|
||
let params = ARTTokenParams(clientId: "myClientId", nonce: "12345")
|
||
expect(params.ttl).to(beNil())
|
||
rest.auth.createTokenRequest(params, options: nil) { req, _ in
|
||
expect(req!.ttl).to(beNil())
|
||
tokenRequest = req!
|
||
done()
|
||
}
|
||
}
|
||
|
||
let encoder = ARTJsonLikeEncoder()
|
||
encoder.delegate = ARTJsonEncoder()
|
||
let encodedTokenRequest: Data
|
||
do {
|
||
encodedTokenRequest = try encoder.encode(tokenRequest)
|
||
}
|
||
catch {
|
||
fail("Encode failure: \(error)")
|
||
return
|
||
}
|
||
guard let tokenRequestJSON = String(data: encodedTokenRequest, encoding: .utf8) else {
|
||
XCTFail("JSON Token Request is empty")
|
||
return
|
||
}
|
||
|
||
options.authUrl = URL(string: "http://echo.ably.io")!
|
||
options.authParams = [URLQueryItem]()
|
||
options.authParams?.append(URLQueryItem(name: "type", value: "json"))
|
||
options.authParams?.append(URLQueryItem(name: "body", value: tokenRequestJSON))
|
||
options.key = nil
|
||
rest = ARTRest(options: options)
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
expect(tokenDetails?.clientId).to(equal("myClientId"))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("authUrl with plain text") {
|
||
let token = getTestToken()
|
||
let options = ARTClientOptions()
|
||
// Use authUrl for authentication with plain text token response
|
||
options.authUrl = URL(string: "http://echo.ably.io")!
|
||
options.authParams = [URLQueryItem]()
|
||
options.authParams?.append(URLQueryItem(name: "type", value: "text"))
|
||
options.authParams?.append(URLQueryItem(name: "body", value: ""))
|
||
var rest = ARTRest(options: options)
|
||
|
||
// Invalid token
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).toNot(beNil())
|
||
expect(tokenDetails).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
options.authParams?.removeLast()
|
||
options.authParams?.append(URLQueryItem(name: "body", value: token))
|
||
rest = ARTRest(options: options)
|
||
|
||
// Valid token
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// RSA10j
|
||
context("when TokenParams and AuthOptions are provided") {
|
||
|
||
it("should supersede configured AuthOptions (using key) even if arguments objects are empty") {
|
||
let defaultOptions = AblyTests.clientOptions() //sandbox
|
||
defaultOptions.key = "xxxx:xxxx"
|
||
let rest = ARTRest(options: defaultOptions)
|
||
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.key = AblyTests.commonAppSetup().key //valid key
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.ttl = 1.0
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(tokenParams, options: authOptions) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let issued = tokenDetails?.issued else {
|
||
fail("TokenDetails.issued is nil"); done(); return
|
||
}
|
||
guard let expires = tokenDetails?.expires else {
|
||
fail("TokenDetails.expires is nil"); done(); return
|
||
}
|
||
expect(issued).to(beCloseTo(expires, within: tokenParams.ttl as! TimeInterval + 0.1))
|
||
delay(tokenParams.ttl as! TimeInterval + 0.1) {
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
authOptions.key = nil
|
||
// First time
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: authOptions) { _, error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(error.localizedDescription).to(contain("no means to renew the token"))
|
||
done()
|
||
}
|
||
}
|
||
|
||
// Second time
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { _, error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(error.localizedDescription).to(contain("no means to renew the token"))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("should supersede configured AuthOptions (using authUrl) even if arguments objects are empty") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
|
||
let testTokenDetails = getTestTokenDetails(ttl: 0.1)
|
||
let encoder = ARTJsonLikeEncoder()
|
||
encoder.delegate = ARTJsonEncoder()
|
||
guard let currentTokenDetails = testTokenDetails, let jsonTokenDetails = try? encoder.encode(currentTokenDetails) else {
|
||
fail("Invalid TokenDetails")
|
||
return
|
||
}
|
||
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.authUrl = URL(string: "http://echo.ably.io")!
|
||
authOptions.authParams = [URLQueryItem]()
|
||
authOptions.authParams?.append(URLQueryItem(name: "type", value: "json"))
|
||
authOptions.authParams?.append(URLQueryItem(name: "body", value: jsonTokenDetails.toUTF8String))
|
||
authOptions.authHeaders = ["X-Ably":"Test"]
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: authOptions) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.token).to(equal(currentTokenDetails.token))
|
||
expect(rest.auth.internal.options.authUrl).toNot(beNil())
|
||
expect(rest.auth.internal.options.authParams).toNot(beNil())
|
||
expect(rest.auth.internal.options.authHeaders).toNot(beNil())
|
||
delay(0.1) { //force to use the authUrl again
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
authOptions.authParams = nil
|
||
authOptions.authHeaders = nil
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: authOptions) { tokenDetails, error in
|
||
guard let error = error as? ARTErrorInfo else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(error.statusCode).to(equal(400))
|
||
expect(tokenDetails).to(beNil())
|
||
expect(rest.auth.internal.options.authParams).to(beNil())
|
||
expect(rest.auth.internal.options.authHeaders).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
// Repeat
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
guard let error = error as? ARTErrorInfo else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(error.statusCode).to(equal(400))
|
||
expect(tokenDetails).to(beNil())
|
||
expect(rest.auth.internal.options.authParams).to(beNil())
|
||
expect(rest.auth.internal.options.authHeaders).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
authOptions.authUrl = nil
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: authOptions) { tokenDetails, error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(UInt((error as! ARTErrorInfo).code)).to(equal(ARTState.requestTokenFailed.rawValue))
|
||
expect(tokenDetails).to(beNil())
|
||
expect(rest.auth.internal.options.authUrl).to(beNil())
|
||
expect(rest.auth.internal.options.authParams).to(beNil())
|
||
expect(rest.auth.internal.options.authHeaders).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
// Repeat
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(UInt((error as! ARTErrorInfo).code)).to(equal(ARTState.requestTokenFailed.rawValue))
|
||
expect(tokenDetails).to(beNil())
|
||
expect(rest.auth.internal.options.authUrl).to(beNil())
|
||
expect(rest.auth.internal.options.authParams).to(beNil())
|
||
expect(rest.auth.internal.options.authHeaders).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("should supersede configured AuthOptions (using authCallback) even if arguments objects are empty") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
|
||
let testTokenDetails = ARTTokenDetails(token: "token", expires: Date(), issued: Date(), capability: nil, clientId: nil)
|
||
var authCallbackHasBeenInvoked = false
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.authCallback = { tokenParams, completion in
|
||
authCallbackHasBeenInvoked = true
|
||
completion(testTokenDetails, nil)
|
||
}
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: authOptions) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails?.token).to(equal("token"))
|
||
expect(authCallbackHasBeenInvoked).to(beTrue())
|
||
expect(rest.auth.internal.options.authCallback).toNot(beNil())
|
||
done()
|
||
}
|
||
}
|
||
authCallbackHasBeenInvoked = false
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails?.token).to(equal("token"))
|
||
expect(authCallbackHasBeenInvoked).to(beTrue())
|
||
expect(rest.auth.internal.options.authCallback).toNot(beNil())
|
||
done()
|
||
}
|
||
}
|
||
authCallbackHasBeenInvoked = false
|
||
|
||
authOptions.authCallback = nil
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: authOptions) { tokenDetails, error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(UInt((error as! ARTErrorInfo).code)).to(equal(ARTState.requestTokenFailed.rawValue))
|
||
expect(tokenDetails).to(beNil())
|
||
expect(authCallbackHasBeenInvoked).to(beFalse())
|
||
expect(rest.auth.internal.options.authCallback).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(UInt((error as! ARTErrorInfo).code)).to(equal(ARTState.requestTokenFailed.rawValue))
|
||
expect(tokenDetails).to(beNil())
|
||
expect(authCallbackHasBeenInvoked).to(beFalse())
|
||
expect(rest.auth.internal.options.authCallback).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("should supersede configured params and options even if arguments objects are empty") {
|
||
let options = AblyTests.clientOptions()
|
||
options.key = "xxxx:xxxx"
|
||
options.clientId = "client_string"
|
||
let rest = ARTRest(options: options)
|
||
|
||
let tokenParams = ARTTokenParams(clientId: options.clientId)
|
||
|
||
// Defaults
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect((error as! ARTErrorInfo).code).to(equal(ARTErrorCode.notFound.intValue))
|
||
expect(tokenDetails).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
// Custom
|
||
tokenParams.ttl = ExpectedTokenParams.ttl as NSNumber
|
||
tokenParams.capability = ExpectedTokenParams.capability
|
||
tokenParams.clientId = nil
|
||
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.key = AblyTests.commonAppSetup().key
|
||
authOptions.queryTime = true
|
||
|
||
var serverTimeRequestCount = 0
|
||
let hook = rest.internal.testSuite_injectIntoMethod(after: #selector(rest.internal._time(_:))) {
|
||
serverTimeRequestCount += 1
|
||
}
|
||
defer { hook.remove() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(tokenParams, options: authOptions) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
XCTFail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.clientId).to(beNil())
|
||
expect(tokenDetails.issued!.addingTimeInterval(ExpectedTokenParams.ttl)).to(beCloseTo(tokenDetails.expires!))
|
||
expect(tokenDetails.capability).to(equal(ExpectedTokenParams.capability))
|
||
expect(serverTimeRequestCount) == 1
|
||
done()
|
||
}
|
||
}
|
||
|
||
rest.auth.internal.testSuite_forceTokenToExpire()
|
||
|
||
// Subsequent authorisations
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
fail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.clientId).to(beNil())
|
||
expect(tokenDetails.issued!.addingTimeInterval(ExpectedTokenParams.ttl)).to(beCloseTo(tokenDetails.expires!))
|
||
expect(tokenDetails.capability).to(equal(ExpectedTokenParams.capability))
|
||
expect(serverTimeRequestCount) == 1
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("example: if a client is initialised with TokenParams#ttl configured with a custom value, and a TokenParams object is passed in as an argument to #authorize with a null value for ttl, then the ttl used for every subsequent authorization will be null") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.defaultTokenParams = {
|
||
$0.ttl = 0.1;
|
||
$0.clientId = "tester";
|
||
return $0
|
||
}(ARTTokenParams())
|
||
|
||
let rest = ARTRest(options: options)
|
||
|
||
let testTokenParams = ARTTokenParams()
|
||
testTokenParams.ttl = nil
|
||
testTokenParams.clientId = nil
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(testTokenParams, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
fail("TokenDetails is nil"); done(); return
|
||
}
|
||
guard let issued = tokenDetails.issued else {
|
||
fail("TokenDetails.issued is nil"); done(); return
|
||
}
|
||
guard let expires = tokenDetails.expires else {
|
||
fail("TokenDetails.expires is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.clientId).to(beNil())
|
||
// `ttl` when omitted, the default value is applied
|
||
expect(issued.addingTimeInterval(ARTDefault.ttl())).to(equal(expires))
|
||
done()
|
||
}
|
||
}
|
||
|
||
// Subsequent authorization
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
fail("TokenDetails is nil"); done(); return
|
||
}
|
||
guard let issued = tokenDetails.issued else {
|
||
fail("TokenDetails.issued is nil"); done(); return
|
||
}
|
||
guard let expires = tokenDetails.expires else {
|
||
fail("TokenDetails.expires is nil"); done(); return
|
||
}
|
||
expect(tokenDetails.clientId).to(beNil())
|
||
expect(issued.addingTimeInterval(ARTDefault.ttl())).to(equal(expires))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// RSA10k
|
||
context("server time offset") {
|
||
|
||
xit("should obtain server time once and persist the offset from the local clock") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let rest = ARTRest(options: options)
|
||
|
||
let mockServerDate = Date().addingTimeInterval(120)
|
||
rest.auth.internal.testSuite_returnValue(for: NSSelectorFromString("handleServerTime:"), with: mockServerDate)
|
||
let currentDate = Date()
|
||
|
||
var serverTimeRequestCount = 0
|
||
let hook = rest.internal.testSuite_injectIntoMethod(after: #selector(rest.internal._time(_:))) {
|
||
serverTimeRequestCount += 1
|
||
}
|
||
defer { hook.remove() }
|
||
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.key = options.key
|
||
authOptions.queryTime = true
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: authOptions, callback: { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard tokenDetails != nil else {
|
||
fail("TokenDetails is nil"); done(); return
|
||
}
|
||
guard let timeOffset = rest.auth.internal.timeOffset?.doubleValue else {
|
||
fail("Server Time Offset is nil"); done(); return
|
||
}
|
||
expect(timeOffset).toNot(equal(0))
|
||
expect(rest.auth.internal.timeOffset).toNot(beNil())
|
||
let calculatedServerDate = currentDate.addingTimeInterval(timeOffset)
|
||
expect(calculatedServerDate).to(beCloseTo(mockServerDate, within: 0.9))
|
||
expect(serverTimeRequestCount) == 1
|
||
done()
|
||
})
|
||
}
|
||
|
||
rest.auth.internal.testSuite_forceTokenToExpire()
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard tokenDetails != nil else {
|
||
fail("TokenDetails is nil"); done(); return
|
||
}
|
||
guard let timeOffset = rest.auth.internal.timeOffset?.doubleValue else {
|
||
fail("Server Time Offset is nil"); done(); return
|
||
}
|
||
expect(timeOffset).toNot(equal(0))
|
||
let calculatedServerDate = currentDate.addingTimeInterval(timeOffset)
|
||
expect(calculatedServerDate).to(beCloseTo(mockServerDate, within: 0.9))
|
||
expect(serverTimeRequestCount) == 1
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("should be consistent the timestamp request with the server time") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let rest = ARTRest(options: options)
|
||
|
||
let mockServerDate = Date().addingTimeInterval(120)
|
||
rest.auth.internal.testSuite_returnValue(for: NSSelectorFromString("handleServerTime:"), with: mockServerDate)
|
||
|
||
var serverTimeRequestCount = 0
|
||
let hook = rest.internal.testSuite_injectIntoMethod(after: #selector(rest.internal._time(_:))) {
|
||
serverTimeRequestCount += 1
|
||
}
|
||
defer { hook.remove() }
|
||
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.key = options.key
|
||
authOptions.queryTime = true
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(nil, options: authOptions) { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
fail("TokenRequest is nil"); done(); return
|
||
}
|
||
guard let timeOffset = rest.auth.internal.timeOffset?.doubleValue else {
|
||
fail("Server Time Offset is nil"); done(); return
|
||
}
|
||
expect(timeOffset).toNot(equal(0))
|
||
expect(mockServerDate.timeIntervalSinceNow).to(beCloseTo(timeOffset, within: 0.1))
|
||
expect(tokenRequest.timestamp).to(beCloseTo(mockServerDate))
|
||
expect(serverTimeRequestCount) == 1
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("should be possible by lib Client to discard the cached local clock offset") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.queryTime = true
|
||
let rest = ARTRest(options: options)
|
||
|
||
var serverTimeRequestCount = 0
|
||
let hook = rest.internal.testSuite_injectIntoMethod(after: #selector(rest.internal._time(_:))) {
|
||
serverTimeRequestCount += 1
|
||
}
|
||
defer { hook.remove() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenDetails = tokenDetails else {
|
||
fail("TokenDetails is nil"); done(); return
|
||
}
|
||
guard let timeOffset = rest.auth.internal.timeOffset?.doubleValue else {
|
||
fail("Server Time Offset is nil"); done(); return
|
||
}
|
||
expect(timeOffset).toNot(beCloseTo(0))
|
||
let calculatedServerDate = Date().addingTimeInterval(timeOffset)
|
||
expect(tokenDetails.expires).to(beCloseTo(calculatedServerDate.addingTimeInterval(ARTDefault.ttl()), within: 1.0))
|
||
expect(serverTimeRequestCount) == 1
|
||
done()
|
||
}
|
||
}
|
||
|
||
#if TARGET_OS_IPHONE
|
||
NotificationCenter.default.post(name: UIApplication.significantTimeChangeNotification, object: nil)
|
||
#else
|
||
NotificationCenter.default.post(name: .NSSystemClockDidChange, object: nil)
|
||
#endif
|
||
|
||
rest.auth.internal.testSuite_forceTokenToExpire()
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
guard tokenDetails != nil else {
|
||
fail("TokenDetails is nil"); done(); return
|
||
}
|
||
expect(rest.auth.internal.timeOffset).to(beNil())
|
||
expect(serverTimeRequestCount) == 1
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("should use the local clock offset to calculate the server time") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let rest = ARTRest(options: options)
|
||
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.key = options.key
|
||
authOptions.queryTime = false
|
||
|
||
let fakeOffset: TimeInterval = 60 //1 minute
|
||
rest.auth.internal.setTimeOffset(fakeOffset)
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.createTokenRequest(nil, options: authOptions) { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
guard let tokenRequest = tokenRequest else {
|
||
fail("TokenRequest is nil"); done(); return
|
||
}
|
||
guard let timeOffset = rest.auth.internal.timeOffset?.doubleValue else {
|
||
fail("Server Time Offset is nil"); done(); return
|
||
}
|
||
expect(timeOffset) == fakeOffset
|
||
let calculatedServerDate = Date().addingTimeInterval(timeOffset)
|
||
expect(tokenRequest.timestamp).to(beCloseTo(calculatedServerDate, within: 0.5))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("should request server time when queryTime is true even if the time offset is assigned") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let rest = ARTRest(options: options)
|
||
|
||
var serverTimeRequestCount = 0
|
||
let hook = rest.internal.testSuite_injectIntoMethod(after: #selector(rest.internal._time)) {
|
||
serverTimeRequestCount += 1
|
||
}
|
||
defer { hook.remove() }
|
||
|
||
let fakeOffset: TimeInterval = 60 //1 minute
|
||
rest.auth.internal.setTimeOffset(fakeOffset)
|
||
|
||
let authOptions = ARTAuthOptions()
|
||
authOptions.key = options.key
|
||
authOptions.queryTime = true
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: authOptions) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
expect(serverTimeRequestCount) == 1
|
||
guard let timeOffset = rest.auth.internal.timeOffset?.doubleValue else {
|
||
fail("Server Time Offset is nil"); done(); return
|
||
}
|
||
expect(timeOffset).toNot(equal(fakeOffset))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("should discard the time offset in situations in which it may have been invalidated") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
|
||
var discardTimeOffsetCallCount = 0
|
||
let hook = rest.auth.internal.testSuite_injectIntoMethod(after: #selector(rest.auth.internal.discardTimeOffset)) {
|
||
discardTimeOffsetCallCount += 1
|
||
}
|
||
defer { hook.remove() }
|
||
|
||
#if TARGET_OS_IPHONE
|
||
// Force notification
|
||
NotificationCenter.default.post(name: UIApplication.significantTimeChangeNotification, object: nil)
|
||
|
||
expect(discardTimeOffsetCallCount).toEventually(equal(1), timeout: testTimeout)
|
||
|
||
// Force notification
|
||
NotificationCenter.default.post(name: NSLocale.currentLocaleDidChangeNotification, object: nil)
|
||
#else
|
||
// Force notification
|
||
NotificationCenter.default.post(name: NSNotification.Name.NSSystemClockDidChange, object: nil)
|
||
|
||
expect(discardTimeOffsetCallCount).toEventually(equal(1), timeout: testTimeout)
|
||
|
||
// Force notification
|
||
NotificationCenter.default.post(name: NSLocale.currentLocaleDidChangeNotification, object: nil)
|
||
#endif
|
||
|
||
expect(discardTimeOffsetCallCount).toEventually(equal(2), timeout: testTimeout)
|
||
}
|
||
|
||
}
|
||
|
||
context("two consecutive authorizations") {
|
||
it("using REST, should call each authorize callback") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.useTokenAuth = true
|
||
let rest = ARTRest(options: options)
|
||
|
||
var tokenDetailsFirst: ARTTokenDetails?
|
||
var tokenDetailsLast: ARTTokenDetails?
|
||
waitUntil(timeout: testTimeout) { done in
|
||
let partialDone = AblyTests.splitDone(2, done: done)
|
||
rest.auth.authorize { tokenDetails, error in
|
||
if let error = error {
|
||
fail(error.localizedDescription); partialDone(); return
|
||
}
|
||
expect(tokenDetails).toNot(beNil())
|
||
if tokenDetailsFirst == nil {
|
||
tokenDetailsFirst = tokenDetails
|
||
}
|
||
else {
|
||
tokenDetailsLast = tokenDetails
|
||
}
|
||
partialDone()
|
||
}
|
||
rest.auth.authorize { tokenDetails, error in
|
||
if let error = error {
|
||
fail(error.localizedDescription); partialDone(); return
|
||
}
|
||
expect(tokenDetails).toNot(beNil())
|
||
if tokenDetailsFirst == nil {
|
||
tokenDetailsFirst = tokenDetails
|
||
}
|
||
else {
|
||
tokenDetailsLast = tokenDetails
|
||
}
|
||
partialDone()
|
||
}
|
||
}
|
||
|
||
expect(tokenDetailsFirst?.token).toNot(equal(tokenDetailsLast?.token))
|
||
expect(rest.auth.tokenDetails).to(beIdenticalTo(tokenDetailsLast))
|
||
expect(rest.auth.tokenDetails?.token).to(equal(tokenDetailsLast?.token))
|
||
}
|
||
it("using Realtime and connection is CONNECTING, should call each Realtime authorize callback") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.useTokenAuth = true
|
||
let realtime = AblyTests.newRealtime(options)
|
||
defer { realtime.close(); realtime.dispose() }
|
||
|
||
var connectedStateCount = 0
|
||
realtime.connection.on(.connected) { _ in
|
||
connectedStateCount += 1
|
||
}
|
||
|
||
var tokenDetailsLast: ARTTokenDetails?
|
||
var didCancelAuthorization = false
|
||
waitUntil(timeout: testTimeout) { done in
|
||
let partialDone = AblyTests.splitDone(2, done: done)
|
||
let callback: (ARTTokenDetails?, Error?) -> Void = { tokenDetails, error in
|
||
if let error = error {
|
||
if (error as NSError).code == URLError.cancelled.rawValue {
|
||
expect(tokenDetails).to(beNil())
|
||
didCancelAuthorization = true
|
||
}
|
||
else {
|
||
fail(error.localizedDescription); partialDone(); return
|
||
}
|
||
}
|
||
else {
|
||
expect(tokenDetails).toNot(beNil())
|
||
tokenDetailsLast = tokenDetails
|
||
}
|
||
partialDone()
|
||
}
|
||
// One of them will be canceled by the connection:
|
||
realtime.auth.authorize(callback)
|
||
realtime.auth.authorize(callback)
|
||
}
|
||
|
||
expect(didCancelAuthorization).to(be(true))
|
||
expect(realtime.auth.tokenDetails).to(beIdenticalTo(tokenDetailsLast))
|
||
expect(realtime.auth.tokenDetails?.token).to(equal(tokenDetailsLast?.token))
|
||
|
||
if let transport = realtime.internal.transport as? TestProxyTransport, let query = transport.lastUrl?.query {
|
||
expect(query).to(haveParam("accessToken", withValue: realtime.auth.tokenDetails?.token ?? ""))
|
||
}
|
||
else {
|
||
fail("MockTransport is not working")
|
||
}
|
||
|
||
expect(connectedStateCount) == 1
|
||
}
|
||
it("using Realtime and connection is CONNECTED, should call each Realtime authorize callback") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.useTokenAuth = true
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.close(); realtime.dispose() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.once(.connected) { state in
|
||
done()
|
||
}
|
||
}
|
||
|
||
var tokenDetailsFirst: ARTTokenDetails?
|
||
var tokenDetailsLast: ARTTokenDetails?
|
||
waitUntil(timeout: testTimeout) { done in
|
||
let partialDone = AblyTests.splitDone(2, done: done)
|
||
realtime.auth.authorize { tokenDetails, error in
|
||
if let error = error {
|
||
fail(error.localizedDescription); partialDone(); return
|
||
}
|
||
expect(tokenDetails).toNot(beNil())
|
||
if tokenDetailsFirst == nil {
|
||
tokenDetailsFirst = tokenDetails
|
||
}
|
||
else {
|
||
tokenDetailsLast = tokenDetails
|
||
}
|
||
partialDone()
|
||
}
|
||
realtime.auth.authorize { tokenDetails, error in
|
||
if let error = error {
|
||
fail(error.localizedDescription); partialDone(); return
|
||
}
|
||
expect(tokenDetails).toNot(beNil())
|
||
if tokenDetailsFirst == nil {
|
||
tokenDetailsFirst = tokenDetails
|
||
}
|
||
else {
|
||
tokenDetailsLast = tokenDetails
|
||
}
|
||
partialDone()
|
||
}
|
||
}
|
||
|
||
expect(tokenDetailsFirst?.token).toNot(equal(tokenDetailsLast?.token))
|
||
expect(realtime.auth.tokenDetails).to(beIdenticalTo(tokenDetailsLast))
|
||
expect(realtime.auth.tokenDetails?.token).to(equal(tokenDetailsLast?.token))
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
describe("TokenParams") {
|
||
context("timestamp") {
|
||
it("if explicitly set, should be returned by the getter") {
|
||
let params = ARTTokenParams()
|
||
params.timestamp = Date(timeIntervalSince1970: 123)
|
||
expect(params.timestamp).to(equal(Date(timeIntervalSince1970: 123)))
|
||
}
|
||
|
||
it("if explicitly set, the value should stick") {
|
||
let params = ARTTokenParams()
|
||
params.timestamp = Date()
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
let now = Double(NSDate().artToIntegerMs())
|
||
guard let timestamp = params.timestamp else {
|
||
fail("timestamp is nil"); done(); return
|
||
}
|
||
let firstParamsTimestamp = Double((timestamp as NSDate).artToIntegerMs())
|
||
expect(firstParamsTimestamp).to(beCloseTo(now, within: 2.5))
|
||
delay(0.25) {
|
||
expect(Double((timestamp as NSDate).artToIntegerMs())).to(equal(firstParamsTimestamp))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
// https://github.com/ably/ably-cocoa/pull/508#discussion_r82577728
|
||
it("object has no timestamp value unless explicitly set") {
|
||
let params = ARTTokenParams()
|
||
expect(params.timestamp).to(beNil())
|
||
}
|
||
}
|
||
}
|
||
|
||
describe("Reauth") {
|
||
|
||
// RTC8
|
||
it("should use authorize({force: true}) to reauth with a token with a different set of capabilities") {
|
||
let options = AblyTests.commonAppSetup()
|
||
let initialToken = getTestToken(clientId: "tester", capability: "{\"restricted\":[\"*\"]}")
|
||
options.token = initialToken
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
let channel = realtime.channels.get("foo")
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
channel.attach { error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect(error.code) == ARTErrorCode.operationNotPermittedWithProvidedCapability.intValue
|
||
done()
|
||
}
|
||
}
|
||
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.capability = "{\"\(channel.name)\":[\"*\"]}"
|
||
tokenParams.clientId = "tester"
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.auth.authorize(tokenParams, options: nil) { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
expect(tokenDetails).toNot(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
expect(realtime.auth.tokenDetails?.token).toNot(equal(initialToken))
|
||
expect(realtime.auth.tokenDetails?.capability).to(equal(tokenParams.capability))
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
channel.attach { error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
// RTC8
|
||
it("for a token change that fails due to an incompatible token, which should result in the connection entering the FAILED state") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.clientId = "tester"
|
||
options.useTokenAuth = true
|
||
let realtime = ARTRealtime(options: options)
|
||
defer { realtime.dispose(); realtime.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.connection.on(.connected) { stateChange in
|
||
expect(stateChange.reason).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
guard let initialToken = realtime.auth.tokenDetails?.token else {
|
||
fail("TokenDetails is nil"); return
|
||
}
|
||
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.capability = "{\"restricted\":[\"*\"]}"
|
||
tokenParams.clientId = "secret"
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
realtime.auth.authorize(tokenParams, options: nil) { tokenDetails, error in
|
||
guard let error = error else {
|
||
fail("Error is nil"); done(); return
|
||
}
|
||
expect((error as! ARTErrorInfo).code) == ARTErrorCode.incompatibleCredentials.intValue
|
||
expect(tokenDetails).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
expect(realtime.connection.state).toEventually(equal(ARTRealtimeConnectionState.connected), timeout: testTimeout)
|
||
expect(realtime.auth.tokenDetails?.token).to(equal(initialToken))
|
||
expect(realtime.auth.tokenDetails?.capability).toNot(equal(tokenParams.capability))
|
||
}
|
||
|
||
}
|
||
|
||
describe("TokenParams") {
|
||
// TK2d
|
||
it("timestamp should not be a member of any default token params") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize(nil, options: nil) { _, error in
|
||
expect(error).to(beNil())
|
||
guard let defaultTokenParams = rest.auth.internal.options.defaultTokenParams else {
|
||
fail("DefaultTokenParams is nil"); done(); return
|
||
}
|
||
expect(defaultTokenParams.timestamp).to(beNil())
|
||
|
||
var defaultTokenParamsCallCount = 0
|
||
let hook = rest.auth.internal.options.testSuite_injectIntoMethod(after: NSSelectorFromString("defaultTokenParams")) {
|
||
defaultTokenParamsCallCount += 1
|
||
}
|
||
defer { hook.remove() }
|
||
|
||
let newTokenParams = ARTTokenParams(options: rest.auth.internal.options)
|
||
expect(defaultTokenParamsCallCount) > 0
|
||
|
||
newTokenParams.timestamp = Date()
|
||
expect(newTokenParams.timestamp).toNot(beNil())
|
||
expect(defaultTokenParams.timestamp).to(beNil()) //remain nil
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
describe("TokenRequest") {
|
||
// TE6
|
||
describe("fromJson") {
|
||
let cases = [
|
||
"with TTL": (
|
||
"{" +
|
||
" \"clientId\":\"myClientId\"," +
|
||
" \"mac\":\"4rr4J+JzjiCL1DoS8wq7k11Z4oTGCb1PoeN+yGjkaH4=\"," +
|
||
" \"capability\":\"{\\\"test\\\":[\\\"publish\\\"]}\"," +
|
||
" \"ttl\":42000," +
|
||
" \"timestamp\":1479087321934," +
|
||
" \"keyName\":\"xxxxxx.yyyyyy\"," +
|
||
" \"nonce\":\"7830658976108826\"" +
|
||
"}",
|
||
{ (_ request: ARTTokenRequest) in
|
||
expect(request.clientId).to(equal("myClientId"))
|
||
expect(request.mac).to(equal("4rr4J+JzjiCL1DoS8wq7k11Z4oTGCb1PoeN+yGjkaH4="))
|
||
expect(request.capability).to(equal("{\"test\":[\"publish\"]}"))
|
||
expect(request.ttl as? TimeInterval).to(equal(TimeInterval(42)))
|
||
expect(request.timestamp).to(equal(Date(timeIntervalSince1970: 1479087321.934)))
|
||
expect(request.keyName).to(equal("xxxxxx.yyyyyy"))
|
||
expect(request.nonce).to(equal("7830658976108826"))
|
||
}
|
||
),
|
||
"without TTL": (
|
||
"{" +
|
||
" \"mac\":\"4rr4J+JzjiCL1DoS8wq7k11Z4oTGCb1PoeN+yGjkaH4=\"," +
|
||
" \"capability\":\"{\\\"test\\\":[\\\"publish\\\"]}\"," +
|
||
" \"timestamp\":1479087321934," +
|
||
" \"keyName\":\"xxxxxx.yyyyyy\"," +
|
||
" \"nonce\":\"7830658976108826\"" +
|
||
"}",
|
||
{ (_ request: ARTTokenRequest) in
|
||
expect(request.clientId).to(beNil())
|
||
expect(request.mac).to(equal("4rr4J+JzjiCL1DoS8wq7k11Z4oTGCb1PoeN+yGjkaH4="))
|
||
expect(request.capability).to(equal("{\"test\":[\"publish\"]}"))
|
||
expect(request.ttl).to(beNil())
|
||
expect(request.timestamp).to(equal(Date(timeIntervalSince1970: 1479087321.934)))
|
||
expect(request.keyName).to(equal("xxxxxx.yyyyyy"))
|
||
expect(request.nonce).to(equal("7830658976108826"))
|
||
}
|
||
)
|
||
]
|
||
|
||
for (caseName, (json, check)) in cases {
|
||
context(caseName) {
|
||
it("accepts a string, which should be interpreted as JSON") {
|
||
check(try! ARTTokenRequest.fromJson(json as ARTJsonCompatible))
|
||
}
|
||
|
||
it("accepts a NSDictionary") {
|
||
let data = json.data(using: String.Encoding.utf8)!
|
||
let dict = try! JSONSerialization.jsonObject(with: data, options: .mutableLeaves) as! NSDictionary
|
||
check(try! ARTTokenRequest.fromJson(dict))
|
||
}
|
||
}
|
||
}
|
||
|
||
it("rejects invalid JSON") {
|
||
expect{try ARTTokenRequest.fromJson("not JSON" as ARTJsonCompatible)}.to(throwError())
|
||
}
|
||
|
||
it("rejects non-object JSON") {
|
||
expect{try ARTTokenRequest.fromJson("[]" as ARTJsonCompatible)}.to(throwError())
|
||
}
|
||
}
|
||
}
|
||
|
||
describe("TokenDetails") {
|
||
// TD7
|
||
describe("fromJson") {
|
||
let json = "{" +
|
||
" \"token\": \"xxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\"," +
|
||
" \"issued\": 1479087321934," +
|
||
" \"expires\": 1479087363934," +
|
||
" \"capability\": \"{\\\"test\\\":[\\\"publish\\\"]}\"," +
|
||
" \"clientId\": \"myClientId\"" +
|
||
"}"
|
||
|
||
func check(_ details: ARTTokenDetails) {
|
||
expect(details.token).to(equal("xxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"))
|
||
expect(details.issued).to(equal(Date(timeIntervalSince1970: 1479087321.934)))
|
||
expect(details.expires).to(equal(Date(timeIntervalSince1970: 1479087363.934)))
|
||
expect(details.capability).to(equal("{\"test\":[\"publish\"]}"))
|
||
expect(details.clientId).to(equal("myClientId"))
|
||
}
|
||
|
||
it("accepts a string, which should be interpreted as JSON") {
|
||
check(try! ARTTokenDetails.fromJson(json as ARTJsonCompatible))
|
||
}
|
||
|
||
it("accepts a NSDictionary") {
|
||
let data = json.data(using: String.Encoding.utf8)!
|
||
let dict = try! JSONSerialization.jsonObject(with: data, options: .mutableLeaves) as! NSDictionary
|
||
check(try! ARTTokenDetails.fromJson(dict))
|
||
}
|
||
|
||
it("rejects invalid JSON") {
|
||
expect{try ARTTokenDetails.fromJson("not JSON" as ARTJsonCompatible)}.to(throwError())
|
||
}
|
||
|
||
it("rejects non-object JSON") {
|
||
expect{try ARTTokenDetails.fromJson("[]" as ARTJsonCompatible)}.to(throwError())
|
||
}
|
||
}
|
||
}
|
||
|
||
describe("JWT and realtime") {
|
||
let channelName = "test_JWT"
|
||
let messageName = "message_JWT"
|
||
|
||
context("client initialized with a JWT token in ClientOptions") {
|
||
let options = AblyTests.clientOptions()
|
||
|
||
context("with valid credentials") {
|
||
xit("pulls stats successfully") {
|
||
options.token = getJWTToken()
|
||
let client = AblyTests.newRealtime(options)
|
||
defer { client.dispose(); client.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.stats { stats, error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
context("with invalid credentials") {
|
||
it("fails to connect with reason 'invalid signature'") {
|
||
options.token = getJWTToken(invalid: true)
|
||
options.autoConnect = false
|
||
let client = AblyTests.newRealtime(options)
|
||
defer { client.dispose(); client.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.connection.once(.failed) { stateChange in
|
||
guard let reason = stateChange.reason else {
|
||
fail("Reason error is nil"); done(); return
|
||
}
|
||
expect(reason.code).to(equal(ARTErrorCode.invalidJwtFormat.intValue))
|
||
expect(reason.description).to(satisfyAnyOf(contain("invalid signature"), contain("signature verification failed")))
|
||
done()
|
||
}
|
||
client.connect()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA8g RSA8c
|
||
context("when using authUrl") {
|
||
let options = AblyTests.clientOptions()
|
||
options.authUrl = URL(string: echoServerAddress)!
|
||
|
||
var keys: [String: String]!
|
||
|
||
func setupDependencies() {
|
||
if (keys == nil) {
|
||
keys = getKeys()
|
||
}
|
||
}
|
||
|
||
context("with valid credentials") {
|
||
it("fetches a channels and posts a message") {
|
||
setupDependencies()
|
||
|
||
options.authParams = [URLQueryItem]()
|
||
options.authParams?.append(URLQueryItem(name: "keyName", value: keys["keyName"]))
|
||
options.authParams?.append(URLQueryItem(name: "keySecret", value: keys["keySecret"]))
|
||
let client = ARTRealtime(options: options)
|
||
defer { client.dispose(); client.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.connection.once(.connected, callback: { _ in
|
||
let channel = client.channels.get(channelName)
|
||
channel.publish(messageName, data: nil, callback: { error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
})
|
||
})
|
||
client.connect()
|
||
}
|
||
}
|
||
}
|
||
|
||
context("with wrong credentials") {
|
||
it("fails to connect with reason 'invalid signature'") {
|
||
setupDependencies()
|
||
|
||
options.authParams = [URLQueryItem]()
|
||
options.authParams?.append(URLQueryItem(name: "keyName", value: keys["keyName"]))
|
||
options.authParams?.append(URLQueryItem(name: "keySecret", value: "INVALID"))
|
||
let client = ARTRealtime(options: options)
|
||
defer { client.dispose(); client.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.connection.once(.disconnected) { stateChange in
|
||
guard let reason = stateChange.reason else {
|
||
fail("Reason error is nil"); done(); return
|
||
}
|
||
expect(reason.code).to(equal(ARTErrorCode.invalidJwtFormat.intValue))
|
||
expect(reason.description).to(satisfyAnyOf(contain("invalid signature"), contain("signature verification failed")))
|
||
done()
|
||
}
|
||
client.connect()
|
||
}
|
||
}
|
||
}
|
||
|
||
context("when token expires") {
|
||
|
||
it ("receives a 40142 error from the server") {
|
||
setupDependencies()
|
||
|
||
let tokenDuration = 5.0
|
||
options.authParams = [URLQueryItem]()
|
||
options.authParams?.append(URLQueryItem(name: "keyName", value: keys["keyName"]))
|
||
options.authParams?.append(URLQueryItem(name: "keySecret", value: keys["keySecret"]))
|
||
options.authParams?.append(URLQueryItem(name: "expiresIn", value: String(UInt(tokenDuration))))
|
||
let client = ARTRealtime(options: options)
|
||
defer { client.dispose(); client.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.connection.once(.connected) { stateChange in
|
||
client.connection.once(.disconnected) { stateChange in
|
||
expect(stateChange.reason?.code).to(equal(ARTErrorCode.tokenExpired.intValue))
|
||
expect(stateChange.reason?.description).to(contain("Key/token status changed (expire)"))
|
||
done()
|
||
}
|
||
}
|
||
client.connect()
|
||
}
|
||
}
|
||
}
|
||
|
||
// RTC8a4
|
||
context("when the server sends and AUTH protocol message") {
|
||
it("client reauths correctly without going through a disconnection") {
|
||
setupDependencies()
|
||
|
||
// The server sends an AUTH protocol message 30 seconds before a token expires
|
||
// We create a token that lasts 35 seconds, so there's room to receive the AUTH message
|
||
let tokenDuration = 35.0
|
||
options.authParams = [URLQueryItem]()
|
||
options.authParams?.append(URLQueryItem(name: "keyName", value: keys["keyName"]))
|
||
options.authParams?.append(URLQueryItem(name: "keySecret", value: keys["keySecret"]))
|
||
options.authParams?.append(URLQueryItem(name: "expiresIn", value: String(UInt(tokenDuration))))
|
||
options.autoConnect = false // Prevent auto connection so we can set the transport proxy
|
||
let client = ARTRealtime(options: options)
|
||
client.internal.setTransport(TestProxyTransport.self)
|
||
defer { client.dispose(); client.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.connection.once(.connected) { stateChange in
|
||
let originalToken = client.auth.tokenDetails?.token
|
||
let transport = client.internal.transport as! TestProxyTransport
|
||
|
||
client.connection.once(.update) { stateChange in
|
||
expect(transport.protocolMessagesReceived.filter({ $0.action == .auth })).to(haveCount(1))
|
||
expect(originalToken).toNot(equal(client.auth.tokenDetails?.token))
|
||
done()
|
||
}
|
||
}
|
||
client.connect()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA8g
|
||
context("when using authCallback") {
|
||
let options = AblyTests.clientOptions()
|
||
|
||
context("with valid credentials") {
|
||
xit("pulls stats successfully") {
|
||
options.authCallback = { tokenParams, completion in
|
||
let token = ARTTokenDetails(token: getJWTToken()!)
|
||
completion(token, nil)
|
||
}
|
||
let client = ARTRealtime(options: options)
|
||
defer { client.dispose(); client.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.stats { stats, error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
context("with invalid credentials") {
|
||
it("fails to connect") {
|
||
options.authCallback = { tokenParams, completion in
|
||
let token = ARTTokenDetails(token: getJWTToken(invalid: true)!)
|
||
completion(token, nil)
|
||
}
|
||
let client = ARTRealtime(options: options)
|
||
defer { client.dispose(); client.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.connection.once(.disconnected) { stateChange in
|
||
guard let reason = stateChange.reason else {
|
||
fail("Reason error is nil"); done(); return
|
||
}
|
||
expect(reason.code).to(equal(ARTErrorCode.invalidJwtFormat.intValue))
|
||
expect(reason.description).to(satisfyAnyOf(contain("invalid signature"), contain("signature verification failed")))
|
||
done()
|
||
}
|
||
client.connect()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
context("when token expires and has a means to renew") {
|
||
|
||
it("reconnects using authCallback and obtains a new token") {
|
||
let tokenDuration = 3.0
|
||
let options = AblyTests.clientOptions()
|
||
options.useTokenAuth = true
|
||
options.autoConnect = false
|
||
options.authCallback = { tokenParams, completion in
|
||
let token = ARTTokenDetails(token: getJWTToken(expiresIn: Int(tokenDuration))!)
|
||
completion(token, nil)
|
||
}
|
||
let client = ARTRealtime(options: options)
|
||
defer { client.dispose(); client.close() }
|
||
var originalToken = ""
|
||
var originalConnectionID = ""
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.connection.once(.connected) { _ in
|
||
originalToken = client.auth.tokenDetails!.token
|
||
originalConnectionID = client.connection.id!
|
||
|
||
client.connection.once(.disconnected) { stateChange in
|
||
expect(stateChange.reason?.code).to(equal(ARTErrorCode.tokenExpired.intValue))
|
||
|
||
client.connection.once(.connected) { _ in
|
||
expect(client.connection.id).to(equal(originalConnectionID))
|
||
expect(client.auth.tokenDetails!.token).toNot(equal(originalToken))
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
client.connect()
|
||
}
|
||
}
|
||
}
|
||
|
||
context("when the token request includes a clientId") {
|
||
it("the clientId is the same specified in the JWT token request") {
|
||
let clientId = "JWTClientId"
|
||
let options = AblyTests.clientOptions()
|
||
options.tokenDetails = ARTTokenDetails(token: getJWTToken(clientId: clientId)!)
|
||
let client = ARTRealtime(options: options)
|
||
defer { client.dispose(); client.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.connection.once(.connected) { _ in
|
||
expect(client.auth.clientId).to(equal(clientId))
|
||
done()
|
||
}
|
||
client.connect()
|
||
}
|
||
}
|
||
}
|
||
|
||
context("when the token request includes subscribe-only capabilities") {
|
||
it("fails to publish to a channel with subscribe-only capability") {
|
||
let capability = "{\"\(channelName)\":[\"subscribe\"]}"
|
||
let options = AblyTests.clientOptions()
|
||
options.tokenDetails = ARTTokenDetails(token: getJWTToken(capability: capability)!)
|
||
// Prevent channel name to be prefixed by test-*
|
||
options.channelNamePrefix = nil
|
||
let client = ARTRealtime(options: options)
|
||
defer { client.dispose(); client.close() }
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.channels.get(channelName).publish(messageName, data: nil, callback: { error in
|
||
expect(error?.code).to(equal(ARTErrorCode.operationNotPermittedWithProvidedCapability.intValue))
|
||
expect(error?.message).to(contain("permission denied"))
|
||
done()
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA11
|
||
context("currentTokenDetails") {
|
||
|
||
// RSA11b
|
||
it("should hold a @TokenDetails@ instance in which only the @token@ attribute is populated with that token string") {
|
||
let token = getTestToken()
|
||
let rest = ARTRest(token: token)
|
||
expect(rest.auth.tokenDetails?.token).to(equal(token))
|
||
}
|
||
|
||
// RSA11c
|
||
it("should be set with the current token (if applicable) on instantiation and each time it is replaced") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
expect(rest.auth.tokenDetails).to(beNil())
|
||
var authenticatedTokenDetails: ARTTokenDetails?
|
||
waitUntil(timeout: testTimeout) { done in
|
||
rest.auth.authorize { tokenDetails, error in
|
||
expect(error).to(beNil())
|
||
authenticatedTokenDetails = tokenDetails
|
||
done()
|
||
}
|
||
}
|
||
expect(rest.auth.tokenDetails).to(equal(authenticatedTokenDetails))
|
||
}
|
||
|
||
// RSA11d
|
||
it("should be empty if there is no current token") {
|
||
let rest = ARTRest(options: AblyTests.commonAppSetup())
|
||
expect(rest.auth.tokenDetails).to(beNil())
|
||
}
|
||
|
||
}
|
||
|
||
// RSC1 RSC1a RSC1c RSA3d
|
||
describe("JWT and rest") {
|
||
let options = AblyTests.clientOptions()
|
||
|
||
context("when the JWT token embeds an Ably token") {
|
||
it ("pulls stats successfully") {
|
||
options.tokenDetails = ARTTokenDetails(token: getJWTToken(jwtType: "embedded")!)
|
||
let client = ARTRest(options: options)
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.stats { stats, error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
context("when the JWT token embeds an Ably token and it is requested as encrypted") {
|
||
it ("pulls stats successfully") {
|
||
options.tokenDetails = ARTTokenDetails(token: getJWTToken(jwtType: "embedded", encrypted: 1)!)
|
||
let client = ARTRest(options: options)
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.stats { stats, error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// RSA4f, RSA8c
|
||
context("when the JWT token is returned with application/jwt content type") {
|
||
var client: ARTRest!
|
||
|
||
func setupDependencies() {
|
||
if (client == nil) {
|
||
let options = AblyTests.clientOptions()
|
||
let keys = getKeys()
|
||
options.authUrl = URL(string: echoServerAddress)!
|
||
options.authParams = [URLQueryItem]()
|
||
options.authParams?.append(URLQueryItem(name: "keyName", value: keys["keyName"]))
|
||
options.authParams?.append(URLQueryItem(name: "keySecret", value: keys["keySecret"]))
|
||
options.authParams?.append(URLQueryItem(name: "returnType", value: "jwt"))
|
||
client = ARTRest(options: options)
|
||
}
|
||
}
|
||
|
||
beforeEach {
|
||
setupDependencies()
|
||
}
|
||
|
||
it("the client successfully connects and pulls stats") {
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.stats { stats, error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
}
|
||
|
||
it("the client can request a new token to initilize another client that connects and pulls stats") {
|
||
waitUntil(timeout: testTimeout) { done in
|
||
client.auth.requestToken(nil, with: nil, callback: { tokenDetails, error in
|
||
let newClientOptions = AblyTests.clientOptions()
|
||
newClientOptions.token = tokenDetails!.token
|
||
let newClient = ARTRest(options: newClientOptions)
|
||
newClient.stats { stats, error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// https://github.com/ably/ably-cocoa/issues/849
|
||
it("should not force token auth when clientId is set") {
|
||
let options = AblyTests.commonAppSetup()
|
||
options.clientId = "foo"
|
||
expect(options.isBasicAuth()).to(beTrue())
|
||
}
|
||
|
||
// https://github.com/ably/ably-cocoa/issues/1093
|
||
it("should accept authURL response with timestamp argument as string") {
|
||
var originalTokenRequest: ARTTokenRequest!
|
||
let tmpRest = ARTRest(options: AblyTests.commonAppSetup())
|
||
waitUntil(timeout: testTimeout) { done in
|
||
let tokenParams = ARTTokenParams()
|
||
tokenParams.clientId = "john"
|
||
tokenParams.capability = """
|
||
{"chat:*":["publish","subscribe","presence","history"]}
|
||
"""
|
||
tokenParams.ttl = 43200
|
||
tmpRest.auth.createTokenRequest(tokenParams, options: nil) { tokenRequest, error in
|
||
expect(error).to(beNil())
|
||
originalTokenRequest = try! XCTUnwrap(tokenRequest)
|
||
done()
|
||
}
|
||
}
|
||
// "timestamp" as String
|
||
let tokenRequestJsonString = """
|
||
{"keyName":"\(originalTokenRequest.keyName)","timestamp":"\(String(dateToMilliseconds(originalTokenRequest.timestamp))))","clientId":"\(originalTokenRequest.clientId!)","nonce":"\(originalTokenRequest.nonce)","mac":"\(originalTokenRequest.mac)","ttl":"\(String(originalTokenRequest.ttl!.intValue * 1000)))","capability":"\(originalTokenRequest.capability!.replace("\"", withString: "\\\""))"}
|
||
"""
|
||
|
||
let options = AblyTests.clientOptions()
|
||
options.authUrl = URL(string: "http://auth-test.ably.cocoa")
|
||
|
||
let rest = ARTRest(options: options)
|
||
expect(rest.auth.clientId).to(beNil())
|
||
#if TARGET_OS_IOS
|
||
expect(rest.device.clientId).to(beNil())
|
||
#endif
|
||
let testHttpExecutor = TestProxyHTTPExecutor(options.logHandler)
|
||
rest.internal.httpExecutor = testHttpExecutor
|
||
let channel = rest.channels.get("chat:one")
|
||
|
||
testHttpExecutor.simulateIncomingPayloadOnNextRequest(tokenRequestJsonString.data(using: .utf8)!)
|
||
|
||
waitUntil(timeout: testTimeout) { done in
|
||
channel.publish("foo", data: nil) { error in
|
||
expect(error).to(beNil())
|
||
done()
|
||
}
|
||
}
|
||
|
||
expect(testHttpExecutor.requests.at(0)?.url?.host).to(equal("auth-test.ably.cocoa"))
|
||
guard let tokenDetails = rest.internal.auth.tokenDetails else {
|
||
fail("Should have token details"); return
|
||
}
|
||
expect(tokenDetails.clientId).to(equal(originalTokenRequest.clientId))
|
||
expect(tokenDetails.token).toNot(beNil())
|
||
}
|
||
|
||
}
|
||
}
|