ably 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +103 -0
- data/Rakefile +4 -0
- data/ably.gemspec +32 -0
- data/lib/ably.rb +11 -0
- data/lib/ably/auth.rb +381 -0
- data/lib/ably/exceptions.rb +16 -0
- data/lib/ably/realtime.rb +38 -0
- data/lib/ably/realtime/callbacks.rb +15 -0
- data/lib/ably/realtime/channel.rb +51 -0
- data/lib/ably/realtime/client.rb +82 -0
- data/lib/ably/realtime/connection.rb +61 -0
- data/lib/ably/rest.rb +15 -0
- data/lib/ably/rest/channel.rb +58 -0
- data/lib/ably/rest/client.rb +194 -0
- data/lib/ably/rest/middleware/exceptions.rb +42 -0
- data/lib/ably/rest/middleware/external_exceptions.rb +26 -0
- data/lib/ably/rest/middleware/parse_json.rb +15 -0
- data/lib/ably/rest/paged_resource.rb +107 -0
- data/lib/ably/rest/presence.rb +44 -0
- data/lib/ably/support.rb +14 -0
- data/lib/ably/token.rb +55 -0
- data/lib/ably/version.rb +3 -0
- data/spec/acceptance/realtime_client_spec.rb +12 -0
- data/spec/acceptance/rest/auth_spec.rb +441 -0
- data/spec/acceptance/rest/base_spec.rb +113 -0
- data/spec/acceptance/rest/channel_spec.rb +68 -0
- data/spec/acceptance/rest/presence_spec.rb +22 -0
- data/spec/acceptance/rest/stats_spec.rb +57 -0
- data/spec/acceptance/rest/time_spec.rb +14 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/support/api_helper.rb +41 -0
- data/spec/support/test_app.rb +77 -0
- data/spec/unit/auth.rb +9 -0
- data/spec/unit/realtime_spec.rb +9 -0
- data/spec/unit/rest_spec.rb +99 -0
- data/spec/unit/token_spec.rb +90 -0
- metadata +240 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Ably
|
4
|
+
module Rest
|
5
|
+
module Middleware
|
6
|
+
# HTTP exceptions raised by Ably due to an error status code
|
7
|
+
# Ably returns JSON error codes and messages so include this if possible in the exception messages
|
8
|
+
class Exceptions < Faraday::Response::Middleware
|
9
|
+
def call(env)
|
10
|
+
@app.call(env).on_complete do
|
11
|
+
if env[:status] >= 400
|
12
|
+
error_status_code = env[:status]
|
13
|
+
error_code = nil
|
14
|
+
|
15
|
+
begin
|
16
|
+
error = JSON.parse(env[:body])['error']
|
17
|
+
error_status_code = error['statusCode'].to_i if error['statusCode']
|
18
|
+
error_code = error['code'].to_i if error['code']
|
19
|
+
|
20
|
+
if error
|
21
|
+
message = "#{error['message']} (status: #{error_status_code}, code: #{error_code})"
|
22
|
+
else
|
23
|
+
message = env[:body]
|
24
|
+
end
|
25
|
+
rescue JSON::ParserError
|
26
|
+
message = env[:body]
|
27
|
+
end
|
28
|
+
|
29
|
+
message = "Unknown server error" if message.to_s.strip == ''
|
30
|
+
|
31
|
+
if env[:status] >= 500
|
32
|
+
raise Ably::ServerError, message
|
33
|
+
else
|
34
|
+
raise Ably::InvalidRequest.new(message, status: error_status_code, code: error_code)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Ably
|
4
|
+
module Rest
|
5
|
+
module Middleware
|
6
|
+
# HTTP exceptions raised due to a status code error on a 3rd party site
|
7
|
+
# Used by auth calls
|
8
|
+
class ExternalExceptions < Faraday::Response::Middleware
|
9
|
+
def call(env)
|
10
|
+
@app.call(env).on_complete do
|
11
|
+
if env[:status] >= 400
|
12
|
+
error_status_code = env[:status]
|
13
|
+
message = "Error #{error_status_code}: #{(env[:body] || '')[0...200]}"
|
14
|
+
|
15
|
+
if error_status_code >= 500
|
16
|
+
raise Ably::ServerError, message
|
17
|
+
else
|
18
|
+
raise Ably::InvalidRequest, message
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Ably
|
4
|
+
module Rest
|
5
|
+
module Middleware
|
6
|
+
class ParseJson < Faraday::Response::Middleware
|
7
|
+
def parse(body)
|
8
|
+
JSON.parse(body, symbolize_names: true)
|
9
|
+
rescue JSON::ParserError => e
|
10
|
+
raise InvalidResponseBody, "Expected JSON response. #{e.message}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Ably
|
2
|
+
module Rest
|
3
|
+
# Wraps any Ably HTTP response that supports paging and automatically provides methdos to iterated through
|
4
|
+
# the array of resources using {#first}, {#next}, {#last?} and {#first?}
|
5
|
+
#
|
6
|
+
# Paging information is provided by Ably in the LINK HTTP headers
|
7
|
+
class PagedResource
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
# @param [Faraday::Response] http_response Initial HTTP response from an Ably request to a paged resource
|
11
|
+
# @param [String] base_url Base URL for request that generated the http_response so that subsequent paged requests can be made
|
12
|
+
# @param [Ably::Rest::Client] client {Ably::Client} used to make the request to Ably
|
13
|
+
#
|
14
|
+
# @return [Ably::Rest::PagedResource]
|
15
|
+
def initialize(http_response, base_url, client)
|
16
|
+
@http_response = http_response
|
17
|
+
@body = http_response.body
|
18
|
+
@client = client
|
19
|
+
@base_url = "#{base_url.gsub(%r{/[^/]*$}, '')}/"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Retrieve the first page of results
|
23
|
+
#
|
24
|
+
# @return [Ably::Rest::PagedResource]
|
25
|
+
def first
|
26
|
+
PagedResource.new(@client.get(pagination_url('first')), @base_url, @client)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Retrieve the next page of results
|
30
|
+
#
|
31
|
+
# @return [Ably::Rest::PagedResource]
|
32
|
+
def next
|
33
|
+
PagedResource.new(@client.get(pagination_url('next')), @base_url, @client)
|
34
|
+
end
|
35
|
+
|
36
|
+
# True if this is the last page in the paged resource set
|
37
|
+
#
|
38
|
+
# @return [Boolean]
|
39
|
+
def last?
|
40
|
+
!supports_pagination? ||
|
41
|
+
pagination_header('next').nil?
|
42
|
+
end
|
43
|
+
|
44
|
+
# True if this is the first page in the paged resource set
|
45
|
+
#
|
46
|
+
# @return [Boolean]
|
47
|
+
def first?
|
48
|
+
!supports_pagination? ||
|
49
|
+
pagination_header('first') == pagination_header('current')
|
50
|
+
end
|
51
|
+
|
52
|
+
# True if the HTTP response supports paging with the expected LINK HTTP headers
|
53
|
+
#
|
54
|
+
# @return [Boolean]
|
55
|
+
def supports_pagination?
|
56
|
+
!pagination_headers.empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
# Standard Array accessor method
|
60
|
+
def [](index)
|
61
|
+
@body[index]
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns number of items within this page, not the total number of items in the entire paged resource set
|
65
|
+
def length
|
66
|
+
@body.length
|
67
|
+
end
|
68
|
+
alias_method :count, :length
|
69
|
+
alias_method :size, :length
|
70
|
+
|
71
|
+
# Method ensuring this {Ably::Rest::PagedResource} is {http://ruby-doc.org/core-2.1.3/Enumerable.html Enumerable}
|
72
|
+
def each(&block)
|
73
|
+
@body.each do |item|
|
74
|
+
if block_given?
|
75
|
+
block.call item
|
76
|
+
else
|
77
|
+
yield item
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
def pagination_headers
|
84
|
+
link_regex = %r{<(?<url>[^>]+)>; rel="(?<rel>[^"]+)"}
|
85
|
+
@pagination_headers ||= @http_response.headers['link'].scan(link_regex).inject({}) do |hash, val_array|
|
86
|
+
url, rel = val_array
|
87
|
+
hash[rel] = url
|
88
|
+
hash
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def pagination_header(id)
|
93
|
+
pagination_headers[id]
|
94
|
+
end
|
95
|
+
|
96
|
+
def pagination_url(id)
|
97
|
+
raise InvalidPageError, "Paging heading link #{id} does not exist" unless pagination_header(id)
|
98
|
+
|
99
|
+
if pagination_header(id).match(%r{^\./})
|
100
|
+
"#{@base_url}#{pagination_header(id)[2..-1]}"
|
101
|
+
else
|
102
|
+
pagination_header[id]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Ably
|
2
|
+
module Rest
|
3
|
+
class Presence
|
4
|
+
attr_reader :client, :channel
|
5
|
+
|
6
|
+
# Initialize a new Presence object
|
7
|
+
#
|
8
|
+
# @param client [Ably::Rest::Client]
|
9
|
+
# @param channel [Channel] The channel object
|
10
|
+
def initialize(client, channel)
|
11
|
+
@client = client
|
12
|
+
@channel = channel
|
13
|
+
end
|
14
|
+
|
15
|
+
# Obtain the set of members currently present for a channel
|
16
|
+
#
|
17
|
+
# @return [PagedResource] An Array of presence-message Hash objects that supports paging (next, first)
|
18
|
+
def get(options = {})
|
19
|
+
response = client.get(base_path, options)
|
20
|
+
PagedResource.new(response, base_path, client)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return the presence messages history for the channel
|
24
|
+
#
|
25
|
+
# Options:
|
26
|
+
# - start: Time or millisecond since epoch
|
27
|
+
# - end: Time or millisecond since epoch
|
28
|
+
# - direction: :forwards or :backwards (default is :backwards)
|
29
|
+
# - limit: Maximum number of messages to retrieve up to 10,000
|
30
|
+
#
|
31
|
+
# @return [PagedResource] An Array of presence-message Hash objects that supports paging (next, first)
|
32
|
+
def history(options = {})
|
33
|
+
url = "#{base_path}/history"
|
34
|
+
response = client.get(url, options)
|
35
|
+
PagedResource.new(response, url, client)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def base_path
|
40
|
+
"/channels/#{CGI.escape(channel.name)}/presence"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/ably/support.rb
ADDED
data/lib/ably/token.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
module Ably
|
2
|
+
class Token
|
3
|
+
DEFAULTS = {
|
4
|
+
capability: { "*" => ["*"] },
|
5
|
+
ttl: 60 * 60 # 1 hour
|
6
|
+
}
|
7
|
+
|
8
|
+
TOKEN_EXPIRY_BUFFER = 5
|
9
|
+
|
10
|
+
def initialize(attributes)
|
11
|
+
@attributes = attributes.freeze
|
12
|
+
end
|
13
|
+
|
14
|
+
def id
|
15
|
+
attributes.fetch(:id)
|
16
|
+
end
|
17
|
+
|
18
|
+
def key_id
|
19
|
+
attributes.fetch(:key)
|
20
|
+
end
|
21
|
+
|
22
|
+
def issued_at
|
23
|
+
Time.at(attributes.fetch(:issued_at))
|
24
|
+
end
|
25
|
+
|
26
|
+
def expires_at
|
27
|
+
Time.at(attributes.fetch(:expires))
|
28
|
+
end
|
29
|
+
|
30
|
+
def capability
|
31
|
+
attributes.fetch(:capability)
|
32
|
+
end
|
33
|
+
|
34
|
+
def client_id
|
35
|
+
attributes.fetch(:client_id)
|
36
|
+
end
|
37
|
+
|
38
|
+
def nonce
|
39
|
+
attributes.fetch(:nonce)
|
40
|
+
end
|
41
|
+
|
42
|
+
def ==(other)
|
43
|
+
other.class == self.class &&
|
44
|
+
attributes == other.attributes
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns true if token is expired or about to expire
|
48
|
+
def expired?
|
49
|
+
expires_at < Time.now + TOKEN_EXPIRY_BUFFER
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
attr_reader :attributes
|
54
|
+
end
|
55
|
+
end
|
data/lib/ably/version.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "Using the Realtime client" do
|
4
|
+
describe "initializing the client" do
|
5
|
+
it "should disallow an invalid key" do
|
6
|
+
expect { Ably::Realtime::Client.new({}) }.to raise_error(ArgumentError, /api_key is missing/)
|
7
|
+
expect { Ably::Realtime::Client.new(api_key: 'invalid') }.to raise_error(ArgumentError, /api_key is invalid/)
|
8
|
+
expect { Ably::Realtime::Client.new(api_key: 'invalid:asdad') }.to raise_error(ArgumentError, /api_key is invalid/)
|
9
|
+
expect { Ably::Realtime::Client.new(api_key: 'appid.keyuid:keysecret') }.to_not raise_error
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,441 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
describe "REST" do
|
5
|
+
let(:client) do
|
6
|
+
Ably::Rest::Client.new(api_key: api_key, environment: environment)
|
7
|
+
end
|
8
|
+
let(:auth) { client.auth }
|
9
|
+
|
10
|
+
describe "#request_token" do
|
11
|
+
let(:ttl) { 30 * 60 }
|
12
|
+
let(:capability) { { :foo => ["publish"] } }
|
13
|
+
|
14
|
+
it "returns the requested token" do
|
15
|
+
actual_token = auth.request_token(
|
16
|
+
ttl: ttl,
|
17
|
+
capability: capability
|
18
|
+
)
|
19
|
+
|
20
|
+
expect(actual_token.id).to match(/^#{app_id}\.[\w-]+$/)
|
21
|
+
expect(actual_token.key_id).to match(/^#{key_id}$/)
|
22
|
+
expect(actual_token.issued_at).to be_within(2).of(Time.now)
|
23
|
+
expect(actual_token.expires_at).to be_within(2).of(Time.now + ttl)
|
24
|
+
end
|
25
|
+
|
26
|
+
%w(client_id ttl timestamp capability nonce).each do |option|
|
27
|
+
context "option :#{option}", webmock: true do
|
28
|
+
let(:random) { SecureRandom.hex }
|
29
|
+
let(:options) { { option.to_sym => random } }
|
30
|
+
|
31
|
+
let(:token_response) { { access_token: {} }.to_json }
|
32
|
+
let!(:request_token_stub) do
|
33
|
+
stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
|
34
|
+
with(:body => hash_including({ option => random })).
|
35
|
+
to_return(:status => 201, :body => token_response, :headers => { 'Content-Type' => 'application/json' })
|
36
|
+
end
|
37
|
+
|
38
|
+
before { auth.request_token options }
|
39
|
+
|
40
|
+
it 'overrides default' do
|
41
|
+
expect(request_token_stub).to have_been_requested
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'with :key_id & :key_secret options', webmock: true do
|
47
|
+
let(:key_id) { SecureRandom.hex }
|
48
|
+
let(:key_secret) { SecureRandom.hex }
|
49
|
+
let(:nonce) { SecureRandom.hex }
|
50
|
+
let(:token_options) { { key_id: key_id, key_secret: key_secret, nonce: nonce, timestamp: Time.now.to_i } }
|
51
|
+
let(:token_request) { auth.create_token_request(token_options) }
|
52
|
+
let(:mac) do
|
53
|
+
hmac_for(token_request, key_secret)
|
54
|
+
end
|
55
|
+
|
56
|
+
let(:token_response) { { access_token: {} }.to_json }
|
57
|
+
let!(:request_token_stub) do
|
58
|
+
stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
|
59
|
+
with(:body => hash_including({ 'mac' => mac })).
|
60
|
+
to_return(:status => 201, :body => token_response, :headers => { 'Content-Type' => 'application/json' })
|
61
|
+
end
|
62
|
+
|
63
|
+
let!(:token) { auth.request_token(token_options) }
|
64
|
+
|
65
|
+
specify 'key_id is used in request and signing uses key_secret' do
|
66
|
+
expect(request_token_stub).to have_been_requested
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context "with :query_time option" do
|
71
|
+
let(:options) { { query_time: true } }
|
72
|
+
|
73
|
+
it 'queries the server for the time' do
|
74
|
+
expect(client).to receive(:time).and_call_original
|
75
|
+
auth.request_token(options)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context "without :query_time option" do
|
80
|
+
let(:options) { { query_time: false } }
|
81
|
+
|
82
|
+
it 'queries the server for the time' do
|
83
|
+
expect(client).to_not receive(:time)
|
84
|
+
auth.request_token(options)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'with :auth_url option', webmock: true do
|
89
|
+
let(:auth_url) { 'https://www.fictitious.com/get_token' }
|
90
|
+
let(:token_request) { { id: key_id }.to_json }
|
91
|
+
let(:token_response) { { access_token: { } }.to_json }
|
92
|
+
let(:query_params) { nil }
|
93
|
+
let(:headers) { nil }
|
94
|
+
let(:auth_method) { :get }
|
95
|
+
let(:options) do
|
96
|
+
{
|
97
|
+
auth_url: auth_url,
|
98
|
+
auth_params: query_params,
|
99
|
+
auth_headers: headers,
|
100
|
+
auth_method: auth_method
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
let!(:auth_url_request_stub) do
|
105
|
+
stub = stub_request(auth_method, auth_url)
|
106
|
+
stub.with(:query => hash_including(query_params)) unless query_params.nil?
|
107
|
+
stub.with(:header => hash_including(headers)) unless headers.nil?
|
108
|
+
stub.to_return(:status => 201, :body => token_request, :headers => { 'Content-Type' => 'application/json' })
|
109
|
+
end
|
110
|
+
|
111
|
+
let!(:request_token_stub) do
|
112
|
+
stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
|
113
|
+
with(:body => hash_including({ 'id' => key_id })).
|
114
|
+
to_return(:status => 201, :body => token_response, :headers => { 'Content-Type' => 'application/json' })
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'valid' do
|
118
|
+
before { auth.request_token options }
|
119
|
+
|
120
|
+
context 'and default options' do
|
121
|
+
it 'requests a token from :auth_url' do
|
122
|
+
expect(request_token_stub).to have_been_requested
|
123
|
+
expect(auth_url_request_stub).to have_been_requested
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
context 'with params' do
|
128
|
+
let(:query_params) { { 'key' => SecureRandom.hex } }
|
129
|
+
it 'requests a token from :auth_url' do
|
130
|
+
expect(request_token_stub).to have_been_requested
|
131
|
+
expect(auth_url_request_stub).to have_been_requested
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'with headers' do
|
136
|
+
let(:headers) { { 'key' => SecureRandom.hex } }
|
137
|
+
it 'requests a token from :auth_url' do
|
138
|
+
expect(request_token_stub).to have_been_requested
|
139
|
+
expect(auth_url_request_stub).to have_been_requested
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
context 'with POST' do
|
144
|
+
let(:auth_method) { :post }
|
145
|
+
it 'requests a token from :auth_url' do
|
146
|
+
expect(request_token_stub).to have_been_requested
|
147
|
+
expect(auth_url_request_stub).to have_been_requested
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'when response is invalid' do
|
153
|
+
context '500' do
|
154
|
+
let!(:auth_url_request_stub) do
|
155
|
+
stub_request(auth_method, auth_url).to_return(:status => 500)
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'raises ServerError' do
|
159
|
+
expect { auth.request_token options }.to raise_error(Ably::ServerError)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context 'XML' do
|
164
|
+
let!(:auth_url_request_stub) do
|
165
|
+
stub_request(auth_method, auth_url).
|
166
|
+
to_return(:status => 201, :body => '<xml></xml>', :headers => { 'Content-Type' => 'application/xml' })
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'raises InvalidResponseBody' do
|
170
|
+
expect { auth.request_token options }.to raise_error(Ably::InvalidResponseBody)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
context 'with auth_block' do
|
177
|
+
let(:client_id) { SecureRandom.hex }
|
178
|
+
let(:options) { { client_id: client_id } }
|
179
|
+
let!(:token) do
|
180
|
+
auth.request_token(options) do |block_options|
|
181
|
+
@block_called = true
|
182
|
+
@block_options = block_options
|
183
|
+
auth.create_token_request(client_id: client_id)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'calls the block' do
|
188
|
+
expect(@block_called).to eql(true)
|
189
|
+
expect(@block_options).to include(options)
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'uses the token request when requesting a new token' do
|
193
|
+
expect(token.client_id).to eql(client_id)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
describe '#authorise' do
|
199
|
+
context 'with no previous authorisation' do
|
200
|
+
let(:request_options) do
|
201
|
+
{ auth_url: 'http://somewhere.com/' }
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'has no current_token' do
|
205
|
+
expect(auth.current_token).to be_nil
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'passes all options to request_token' do
|
209
|
+
expect(auth).to receive(:request_token).with(request_options)
|
210
|
+
auth.authorise request_options
|
211
|
+
end
|
212
|
+
|
213
|
+
it 'returns a valid token' do
|
214
|
+
expect(auth.authorise).to be_a(Ably::Token)
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'issues a new token if option :force => true' do
|
218
|
+
expect { auth.authorise(force: true) }.to change { auth.current_token }
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
context 'with previous authorisation' do
|
223
|
+
before do
|
224
|
+
auth.authorise
|
225
|
+
expect(auth.current_token).to_not be_expired
|
226
|
+
end
|
227
|
+
|
228
|
+
it 'does not request a token if token is not expired' do
|
229
|
+
expect(auth).to_not receive(:request_token)
|
230
|
+
auth.authorise
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'requests a new token if token is expired' do
|
234
|
+
allow(auth.current_token).to receive(:expired?).and_return(true)
|
235
|
+
expect(auth).to receive(:request_token)
|
236
|
+
expect { auth.authorise }.to change { auth.current_token }
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'issues a new token if option :force => true' do
|
240
|
+
expect { auth.authorise(force: true) }.to change { auth.current_token }
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
describe "#create_token_request" do
|
246
|
+
let(:ttl) { 60 * 60 }
|
247
|
+
let(:capability) { { :foo => ["publish"] } }
|
248
|
+
let(:options) { Hash.new }
|
249
|
+
subject { auth.create_token_request(options) }
|
250
|
+
|
251
|
+
it "uses the key ID from the client" do
|
252
|
+
expect(subject[:id]).to eql(key_id)
|
253
|
+
end
|
254
|
+
|
255
|
+
it "uses the default TTL" do
|
256
|
+
expect(subject[:ttl]).to eql(Ably::Token::DEFAULTS[:ttl])
|
257
|
+
end
|
258
|
+
|
259
|
+
it "uses the default capability" do
|
260
|
+
expect(subject[:capability]).to eql(Ably::Token::DEFAULTS[:capability].to_json)
|
261
|
+
end
|
262
|
+
|
263
|
+
it "has a unique nonce" do
|
264
|
+
unique_nonces = 100.times.map { auth.create_token_request[:nonce] }
|
265
|
+
expect(unique_nonces.uniq.length).to eql(100)
|
266
|
+
end
|
267
|
+
|
268
|
+
it "has a nonce of at least 16 characters" do
|
269
|
+
expect(subject[:nonce].length).to be >= 16
|
270
|
+
end
|
271
|
+
|
272
|
+
%w(ttl capability nonce timestamp client_id).each do |attribute|
|
273
|
+
context "with option :#{attribute}" do
|
274
|
+
let(:option_value) { SecureRandom.hex }
|
275
|
+
before do
|
276
|
+
options[attribute.to_sym] = option_value
|
277
|
+
end
|
278
|
+
it "overrides default" do
|
279
|
+
expect(subject[attribute.to_sym]).to eql(option_value)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
context "invalid attributes" do
|
285
|
+
let(:options) { { nonce: 'valid', is_not_used_by_token_request: 'invalid' } }
|
286
|
+
specify 'are ignored' do
|
287
|
+
expect(subject.keys).to_not include(:is_not_used_by_token_request)
|
288
|
+
expect(subject.keys).to include(:nonce)
|
289
|
+
expect(subject[:nonce]).to eql('valid')
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
context "missing key ID and/or secret" do
|
294
|
+
let(:client) { Ably::Rest::Client.new(auth_url: 'http://example.com') }
|
295
|
+
|
296
|
+
it "should raise an exception if key secret is missing" do
|
297
|
+
expect { auth.create_token_request(key_id: 'id') }.to raise_error Ably::TokenRequestError
|
298
|
+
end
|
299
|
+
|
300
|
+
it "should raise an exception if key id is missing" do
|
301
|
+
expect { auth.create_token_request(key_secret: 'secret') }.to raise_error Ably::TokenRequestError
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
context "with :query_time option" do
|
306
|
+
let(:time) { Time.now - 30 }
|
307
|
+
let(:options) { { query_time: true } }
|
308
|
+
|
309
|
+
it 'queries the server for the time' do
|
310
|
+
expect(client).to receive(:time).and_return(time)
|
311
|
+
expect(subject[:timestamp]).to eql(time.to_i)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
context "signing" do
|
316
|
+
let(:options) do
|
317
|
+
{
|
318
|
+
id: SecureRandom.hex,
|
319
|
+
ttl: SecureRandom.hex,
|
320
|
+
capability: SecureRandom.hex,
|
321
|
+
client_id: SecureRandom.hex,
|
322
|
+
timestamp: SecureRandom.hex,
|
323
|
+
nonce: SecureRandom.hex
|
324
|
+
}
|
325
|
+
end
|
326
|
+
|
327
|
+
it 'generates a valid HMAC' do
|
328
|
+
hmac = hmac_for(options, key_secret)
|
329
|
+
expect(subject[:mac]).to eql(hmac)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
context "client with token authentication" do
|
335
|
+
let(:capability) { { :foo => ["publish"] } }
|
336
|
+
|
337
|
+
describe "with token_id argument" do
|
338
|
+
let(:ttl) { 60 * 60 }
|
339
|
+
let(:token) do
|
340
|
+
auth.request_token(
|
341
|
+
ttl: ttl,
|
342
|
+
capability: capability
|
343
|
+
)
|
344
|
+
end
|
345
|
+
let(:token_id) { token.id }
|
346
|
+
let(:token_auth_client) do
|
347
|
+
Ably::Rest::Client.new(token_id: token_id, environment: environment)
|
348
|
+
end
|
349
|
+
|
350
|
+
it "authenticates successfully" do
|
351
|
+
expect(token_auth_client.channel("foo").publish("event", "data")).to be_truthy
|
352
|
+
end
|
353
|
+
|
354
|
+
it "disallows publishing on unspecified capability channels" do
|
355
|
+
expect { token_auth_client.channel("bar").publish("event", "data") }.to raise_error do |error|
|
356
|
+
expect(error).to be_a(Ably::InvalidRequest)
|
357
|
+
expect(error.status).to eql(401)
|
358
|
+
expect(error.code).to eql(40160)
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
it "fails if timestamp is invalid" do
|
363
|
+
expect { auth.request_token(timestamp: Time.now.to_i - 180) }.to raise_error do |error|
|
364
|
+
expect(error).to be_a(Ably::InvalidRequest)
|
365
|
+
expect(error.status).to eql(401)
|
366
|
+
expect(error.code).to eql(40101)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
describe "implicit through client id" do
|
372
|
+
let(:client_id) { '999' }
|
373
|
+
let(:client) do
|
374
|
+
Ably::Rest::Client.new(api_key: api_key, client_id: client_id, environment: environment)
|
375
|
+
end
|
376
|
+
let(:token_id) { 'unique-token-id' }
|
377
|
+
let(:token_response) do
|
378
|
+
{
|
379
|
+
access_token: {
|
380
|
+
id: token_id
|
381
|
+
}
|
382
|
+
}.to_json
|
383
|
+
end
|
384
|
+
|
385
|
+
context 'stubbed', webmock: true do
|
386
|
+
let!(:request_token_stub) do
|
387
|
+
stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").
|
388
|
+
to_return(:status => 201, :body => token_response, :headers => { 'Content-Type' => 'application/json' })
|
389
|
+
end
|
390
|
+
let!(:publish_message_stub) do
|
391
|
+
stub_request(:post, "#{client.endpoint}/channels/foo/publish").
|
392
|
+
with(headers: { 'Authorization' => "Bearer #{encode64(token_id)}" }).
|
393
|
+
to_return(status: 201, body: '{}', headers: { 'Content-Type' => 'application/json' })
|
394
|
+
end
|
395
|
+
|
396
|
+
it "will create a token request" do
|
397
|
+
client.channel("foo").publish("event", "data")
|
398
|
+
expect(request_token_stub).to have_been_requested
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
context "will create a token" do
|
403
|
+
let(:token) { client.auth.current_token }
|
404
|
+
|
405
|
+
it "before a request is made" do
|
406
|
+
expect(token).to be_nil
|
407
|
+
end
|
408
|
+
|
409
|
+
it "when a message is published" do
|
410
|
+
expect(client.channel("foo").publish("event", "data")).to be_truthy
|
411
|
+
end
|
412
|
+
|
413
|
+
it "with capability and TTL defaults" do
|
414
|
+
client.channel("foo").publish("event", "data")
|
415
|
+
|
416
|
+
expect(token).to be_a(Ably::Token)
|
417
|
+
capability_with_str_key = Ably::Token::DEFAULTS[:capability]
|
418
|
+
capability = Hash[capability_with_str_key.keys.map(&:to_sym).zip(capability_with_str_key.values)]
|
419
|
+
expect(token.capability).to eql(capability)
|
420
|
+
expect(token.expires_at).to be_within(2).of(Time.now + Ably::Token::DEFAULTS[:ttl])
|
421
|
+
expect(token.client_id).to eql(client_id)
|
422
|
+
end
|
423
|
+
end
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
def hmac_for(token_request, secret)
|
428
|
+
text = token_request.values_at(
|
429
|
+
:id,
|
430
|
+
:ttl,
|
431
|
+
:capability,
|
432
|
+
:client_id,
|
433
|
+
:timestamp,
|
434
|
+
:nonce
|
435
|
+
).map { |t| "#{t}\n" }.join("")
|
436
|
+
|
437
|
+
encode64(
|
438
|
+
Digest::HMAC.digest(text, key_secret, Digest::SHA256)
|
439
|
+
)
|
440
|
+
end
|
441
|
+
end
|