ably 0.8.5 → 0.8.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/CHANGELOG.md +42 -48
  4. data/SPEC.md +1099 -640
  5. data/ably.gemspec +10 -4
  6. data/lib/ably/auth.rb +155 -47
  7. data/lib/ably/exceptions.rb +2 -0
  8. data/lib/ably/models/channel_state_change.rb +2 -3
  9. data/lib/ably/models/connection_details.rb +54 -0
  10. data/lib/ably/models/protocol_message.rb +14 -4
  11. data/lib/ably/models/token_details.rb +13 -7
  12. data/lib/ably/models/token_request.rb +1 -2
  13. data/lib/ably/modules/ably.rb +3 -2
  14. data/lib/ably/modules/message_emitter.rb +1 -3
  15. data/lib/ably/modules/state_emitter.rb +2 -2
  16. data/lib/ably/realtime/auth.rb +6 -0
  17. data/lib/ably/realtime/channel/channel_manager.rb +2 -0
  18. data/lib/ably/realtime/channel.rb +15 -4
  19. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +11 -1
  20. data/lib/ably/realtime/client.rb +10 -3
  21. data/lib/ably/realtime/connection/connection_manager.rb +58 -54
  22. data/lib/ably/realtime/connection.rb +62 -6
  23. data/lib/ably/realtime/presence.rb +18 -5
  24. data/lib/ably/rest/channel.rb +9 -1
  25. data/lib/ably/rest/client.rb +32 -14
  26. data/lib/ably/rest/presence.rb +1 -1
  27. data/lib/ably/version.rb +1 -1
  28. data/lib/ably.rb +2 -0
  29. data/spec/acceptance/realtime/auth_spec.rb +251 -11
  30. data/spec/acceptance/realtime/channel_history_spec.rb +12 -2
  31. data/spec/acceptance/realtime/channel_spec.rb +316 -24
  32. data/spec/acceptance/realtime/client_spec.rb +93 -1
  33. data/spec/acceptance/realtime/connection_failures_spec.rb +177 -86
  34. data/spec/acceptance/realtime/connection_spec.rb +284 -60
  35. data/spec/acceptance/realtime/message_spec.rb +45 -6
  36. data/spec/acceptance/realtime/presence_history_spec.rb +4 -0
  37. data/spec/acceptance/realtime/presence_spec.rb +181 -49
  38. data/spec/acceptance/realtime/time_spec.rb +13 -0
  39. data/spec/acceptance/rest/auth_spec.rb +222 -4
  40. data/spec/acceptance/rest/channel_spec.rb +132 -1
  41. data/spec/acceptance/rest/client_spec.rb +129 -28
  42. data/spec/acceptance/rest/presence_spec.rb +7 -7
  43. data/spec/acceptance/rest/time_spec.rb +10 -0
  44. data/spec/shared/client_initializer_behaviour.rb +41 -17
  45. data/spec/spec_helper.rb +1 -0
  46. data/spec/support/debug_failure_helper.rb +16 -0
  47. data/spec/unit/models/connection_details_spec.rb +60 -0
  48. data/spec/unit/models/protocol_message_spec.rb +45 -0
  49. data/spec/unit/modules/event_emitter_spec.rb +3 -1
  50. data/spec/unit/realtime/channel_spec.rb +6 -5
  51. data/spec/unit/realtime/client_spec.rb +5 -1
  52. data/spec/unit/realtime/connection_spec.rb +5 -1
  53. data/spec/unit/realtime/realtime_spec.rb +5 -1
  54. metadata +54 -7
@@ -197,6 +197,85 @@ describe Ably::Auth do
197
197
  end
198
198
  end
199
199
 
200
+ context 'with :auth_url option merging', :webmock do
201
+ context 'with existing configured auth options' do
202
+ let(:client_id) { random_str }
203
+ let(:auth_url) { "https://www.fictitious.com/#{random_str}" }
204
+ let(:auth_method) { :get }
205
+ let(:auth_params) { { key: 'val', client_id: 'isOverridenByClient' } }
206
+ let(:auth_headers) { { 'Header-X' => 'val1', 'Header-Y' => 'val2' } }
207
+
208
+ let(:base_options) do
209
+ default_options.merge(
210
+ client_id: client_id,
211
+ auth_url: auth_url,
212
+ auth_params: auth_params,
213
+ auth_headers: auth_headers
214
+ )
215
+ end
216
+ let(:client_options) { base_options }
217
+
218
+ let!(:auth_request) do
219
+ stub_request(auth_method, auth_url).to_return(
220
+ :status => 201,
221
+ :body => '123123.12312321321312321', # token string
222
+ :headers => { 'Content-Type' => 'text/plain' }
223
+ )
224
+ end
225
+
226
+ let(:request_token_auth_options) { Hash.new }
227
+ let(:request_token_token_params) { Hash.new }
228
+ after do
229
+ client.auth.request_token(request_token_token_params, request_token_auth_options)
230
+ expect(auth_request).to have_been_requested
231
+ end
232
+
233
+ context 'using unspecified :auth_method' do
234
+ it 'requests a token using a GET request with provided headers, and merges client_id into auth_params' do
235
+ auth_request.with(headers: auth_headers)
236
+ auth_request.with(query: auth_params.merge(client_id: client_id))
237
+ end
238
+
239
+ context 'with provided token_params' do
240
+ let(:request_token_token_params) { { client_id: 'custom', key2: 'val2' } }
241
+
242
+ it 'merges provided token_params with existing auth_params and client_id' do
243
+ auth_request.with(query: auth_params.merge(client_id: client_id).merge(request_token_token_params))
244
+ end
245
+ end
246
+
247
+ context 'with provided auth option auth_params and auth_headers' do
248
+ let(:request_token_auth_options) { { auth_params: {}, auth_headers: {} } }
249
+
250
+ it 'replaces any preconfigured auth_params' do
251
+ auth_request.with(query: {}.merge(client_id: client_id))
252
+ auth_request.with(headers: { 'Accept'=>'*/*' }) # mock library needs at least one header, accept is default
253
+ end
254
+ end
255
+ end
256
+
257
+ context 'using :get :auth_method and query params in the URL' do
258
+ let(:auth_method) { :get }
259
+ let(:client_options) { base_options.merge(auth_method: :get, auth_url: "#{auth_url}?urlparam=true") }
260
+
261
+ it 'requests a token using a GET request with provided headers, and merges client_id into auth_params and existing URL querystring into new URL querystring' do
262
+ auth_request.with(headers: auth_headers)
263
+ auth_request.with(query: auth_params.merge(client_id: client_id).merge(urlparam: 'true'))
264
+ end
265
+ end
266
+
267
+ context 'using :post :auth_method' do
268
+ let(:auth_method) { :post }
269
+ let(:client_options) { base_options.merge(auth_method: :post) }
270
+
271
+ it 'requests a token using a POST request with provided headers, and merges client_id into auth_params as form-encoded post data' do
272
+ auth_request.with(headers: auth_headers)
273
+ auth_request.with(body: auth_params.merge(client_id: client_id))
274
+ end
275
+ end
276
+ end
277
+ end
278
+
200
279
  context 'with :auth_url option', :webmock do
201
280
  let(:auth_url) { 'https://www.fictitious.com/get_token' }
202
281
  let(:auth_url_response) { { keyName: key_name }.to_json }
@@ -365,6 +444,18 @@ describe Ably::Auth do
365
444
  it 'uses the token request returned from the callback when requesting a new token' do
366
445
  expect(request_token.client_id).to eql(client_id)
367
446
  end
447
+
448
+ context 'when authorised' do
449
+ before { auth.authorise(token_params, auth_callback: auth_callback) }
450
+
451
+ it "sets Auth#client_id to the new token's client_id" do
452
+ expect(auth.client_id).to eql(client_id)
453
+ end
454
+
455
+ it "sets Client#client_id to the new token's client_id" do
456
+ expect(client.client_id).to eql(client_id)
457
+ end
458
+ end
368
459
  end
369
460
 
370
461
  context 'that returns a TokenDetails JSON object' do
@@ -377,7 +468,11 @@ describe Ably::Auth do
377
468
  let(:capability_str) { JSON.dump(capability) }
378
469
 
379
470
  let!(:token_details) do
380
- auth.request_token(token_params, auth_callback: Proc.new do |token_params_arg|
471
+ auth.request_token(token_params, auth_callback: auth_callback)
472
+ end
473
+
474
+ let(:auth_callback) do
475
+ Proc.new do |token_params_arg|
381
476
  @block_called = true
382
477
  @block_params = token_params_arg
383
478
  {
@@ -388,7 +483,7 @@ describe Ably::Auth do
388
483
  'expires' => expires.to_i * 1000,
389
484
  'capability'=> capability_str
390
485
  }
391
- end)
486
+ end
392
487
  end
393
488
 
394
489
  it 'calls the Proc when authenticating to obtain the request token' do
@@ -404,6 +499,18 @@ describe Ably::Auth do
404
499
  expect(token_details.issued).to be_within(1).of(issued)
405
500
  expect(token_details.capability).to eql(capability)
406
501
  end
502
+
503
+ context 'when authorised' do
504
+ before { auth.authorise(token_params, auth_callback: auth_callback) }
505
+
506
+ it "sets Auth#client_id to the new token's client_id" do
507
+ expect(auth.client_id).to eql(client_id)
508
+ end
509
+
510
+ it "sets Client#client_id to the new token's client_id" do
511
+ expect(client.client_id).to eql(client_id)
512
+ end
513
+ end
407
514
  end
408
515
 
409
516
  context 'that returns a TokenDetails object' do
@@ -602,6 +709,37 @@ describe Ably::Auth do
602
709
  end
603
710
  end
604
711
  end
712
+
713
+ context 'with an explicit ClientOptions client_id' do
714
+ let(:client_id) { random_str }
715
+ let(:client_options) { default_options.merge(auth_callback: Proc.new { auth_token_object }, client_id: client_id) }
716
+ let(:auth_client) { Ably::Rest::Client.new(default_options.merge(key: api_key, client_id: 'invalid')) }
717
+
718
+ context 'and an incompatible client_id in a TokenDetails object passed to the auth callback' do
719
+ let(:auth_token_object) { auth_client.auth.request_token }
720
+
721
+ it 'rejects a TokenDetails object with an incompatible client_id and raises an exception' do
722
+ expect { client.auth.authorise({}, force: true) }.to raise_error Ably::Exceptions::IncompatibleClientId
723
+ end
724
+ end
725
+
726
+ context 'and an incompatible client_id in a TokenRequest object passed to the auth callback and raises an exception' do
727
+ let(:auth_token_object) { auth_client.auth.create_token_request }
728
+
729
+ it 'rejects a TokenRequests object with an incompatible client_id and raises an exception' do
730
+ expect { client.auth.authorise({}, force: true) }.to raise_error Ably::Exceptions::IncompatibleClientId
731
+ end
732
+ end
733
+
734
+ context 'and a token string without any retrievable client_id' do
735
+ let(:auth_token_object) { auth_client.auth.request_token(client_id: 'different').token }
736
+
737
+ it 'rejects a TokenRequests object with an incompatible client_id and raises an exception' do
738
+ client.auth.authorise({}, force: true)
739
+ expect(client.client_id).to eql(client_id)
740
+ end
741
+ end
742
+ end
605
743
  end
606
744
 
607
745
  describe '#create_token_request' do
@@ -799,7 +937,7 @@ describe Ably::Auth do
799
937
  end
800
938
  end
801
939
 
802
- context 'when implicit as a result of using :client id' do
940
+ context 'when implicit as a result of using :client_id' do
803
941
  let(:client_id) { '999' }
804
942
  let(:client) do
805
943
  Ably::Rest::Client.new(key: api_key, client_id: client_id, environment: environment, protocol: protocol)
@@ -849,11 +987,91 @@ describe Ably::Auth do
849
987
  expect(token.expires.to_i).to be_within(2).of(Time.now.to_i + Ably::Auth::TOKEN_DEFAULTS.fetch(:ttl))
850
988
  expect(token.client_id).to eq(client_id)
851
989
  end
990
+
991
+ specify '#client_id contains the client_id' do
992
+ expect(client.auth.client_id).to eql(client_id)
993
+ end
994
+ end
995
+ end
996
+
997
+ context 'when :client_id is provided in a token' do
998
+ let(:client_id) { '123' }
999
+ let(:token) do
1000
+ Ably::Rest::Client.new(key: api_key, environment: environment, protocol: protocol).auth.request_token(client_id: client_id)
1001
+ end
1002
+ let(:client) do
1003
+ Ably::Rest::Client.new(token: token, environment: environment, protocol: protocol)
1004
+ end
1005
+
1006
+ specify '#client_id contains the client_id' do
1007
+ expect(client.auth.client_id).to eql(client_id)
1008
+ end
1009
+ end
1010
+ end
1011
+
1012
+ describe '#client_id_validated?' do
1013
+ let(:auth) { Ably::Rest::Client.new(default_options.merge(key: api_key)).auth }
1014
+
1015
+ context 'when using basic auth' do
1016
+ let(:client_options) { default_options.merge(key: api_key) }
1017
+
1018
+ it 'is false as basic auth users do not have an identity' do
1019
+ expect(client.auth).to_not be_client_id_validated
1020
+ end
1021
+ end
1022
+
1023
+ context 'when using a token auth string for a token with a client_id' do
1024
+ let(:client_options) { default_options.merge(token: auth.request_token(client_id: 'present').token) }
1025
+
1026
+ it 'is false as identification is not possible from an opaque token string' do
1027
+ expect(client.auth).to_not be_client_id_validated
1028
+ end
1029
+ end
1030
+
1031
+ context 'when using a token' do
1032
+ context 'with a client_id' do
1033
+ let(:client_options) { default_options.merge(token: auth.request_token(client_id: 'present')) }
1034
+
1035
+ it 'is true' do
1036
+ expect(client.auth).to be_client_id_validated
1037
+ end
1038
+ end
1039
+
1040
+ context 'with no client_id (anonymous)' do
1041
+ let(:client_options) { default_options.merge(token: auth.request_token(client_id: nil)) }
1042
+
1043
+ it 'is true' do
1044
+ expect(client.auth).to be_client_id_validated
1045
+ end
1046
+ end
1047
+
1048
+ context 'with a wildcard client_id (anonymous)' do
1049
+ let(:client_options) { default_options.merge(token: auth.request_token(client_id: '*')) }
1050
+
1051
+ it 'is false' do
1052
+ expect(client.auth).to be_client_id_validated
1053
+ end
1054
+ end
1055
+ end
1056
+
1057
+ context 'when using a token request with a client_id' do
1058
+ let(:client_options) { default_options.merge(token: auth.create_token_request(client_id: 'present')) }
1059
+
1060
+ it 'is not true as identification is not confirmed until authenticated' do
1061
+ expect(client.auth).to_not be_client_id_validated
1062
+ end
1063
+
1064
+ context 'after authentication' do
1065
+ before { client.channel('test').publish('a') }
1066
+
1067
+ it 'is true as identification is completed during implicit authentication' do
1068
+ expect(client.auth).to be_client_id_validated
1069
+ end
852
1070
  end
853
1071
  end
854
1072
  end
855
1073
 
856
- context 'when using an :key and basic auth' do
1074
+ context 'when using a :key and basic auth' do
857
1075
  specify '#using_token_auth? is false' do
858
1076
  expect(auth).to_not be_using_token_auth
859
1077
  end
@@ -125,6 +125,137 @@ describe Ably::Rest::Channel do
125
125
  end
126
126
  end
127
127
  end
128
+
129
+ context 'identified clients' do
130
+ context 'when authenticated with a wildcard client_id' do
131
+ let(:token) { Ably::Rest::Client.new(default_options).auth.request_token(client_id: '*') }
132
+ let(:client_options) { default_options.merge(key: nil, token: token) }
133
+ let(:client) { Ably::Rest::Client.new(client_options) }
134
+ let(:channel) { client.channels.get(channel_name) }
135
+
136
+ context 'with a valid client_id in the message' do
137
+ it 'succeeds' do
138
+ channel.publish([name: 'event', client_id: 'valid'])
139
+ channel.history do |messages|
140
+ expect(messages.first.client_id).to eql('valid')
141
+ end
142
+ end
143
+ end
144
+
145
+ context 'with a wildcard client_id in the message' do
146
+ it 'throws an exception' do
147
+ expect { channel.publish([name: 'event', client_id: '*']) }.to raise_error Ably::Exceptions::IncompatibleClientId
148
+ end
149
+ end
150
+
151
+ context 'with an empty client_id in the message' do
152
+ it 'succeeds and publishes without a client_id' do
153
+ channel.publish([name: 'event', client_id: nil])
154
+ channel.history do |messages|
155
+ expect(messages.first.client_id).to eql('valid')
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ context 'when authenticated with a Token string with an implicit client_id' do
162
+ let(:token) { Ably::Rest::Client.new(default_options).auth.request_token(client_id: 'valid').token }
163
+ let(:client_options) { default_options.merge(key: nil, token: token) }
164
+ let(:client) { Ably::Rest::Client.new(client_options) }
165
+ let(:channel) { client.channels.get(channel_name) }
166
+
167
+ context 'without having a confirmed identity' do
168
+ context 'with a valid client_id in the message' do
169
+ it 'succeeds' do
170
+ channel.publish([name: 'event', client_id: 'valid'])
171
+ channel.history do |messages|
172
+ expect(messages.first.client_id).to eql('valid')
173
+ end
174
+ end
175
+ end
176
+
177
+ context 'with an invalid client_id in the message' do
178
+ it 'succeeds in the client library but then fails when published to Ably' do
179
+ expect { channel.publish([name: 'event', client_id: 'invalid']) }.to raise_error Ably::Exceptions::InvalidRequest, /mismatched clientId/
180
+ end
181
+ end
182
+
183
+ context 'with an empty client_id in the message' do
184
+ it 'succeeds and publishes with an implicit client_id' do
185
+ channel.publish([name: 'event', client_id: nil])
186
+ channel.history do |messages|
187
+ expect(messages.first.client_id).to eql('valid')
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ context 'when authenticated with TokenDetails with a valid client_id' do
195
+ let(:token) { Ably::Rest::Client.new(default_options).auth.request_token(client_id: 'valid') }
196
+ let(:client_options) { default_options.merge(key: nil, token: token) }
197
+ let(:client) { Ably::Rest::Client.new(client_options) }
198
+ let(:channel) { client.channels.get(channel_name) }
199
+
200
+ context 'with a valid client_id in the message' do
201
+ it 'succeeds' do
202
+ channel.publish([name: 'event', client_id: 'valid'])
203
+ channel.history do |messages|
204
+ expect(messages.first.client_id).to eql('valid')
205
+ end
206
+ end
207
+ end
208
+
209
+ context 'with a wildcard client_id in the message' do
210
+ it 'throws an exception' do
211
+ expect { channel.publish([name: 'event', client_id: '*']) }.to raise_error Ably::Exceptions::IncompatibleClientId
212
+ end
213
+ end
214
+
215
+ context 'with an invalid client_id in the message' do
216
+ it 'throws an exception' do
217
+ expect { channel.publish([name: 'event', client_id: 'invalid']) }.to raise_error Ably::Exceptions::IncompatibleClientId
218
+ end
219
+ end
220
+
221
+ context 'with an empty client_id in the message' do
222
+ it 'succeeds and publishes with an implicit client_id' do
223
+ channel.publish([name: 'event', client_id: nil])
224
+ channel.history do |messages|
225
+ expect(messages.first.client_id).to eql('valid')
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ context 'when anonymous and no client_id' do
232
+ let(:token) { Ably::Rest::Client.new(default_options).auth.request_token(client_id: nil) }
233
+ let(:client_options) { default_options.merge(key: nil, token: token) }
234
+ let(:client) { Ably::Rest::Client.new(client_options) }
235
+ let(:channel) { client.channels.get(channel_name) }
236
+
237
+ context 'with a client_id in the message' do
238
+ it 'throws an exception' do
239
+ expect { channel.publish([name: 'event', client_id: '*']) }.to raise_error Ably::Exceptions::IncompatibleClientId
240
+ end
241
+ end
242
+
243
+ context 'with a wildcard client_id in the message' do
244
+ it 'throws an exception' do
245
+ expect { channel.publish([name: 'event', client_id: '*']) }.to raise_error Ably::Exceptions::IncompatibleClientId
246
+ end
247
+ end
248
+
249
+ context 'with an empty client_id in the message' do
250
+ it 'succeeds and publishes with an implicit client_id' do
251
+ channel.publish([name: 'event', client_id: nil])
252
+ channel.history do |messages|
253
+ expect(messages.first.client_id).to be_nil
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
128
259
  end
129
260
 
130
261
  describe '#history' do
@@ -246,7 +377,7 @@ describe Ably::Rest::Channel do
246
377
  let!(:history_stub) {
247
378
  query_params = default_history_options
248
379
  .merge(option => milliseconds).map { |k, v| "#{k}=#{v}" }.join('&')
249
- stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/messages?#{query_params}").
380
+ stub_request(:get, "#{endpoint}/channels/#{Addressable::URI.encode(channel_name)}/messages?#{query_params}").
250
381
  to_return(:body => '{}', :headers => { 'Content-Type' => 'application/json' })
251
382
  }
252
383
 
@@ -8,7 +8,7 @@ describe Ably::Rest::Client do
8
8
 
9
9
  let(:client) { Ably::Rest::Client.new(client_options) }
10
10
 
11
- connection_retry = Ably::Rest::Client::CONNECTION_RETRY
11
+ http_defaults = Ably::Rest::Client::HTTP_DEFAULTS
12
12
 
13
13
  def encode64(text)
14
14
  Base64.encode64(text).gsub("\n", '')
@@ -50,6 +50,12 @@ describe Ably::Rest::Client do
50
50
  end
51
51
  end
52
52
 
53
+ context 'with an invalid wildcard "*" :client_id' do
54
+ it 'raises an exception' do
55
+ expect { Ably::Rest::Client.new(client_options.merge(key: api_key, client_id: '*')) }.to raise_error ArgumentError
56
+ end
57
+ end
58
+
53
59
  context 'with an :auth_callback Proc' do
54
60
  let(:client) { Ably::Rest::Client.new(client_options.merge(auth_callback: Proc.new { token_request })) }
55
61
 
@@ -83,7 +89,7 @@ describe Ably::Rest::Client do
83
89
 
84
90
  context 'before any REST request' do
85
91
  before do
86
- expect(client.auth).to receive(:token_request_from_auth_url).with(token_request_url, hash_including(:auth_method => :get)).once do
92
+ expect(client.auth).to receive(:token_request_from_auth_url).with(token_request_url, hash_including(:auth_method => :get), anything).once do
87
93
  client.auth.create_token_request(client_id: client_id)
88
94
  end
89
95
  end
@@ -154,8 +160,10 @@ describe Ably::Rest::Client do
154
160
  send("token_request_#{@request_index > 2 ? 'next' : @request_index}")
155
161
  end))
156
162
  end
157
- let(:token_request_1) { client.auth.create_token_request({}, token_request_options.merge(client_id: random_str)) }
158
- let(:token_request_2) { client.auth.create_token_request({}, token_request_options.merge(client_id: random_str)) }
163
+ let(:client_id) { random_str }
164
+ let(:client_id_2) { client_id }
165
+ let(:token_request_1) { client.auth.create_token_request({}, token_request_options.merge(client_id: client_id)) }
166
+ let(:token_request_2) { client.auth.create_token_request({}, token_request_options.merge(client_id: client_id_2)) }
159
167
 
160
168
  # If token expires against whilst runnig tests in a slower CI environment then use this token
161
169
  let(:token_request_next) { client.auth.create_token_request({}, token_request_options.merge(client_id: random_str)) }
@@ -177,6 +185,16 @@ describe Ably::Rest::Client do
177
185
  expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token_details }
178
186
  expect(client.auth.current_token_details.client_id).to eql(token_request_2.client_id)
179
187
  end
188
+
189
+ context 'with a different client_id in the subsequent token' do
190
+ let(:client_id_2) { random_str }
191
+
192
+ it 'fails to authenticate and raises an exception' do
193
+ client.channel('channel_name').publish('event', 'message')
194
+ sleep 1
195
+ expect { client.channel('channel_name').publish('event', 'message') }.to raise_error(Ably::Exceptions::IncompatibleClientId)
196
+ end
197
+ end
180
198
  end
181
199
 
182
200
  context 'when token has not expired' do
@@ -195,25 +213,53 @@ describe Ably::Rest::Client do
195
213
  end
196
214
 
197
215
  context 'connection transport' do
198
- let(:client_options) { default_options.merge(key: api_key) }
216
+ context 'defaults' do
217
+ let(:client_options) { default_options.merge(key: api_key) }
218
+
219
+ context 'for default host' do
220
+ it "is configured to timeout connection opening in #{http_defaults.fetch(:open_timeout)} seconds" do
221
+ expect(client.connection.options.open_timeout).to eql(http_defaults.fetch(:open_timeout))
222
+ end
199
223
 
200
- context 'for default host' do
201
- it "is configured to timeout connection opening in #{connection_retry.fetch(:single_request_open_timeout)} seconds" do
202
- expect(client.connection.options.open_timeout).to eql(connection_retry.fetch(:single_request_open_timeout))
224
+ it "is configured to timeout connection requests in #{http_defaults.fetch(:request_timeout)} seconds" do
225
+ expect(client.connection.options.timeout).to eql(http_defaults.fetch(:request_timeout))
226
+ end
203
227
  end
204
228
 
205
- it "is configured to timeout connection requests in #{connection_retry.fetch(:single_request_timeout)} seconds" do
206
- expect(client.connection.options.timeout).to eql(connection_retry.fetch(:single_request_timeout))
229
+ context 'for the fallback hosts' do
230
+ it "is configured to timeout connection opening in #{http_defaults.fetch(:open_timeout)} seconds" do
231
+ expect(client.fallback_connection.options.open_timeout).to eql(http_defaults.fetch(:open_timeout))
232
+ end
233
+
234
+ it "is configured to timeout connection requests in #{http_defaults.fetch(:request_timeout)} seconds" do
235
+ expect(client.fallback_connection.options.timeout).to eql(http_defaults.fetch(:request_timeout))
236
+ end
207
237
  end
208
238
  end
209
239
 
210
- context 'for the fallback hosts' do
211
- it "is configured to timeout connection opening in #{connection_retry.fetch(:single_request_open_timeout)} seconds" do
212
- expect(client.fallback_connection.options.open_timeout).to eql(connection_retry.fetch(:single_request_open_timeout))
240
+ context 'with custom http_open_timeout and http_request_timeout options' do
241
+ let(:http_open_timeout) { 999 }
242
+ let(:http_request_timeout) { 666 }
243
+ let(:client_options) { default_options.merge(key: api_key, http_open_timeout: http_open_timeout, http_request_timeout: http_request_timeout) }
244
+
245
+ context 'for default host' do
246
+ it 'is configured to use custom open timeout' do
247
+ expect(client.connection.options.open_timeout).to eql(http_open_timeout)
248
+ end
249
+
250
+ it 'is configured to use custom request timeout' do
251
+ expect(client.connection.options.timeout).to eql(http_request_timeout)
252
+ end
213
253
  end
214
254
 
215
- it "is configured to timeout connection requests in #{connection_retry.fetch(:single_request_timeout)} seconds" do
216
- expect(client.fallback_connection.options.timeout).to eql(connection_retry.fetch(:single_request_timeout))
255
+ context 'for the fallback hosts' do
256
+ it "is configured to timeout connection opening in #{http_defaults.fetch(:open_timeout)} seconds" do
257
+ expect(client.fallback_connection.options.open_timeout).to eql(http_open_timeout)
258
+ end
259
+
260
+ it "is configured to timeout connection requests in #{http_defaults.fetch(:request_timeout)} seconds" do
261
+ expect(client.fallback_connection.options.timeout).to eql(http_request_timeout)
262
+ end
217
263
  end
218
264
  end
219
265
  end
@@ -249,19 +295,20 @@ describe Ably::Rest::Client do
249
295
 
250
296
  context 'when environment is production' do
251
297
  let(:custom_hosts) { %w(A.ably-realtime.com B.ably-realtime.com) }
252
- let(:max_attempts) { 2 }
253
- let(:cumulative_timeout) { 0.5 }
254
- let(:client_options) { default_options.merge(environment: nil, key: api_key) }
298
+ let(:max_retry_count) { 2 }
299
+ let(:max_retry_duration) { 0.5 }
255
300
  let(:fallback_block) { Proc.new { raise Faraday::SSLError.new('ssl error message') } }
301
+ let(:client_options) do
302
+ default_options.merge(
303
+ environment: nil,
304
+ key: api_key,
305
+ http_max_retry_duration: max_retry_duration,
306
+ http_max_retry_count: max_retry_count
307
+ )
308
+ end
256
309
 
257
310
  before do
258
311
  stub_const 'Ably::FALLBACK_HOSTS', custom_hosts
259
- stub_const 'Ably::Rest::Client::CONNECTION_RETRY', {
260
- single_request_open_timeout: 4,
261
- single_request_timeout: 15,
262
- cumulative_request_open_timeout: cumulative_timeout,
263
- max_retry_attempts: max_attempts
264
- }
265
312
  end
266
313
 
267
314
  let!(:first_fallback_request_stub) do
@@ -279,17 +326,17 @@ describe Ably::Rest::Client do
279
326
  end
280
327
  end
281
328
 
282
- it "tries fallback hosts #{connection_retry[:max_retry_attempts]} times" do
329
+ it "tries fallback hosts #{http_defaults.fetch(:max_retry_count)} times" do
283
330
  expect { publish_block.call }.to raise_error Ably::Exceptions::ConnectionError, /ssl error message/
284
331
  expect(default_host_request_stub).to have_been_requested
285
332
  expect(first_fallback_request_stub).to have_been_requested
286
333
  expect(second_fallback_request_stub).to have_been_requested
287
334
  end
288
335
 
289
- context "and the total request time exeeds #{connection_retry[:cumulative_request_open_timeout]} seconds" do
336
+ context "and the total request time exeeds #{http_defaults.fetch(:max_retry_duration)} seconds" do
290
337
  let!(:default_host_request_stub) do
291
338
  stub_request(:post, "https://#{api_key}@#{Ably::Rest::Client::DOMAIN}#{path}").to_return do
292
- sleep cumulative_timeout * 1.5
339
+ sleep max_retry_duration * 1.5
293
340
  raise Faraday::TimeoutError.new('timeout error message')
294
341
  end
295
342
  end
@@ -310,7 +357,7 @@ describe Ably::Rest::Client do
310
357
  end
311
358
  end
312
359
 
313
- it "tries fallback hosts #{connection_retry[:max_retry_attempts]} times" do
360
+ it "tries fallback hosts #{http_defaults.fetch(:max_retry_count)} times" do
314
361
  expect { publish_block.call }.to raise_error Ably::Exceptions::ConnectionError, /ssl error message/
315
362
  expect(default_host_request_stub).to have_been_requested
316
363
  expect(first_fallback_request_stub).to have_been_requested
@@ -429,6 +476,60 @@ describe Ably::Rest::Client do
429
476
  end
430
477
  end
431
478
 
479
+ context 'HTTP configuration options' do
480
+ let(:client_options) { default_options.merge(key: api_key) }
481
+
482
+ context 'defaults' do
483
+ specify '#http_open_timeout is 4s' do
484
+ expect(client.http_defaults[:open_timeout]).to eql(4)
485
+ end
486
+
487
+ specify '#http_request_timeout is 15s' do
488
+ expect(client.http_defaults[:request_timeout]).to eql(15)
489
+ end
490
+
491
+ specify '#http_max_retry_count is 3' do
492
+ expect(client.http_defaults[:max_retry_count]).to eql(3)
493
+ end
494
+
495
+ specify '#http_max_retry_duration is 10s' do
496
+ expect(client.http_defaults[:max_retry_duration]).to eql(10)
497
+ end
498
+ end
499
+
500
+ context 'configured' do
501
+ let(:client_options) do
502
+ default_options.merge(
503
+ key: api_key,
504
+ http_open_timeout: 1,
505
+ http_request_timeout: 2,
506
+ http_max_retry_count: 33,
507
+ http_max_retry_duration: 4
508
+ )
509
+ end
510
+
511
+ specify '#http_open_timeout uses provided value' do
512
+ expect(client.http_defaults[:open_timeout]).to eql(1)
513
+ end
514
+
515
+ specify '#http_request_timeout uses provided value' do
516
+ expect(client.http_defaults[:request_timeout]).to eql(2)
517
+ end
518
+
519
+ specify '#http_max_retry_count uses provided value' do
520
+ expect(client.http_defaults[:max_retry_count]).to eql(33)
521
+ end
522
+
523
+ specify '#http_max_retry_duration uses provided value' do
524
+ expect(client.http_defaults[:max_retry_duration]).to eql(4)
525
+ end
526
+ end
527
+
528
+ it 'is frozen' do
529
+ expect(client.http_defaults).to be_frozen
530
+ end
531
+ end
532
+
432
533
  context '#auth' do
433
534
  let(:dummy_auth_url) { 'http://dummy.url' }
434
535
  let(:unique_ttl) { 1234 }