ably 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +103 -0
  6. data/Rakefile +4 -0
  7. data/ably.gemspec +32 -0
  8. data/lib/ably.rb +11 -0
  9. data/lib/ably/auth.rb +381 -0
  10. data/lib/ably/exceptions.rb +16 -0
  11. data/lib/ably/realtime.rb +38 -0
  12. data/lib/ably/realtime/callbacks.rb +15 -0
  13. data/lib/ably/realtime/channel.rb +51 -0
  14. data/lib/ably/realtime/client.rb +82 -0
  15. data/lib/ably/realtime/connection.rb +61 -0
  16. data/lib/ably/rest.rb +15 -0
  17. data/lib/ably/rest/channel.rb +58 -0
  18. data/lib/ably/rest/client.rb +194 -0
  19. data/lib/ably/rest/middleware/exceptions.rb +42 -0
  20. data/lib/ably/rest/middleware/external_exceptions.rb +26 -0
  21. data/lib/ably/rest/middleware/parse_json.rb +15 -0
  22. data/lib/ably/rest/paged_resource.rb +107 -0
  23. data/lib/ably/rest/presence.rb +44 -0
  24. data/lib/ably/support.rb +14 -0
  25. data/lib/ably/token.rb +55 -0
  26. data/lib/ably/version.rb +3 -0
  27. data/spec/acceptance/realtime_client_spec.rb +12 -0
  28. data/spec/acceptance/rest/auth_spec.rb +441 -0
  29. data/spec/acceptance/rest/base_spec.rb +113 -0
  30. data/spec/acceptance/rest/channel_spec.rb +68 -0
  31. data/spec/acceptance/rest/presence_spec.rb +22 -0
  32. data/spec/acceptance/rest/stats_spec.rb +57 -0
  33. data/spec/acceptance/rest/time_spec.rb +14 -0
  34. data/spec/spec_helper.rb +31 -0
  35. data/spec/support/api_helper.rb +41 -0
  36. data/spec/support/test_app.rb +77 -0
  37. data/spec/unit/auth.rb +9 -0
  38. data/spec/unit/realtime_spec.rb +9 -0
  39. data/spec/unit/rest_spec.rb +99 -0
  40. data/spec/unit/token_spec.rb +90 -0
  41. 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
@@ -0,0 +1,14 @@
1
+ require "base64"
2
+
3
+ module Ably
4
+ module Support
5
+ protected
6
+ def encode64(text)
7
+ Base64.encode64(text).gsub("\n", '')
8
+ end
9
+
10
+ def user_agent
11
+ "Ably Ruby client #{Ably::VERSION} (https://ably.io)"
12
+ end
13
+ end
14
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Ably
2
+ VERSION = "0.1.0"
3
+ end
@@ -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