ably 0.7.2 → 0.7.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/LICENSE.txt +1 -1
  2. data/README.md +107 -24
  3. data/SPEC.md +531 -398
  4. data/lib/ably/auth.rb +23 -15
  5. data/lib/ably/exceptions.rb +9 -0
  6. data/lib/ably/models/message.rb +17 -9
  7. data/lib/ably/models/paginated_resource.rb +12 -8
  8. data/lib/ably/models/presence_message.rb +18 -10
  9. data/lib/ably/models/protocol_message.rb +15 -4
  10. data/lib/ably/modules/async_wrapper.rb +4 -3
  11. data/lib/ably/modules/event_emitter.rb +31 -2
  12. data/lib/ably/modules/message_emitter.rb +77 -0
  13. data/lib/ably/modules/safe_deferrable.rb +71 -0
  14. data/lib/ably/modules/safe_yield.rb +41 -0
  15. data/lib/ably/modules/state_emitter.rb +28 -8
  16. data/lib/ably/realtime.rb +0 -5
  17. data/lib/ably/realtime/channel.rb +24 -29
  18. data/lib/ably/realtime/channel/channel_manager.rb +54 -11
  19. data/lib/ably/realtime/channel/channel_state_machine.rb +21 -6
  20. data/lib/ably/realtime/client.rb +7 -2
  21. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +29 -26
  22. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +4 -4
  23. data/lib/ably/realtime/connection.rb +41 -9
  24. data/lib/ably/realtime/connection/connection_manager.rb +72 -24
  25. data/lib/ably/realtime/connection/connection_state_machine.rb +26 -4
  26. data/lib/ably/realtime/connection/websocket_transport.rb +19 -6
  27. data/lib/ably/realtime/presence.rb +74 -208
  28. data/lib/ably/realtime/presence/members_map.rb +264 -0
  29. data/lib/ably/realtime/presence/presence_manager.rb +59 -0
  30. data/lib/ably/realtime/presence/presence_state_machine.rb +64 -0
  31. data/lib/ably/rest/channel.rb +1 -1
  32. data/lib/ably/rest/client.rb +6 -2
  33. data/lib/ably/rest/presence.rb +1 -1
  34. data/lib/ably/util/pub_sub.rb +3 -1
  35. data/lib/ably/util/safe_deferrable.rb +18 -0
  36. data/lib/ably/version.rb +1 -1
  37. data/spec/acceptance/realtime/channel_history_spec.rb +2 -2
  38. data/spec/acceptance/realtime/channel_spec.rb +28 -6
  39. data/spec/acceptance/realtime/connection_failures_spec.rb +116 -46
  40. data/spec/acceptance/realtime/connection_spec.rb +55 -10
  41. data/spec/acceptance/realtime/message_spec.rb +32 -0
  42. data/spec/acceptance/realtime/presence_spec.rb +456 -96
  43. data/spec/acceptance/realtime/stats_spec.rb +2 -2
  44. data/spec/acceptance/realtime/time_spec.rb +2 -2
  45. data/spec/acceptance/rest/auth_spec.rb +75 -7
  46. data/spec/shared/client_initializer_behaviour.rb +8 -0
  47. data/spec/shared/safe_deferrable_behaviour.rb +71 -0
  48. data/spec/support/api_helper.rb +1 -1
  49. data/spec/support/event_machine_helper.rb +1 -1
  50. data/spec/support/test_app.rb +13 -7
  51. data/spec/unit/models/message_spec.rb +15 -14
  52. data/spec/unit/models/paginated_resource_spec.rb +4 -4
  53. data/spec/unit/models/presence_message_spec.rb +17 -17
  54. data/spec/unit/models/stat_spec.rb +4 -4
  55. data/spec/unit/modules/async_wrapper_spec.rb +28 -9
  56. data/spec/unit/modules/event_emitter_spec.rb +50 -0
  57. data/spec/unit/modules/state_emitter_spec.rb +76 -2
  58. data/spec/unit/realtime/channel_spec.rb +51 -20
  59. data/spec/unit/realtime/channels_spec.rb +3 -3
  60. data/spec/unit/realtime/connection_spec.rb +30 -0
  61. data/spec/unit/realtime/presence_spec.rb +52 -26
  62. data/spec/unit/realtime/safe_deferrable_spec.rb +12 -0
  63. metadata +85 -39
  64. checksums.yaml +0 -7
  65. data/.ruby-version.old +0 -1
@@ -14,8 +14,8 @@ describe Ably::Realtime::Client, '#stats', :event_machine do
14
14
  end
15
15
  end
16
16
 
17
- it 'should return a Deferrable object' do
18
- expect(client.stats).to be_a(EventMachine::Deferrable)
17
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
18
+ expect(client.stats).to be_a(Ably::Util::SafeDeferrable)
19
19
  stop_reactor
20
20
  end
21
21
  end
@@ -16,9 +16,9 @@ describe Ably::Realtime::Client, '#time', :event_machine do
16
16
  end
17
17
  end
18
18
 
19
- it 'should return a deferrable object' do
19
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
20
20
  run_reactor do
21
- expect(client.time).to be_a(EventMachine::Deferrable)
21
+ expect(client.time).to be_a(Ably::Util::SafeDeferrable)
22
22
  stop_reactor
23
23
  end
24
24
  end
@@ -146,7 +146,7 @@ describe Ably::Auth do
146
146
 
147
147
  context 'with :auth_url option', :webmock do
148
148
  let(:auth_url) { 'https://www.fictitious.com/get_token' }
149
- let(:token_request) { { id: key_id } }
149
+ let(:auth_url_response) { { id: key_id } }
150
150
  let(:token_response) { { access_token: { } } }
151
151
  let(:query_params) { nil }
152
152
  let(:headers) { nil }
@@ -166,7 +166,7 @@ describe Ably::Auth do
166
166
  stub.with(:headers => headers) unless headers.nil?
167
167
  stub.to_return(
168
168
  :status => 201,
169
- :body => token_request.to_json,
169
+ :body => auth_url_response.to_json,
170
170
  :headers => { 'Content-Type' => 'application/json' }
171
171
  )
172
172
  end
@@ -182,14 +182,18 @@ describe Ably::Auth do
182
182
  )
183
183
  end
184
184
 
185
- context 'when response is valid' do
186
- before { auth.request_token options }
185
+ context 'when response from :auth_url is a valid token request' do
186
+ let!(:token) { auth.request_token(options) }
187
187
 
188
188
  it 'requests a token from :auth_url using an HTTP GET request' do
189
189
  expect(request_token_stub).to have_been_requested
190
190
  expect(auth_url_request_stub).to have_been_requested
191
191
  end
192
192
 
193
+ it 'returns a valid token generated from the token request' do
194
+ expect(token).to be_a(Ably::Models::Token)
195
+ end
196
+
193
197
  context 'with :query_params' do
194
198
  let(:query_params) { { 'key' => random_str } }
195
199
 
@@ -216,6 +220,32 @@ describe Ably::Auth do
216
220
  end
217
221
  end
218
222
 
223
+ context 'when response from :auth_url is a token' do
224
+ let(:token_id) { 'J_0Tlg.D7AVZkdOZW-PqNNGvCSp38' }
225
+ let(:issued_at) { Time.now }
226
+ let(:expires) { Time.now + 60}
227
+ let(:capability) { {'foo'=>['publish']} }
228
+ let(:auth_url_response) do
229
+ {
230
+ 'id' => token_id,
231
+ 'key' => 'J_0Tlg.NxCRig',
232
+ 'issued_at' => issued_at.to_i,
233
+ 'expires' => expires.to_i,
234
+ 'capability'=> capability
235
+ }
236
+ end
237
+
238
+ let!(:token) { auth.request_token(options) }
239
+
240
+ it 'returns a Token created from the token JSON' do
241
+ expect(request_token_stub).to_not have_been_requested
242
+ expect(token.id).to eql(token_id)
243
+ expect(token.expires_at).to be_within(1).of(expires)
244
+ expect(token.issued_at).to be_within(1).of(issued_at)
245
+ expect(token.capability.to_json).to eql(capability.to_json)
246
+ end
247
+ end
248
+
219
249
  context 'when response is invalid' do
220
250
  context '500' do
221
251
  let!(:auth_url_request_stub) do
@@ -240,10 +270,10 @@ describe Ably::Auth do
240
270
  end
241
271
  end
242
272
 
243
- context 'with token_request_block' do
273
+ context 'with token_request_block that returns a token request' do
244
274
  let(:client_id) { random_str }
245
275
  let(:options) { { client_id: client_id } }
246
- let!(:token) do
276
+ let!(:request_token) do
247
277
  auth.request_token(options) do |block_options|
248
278
  @block_called = true
249
279
  @block_options = block_options
@@ -257,7 +287,45 @@ describe Ably::Auth do
257
287
  end
258
288
 
259
289
  it 'uses the token request from the block when requesting a new token' do
260
- expect(token.client_id).to eql(client_id)
290
+ expect(request_token.client_id).to eql(client_id)
291
+ end
292
+ end
293
+
294
+ context 'with token_request_block that returns a token' do
295
+ let(:client_id) { random_str }
296
+ let(:options) { { client_id: client_id } }
297
+ let(:token_id) { 'J_0Tlg.D7AVZkdOZW-PqNNGvCSp38' }
298
+ let(:issued_at) { Time.now }
299
+ let(:expires) { Time.now + 60}
300
+ let(:capability) { {'foo'=>['publish']} }
301
+
302
+ let!(:request_token) do
303
+ auth.request_token(options) do |block_options|
304
+ @block_called = true
305
+ @block_options = block_options
306
+ {
307
+ 'id' => token_id,
308
+ 'key' => 'J_0Tlg.NxCRig',
309
+ 'client_id' => client_id,
310
+ 'issued_at' => issued_at.to_i,
311
+ 'expires' => expires.to_i,
312
+ 'capability'=> capability
313
+ }
314
+ end
315
+ end
316
+
317
+ it 'calls the block when authenticating to obtain the request token' do
318
+ expect(@block_called).to eql(true)
319
+ expect(@block_options).to include(options)
320
+ end
321
+
322
+ it 'uses the token request from the block when requesting a new token' do
323
+ expect(request_token).to be_a(Ably::Models::Token)
324
+ expect(request_token.id).to eql(token_id)
325
+ expect(request_token.client_id).to eql(client_id)
326
+ expect(request_token.expires_at).to be_within(1).of(expires)
327
+ expect(request_token.issued_at).to be_within(1).of(issued_at)
328
+ expect(request_token.capability.to_json).to eql(capability.to_json)
261
329
  end
262
330
  end
263
331
  end
@@ -113,6 +113,14 @@ shared_examples 'a client initializer' do
113
113
  end
114
114
  end
115
115
 
116
+ context 'with a string token key instead of options hash' do
117
+ let(:client_options) { 'app.kjhkasjhdsakdh127g7g1271' }
118
+
119
+ it 'sets the token_id' do
120
+ expect(subject.auth.token_id).to eql(client_options)
121
+ end
122
+ end
123
+
116
124
  context 'with token' do
117
125
  let(:client_options) { { token_id: 'token' } }
118
126
 
@@ -0,0 +1,71 @@
1
+ # encoding: utf-8
2
+
3
+ shared_examples 'a safe Deferrable' do
4
+ let(:logger) { instance_double('Logger') }
5
+ let(:arguments) { [random_str] }
6
+ let(:errback_calls) { [] }
7
+ let(:success_calls) { [] }
8
+ let(:exception) { StandardError.new("Intentional error") }
9
+
10
+ before do
11
+ allow(subject).to receive(:logger).and_return(logger)
12
+ end
13
+
14
+ context '#errback' do
15
+ it 'adds a callback that is called when #fail is called' do
16
+ subject.errback do |*args|
17
+ expect(args).to eql(arguments)
18
+ end
19
+ subject.fail *arguments
20
+ end
21
+
22
+ it 'catches exceptions in the callback and logs the error to the logger' do
23
+ expect(subject.send(:logger)).to receive(:error).with(/#{exception.message}/)
24
+ subject.errback do
25
+ raise exception
26
+ end
27
+ subject.fail
28
+ end
29
+ end
30
+
31
+ context '#fail' do
32
+ it 'calls the callbacks defined with #errback, but not the ones added for success #callback' do
33
+ 3.times do
34
+ subject.errback { errback_calls << true }
35
+ subject.callback { success_calls << true }
36
+ end
37
+ subject.fail *arguments
38
+ expect(errback_calls.count).to eql(3)
39
+ expect(success_calls.count).to eql(0)
40
+ end
41
+ end
42
+
43
+ context '#callback' do
44
+ it 'adds a callback that is called when #succed is called' do
45
+ subject.callback do |*args|
46
+ expect(args).to eql(arguments)
47
+ end
48
+ subject.succeed *arguments
49
+ end
50
+
51
+ it 'catches exceptions in the callback and logs the error to the logger' do
52
+ expect(subject.send(:logger)).to receive(:error).with(/#{exception.message}/)
53
+ subject.callback do
54
+ raise exception
55
+ end
56
+ subject.succeed
57
+ end
58
+ end
59
+
60
+ context '#succeed' do
61
+ it 'calls the callbacks defined with #callback, but not the ones added for #errback' do
62
+ 3.times do
63
+ subject.errback { errback_calls << true }
64
+ subject.callback { success_calls << true }
65
+ end
66
+ subject.succeed *arguments
67
+ expect(success_calls.count).to eql(3)
68
+ expect(errback_calls.count).to eql(0)
69
+ end
70
+ end
71
+ end
@@ -44,7 +44,7 @@ RSpec.configure do |config|
44
44
 
45
45
  config.after(:suite) do
46
46
  WebMock.disable!
47
- TestApp.instance.delete
47
+ TestApp.instance.delete if TestApp.instance_variable_get('@singleton__instance__')
48
48
  end
49
49
  end
50
50
 
@@ -6,7 +6,7 @@ module RSpec
6
6
  module EventMachine
7
7
  extend self
8
8
 
9
- DEFAULT_TIMEOUT = 10
9
+ DEFAULT_TIMEOUT = 15
10
10
 
11
11
  def run_reactor(timeout = DEFAULT_TIMEOUT)
12
12
  Timeout::timeout(timeout + 0.5) do
@@ -15,10 +15,10 @@ class TestApp
15
15
  {
16
16
  'name' => 'persisted:presence_fixtures',
17
17
  'presence' => [
18
- { 'clientId' => 'client_bool', 'clientData' => 'true' },
19
- { 'clientId' => 'client_int', 'clientData' => '24' },
20
- { 'clientId' => 'client_string', 'clientData' => 'This is a string clientData payload' },
21
- { 'clientId' => 'client_json', 'clientData' => '{ "test" => \'This is a JSONObject clientData payload\'}' }
18
+ { 'clientId' => 'client_bool', 'data' => 'true' },
19
+ { 'clientId' => 'client_int', 'data' => '24' },
20
+ { 'clientId' => 'client_string', 'data' => 'This is a string clientData payload' },
21
+ { 'clientId' => 'client_json', 'data' => '{ "test" => \'This is a JSONObject clientData payload\'}' }
22
22
  ]
23
23
  }
24
24
  ]
@@ -79,7 +79,7 @@ class TestApp
79
79
  end
80
80
 
81
81
  def environment
82
- 'sandbox'
82
+ ENV['ABLY_ENV'] || 'sandbox'
83
83
  end
84
84
 
85
85
  def create_test_app
@@ -90,7 +90,12 @@ class TestApp
90
90
  'Content-Type' => 'application/json'
91
91
  }
92
92
 
93
- @attributes = JSON.parse(Faraday.post(url, APP_SPEC.to_json, headers).body)
93
+ response = Faraday.post(url, APP_SPEC.to_json, headers)
94
+ raise "Could not create test app. Ably responded with status #{response.status}\n#{response.body}" unless (200..299).include?(response.status)
95
+
96
+ @attributes = JSON.parse(response.body)
97
+
98
+ puts "Test app '#{app_id}' created in #{environment} environment"
94
99
  end
95
100
 
96
101
  def host
@@ -103,7 +108,8 @@ class TestApp
103
108
 
104
109
  def create_test_stats(stats)
105
110
  client = Ably::Rest::Client.new(api_key: api_key, environment: environment)
106
- client.post('/stats', stats)
111
+ response = client.post('/stats', stats)
112
+ raise "Could not create stats fixtures. Ably responded with status #{response.status}\n#{response.body}" unless (200..299).include?(response.status)
107
113
  end
108
114
 
109
115
  private
@@ -1,9 +1,10 @@
1
1
  # encoding: utf-8
2
- require 'spec_helper'
3
- require 'shared/model_behaviour'
4
2
  require 'base64'
5
3
  require 'msgpack'
6
4
 
5
+ require 'spec_helper'
6
+ require 'shared/model_behaviour'
7
+
7
8
  describe Ably::Models::Message do
8
9
  include Ably::Modules::Conversions
9
10
 
@@ -12,11 +13,11 @@ describe Ably::Models::Message do
12
13
  let(:protocol_message) { Ably::Models::ProtocolMessage.new(action: 1, timestamp: protocol_message_timestamp) }
13
14
 
14
15
  it_behaves_like 'a model', with_simple_attributes: %w(name client_id data encoding) do
15
- let(:model_args) { [protocol_message] }
16
+ let(:model_args) { [protocol_message: protocol_message] }
16
17
  end
17
18
 
18
19
  context '#timestamp' do
19
- let(:model) { subject.new({}, protocol_message) }
20
+ let(:model) { subject.new({}, protocol_message: protocol_message) }
20
21
 
21
22
  it 'retrieves attribute :timestamp as Time object from ProtocolMessage' do
22
23
  expect(model.timestamp).to be_a(Time)
@@ -39,7 +40,7 @@ describe Ably::Models::Message do
39
40
  end
40
41
 
41
42
  context 'with a protocol message with a different connectionId' do
42
- let(:model) { subject.new({ 'connectionId' => model_connection_id }, protocol_message) }
43
+ let(:model) { subject.new({ 'connectionId' => model_connection_id }, protocol_message: protocol_message) }
43
44
 
44
45
  it 'uses the model value' do
45
46
  expect(model.connection_id).to eql(model_connection_id)
@@ -57,7 +58,7 @@ describe Ably::Models::Message do
57
58
  end
58
59
 
59
60
  context 'with a protocol message with a connectionId' do
60
- let(:model) { subject.new({ }, protocol_message) }
61
+ let(:model) { subject.new({ }, protocol_message: protocol_message) }
61
62
 
62
63
  it 'uses the model value' do
63
64
  expect(model.connection_id).to eql(protocol_connection_id)
@@ -67,7 +68,7 @@ describe Ably::Models::Message do
67
68
  end
68
69
 
69
70
  context 'Java naming', :api_private do
70
- let(:model) { subject.new({ clientId: 'joe' }, protocol_message) }
71
+ let(:model) { subject.new({ clientId: 'joe' }, protocol_message: protocol_message) }
71
72
 
72
73
  it 'converts the attribute to ruby symbol naming convention' do
73
74
  expect(model.client_id).to eql('joe')
@@ -80,7 +81,7 @@ describe Ably::Models::Message do
80
81
  let(:encoded_value) { value.encode(encoding) }
81
82
  let(:value) { random_str }
82
83
  let(:options) { { attribute.to_sym => encoded_value } }
83
- let(:model) { subject.new(options, protocol_message) }
84
+ let(:model) { subject.new(options, protocol_message: protocol_message) }
84
85
  let(:model_attribute) { model.public_send(attribute) }
85
86
 
86
87
  context 'as UTF_8 string' do
@@ -142,7 +143,7 @@ describe Ably::Models::Message do
142
143
  let(:json_object) { JSON.parse(model.to_json) }
143
144
 
144
145
  context 'with valid data' do
145
- let(:model) { subject.new({ name: 'test', clientId: 'joe' }, protocol_message) }
146
+ let(:model) { subject.new({ name: 'test', clientId: 'joe' }, protocol_message: protocol_message) }
146
147
 
147
148
  it 'converts the attribute back to Java mixedCase notation using string keys' do
148
149
  expect(json_object["clientId"]).to eql('joe')
@@ -150,7 +151,7 @@ describe Ably::Models::Message do
150
151
  end
151
152
 
152
153
  context 'with invalid data' do
153
- let(:model) { subject.new({ clientId: 'joe' }, protocol_message) }
154
+ let(:model) { subject.new({ clientId: 'joe' }, protocol_message: protocol_message) }
154
155
 
155
156
  it 'raises an exception' do
156
157
  expect { model.to_json }.to raise_error RuntimeError, /cannot generate a valid Hash/
@@ -159,7 +160,7 @@ describe Ably::Models::Message do
159
160
 
160
161
  context 'with binary data' do
161
162
  let(:data) { MessagePack.pack(random_str(32)) }
162
- let(:model) { subject.new({ name: 'test', data: data }, protocol_message) }
163
+ let(:model) { subject.new({ name: 'test', data: data }, protocol_message: protocol_message) }
163
164
 
164
165
  it 'encodes as Base64 so that it can be converted to UTF-8 automatically by JSON#dump' do
165
166
  expect(json_object["data"]).to eql(::Base64.encode64(data))
@@ -188,7 +189,7 @@ describe Ably::Models::Message do
188
189
  end
189
190
 
190
191
  context 'with protocol message' do
191
- let(:model) { subject.new({ id: id, timestamp: message_timestamp }, protocol_message) }
192
+ let(:model) { subject.new({ id: id, timestamp: message_timestamp }, protocol_message: protocol_message) }
192
193
 
193
194
  specify '#id prefers embedded ID' do
194
195
  expect(model.id).to eql(id)
@@ -322,7 +323,7 @@ describe Ably::Models::Message do
322
323
  end
323
324
 
324
325
  context 'with ProtocolMessage' do
325
- subject { Ably::Models.Message(json, protocol_message) }
326
+ subject { Ably::Models.Message(json, protocol_message: protocol_message) }
326
327
 
327
328
  it 'returns a Message object' do
328
329
  expect(subject).to be_a(Ably::Models::Message)
@@ -366,7 +367,7 @@ describe Ably::Models::Message do
366
367
  end
367
368
 
368
369
  context 'with ProtocolMessage' do
369
- subject { Ably::Models.Message(message, protocol_message) }
370
+ subject { Ably::Models.Message(message, protocol_message: protocol_message) }
370
371
 
371
372
  it 'returns a Message object' do
372
373
  expect(subject).to be_a(Ably::Models::Message)
@@ -5,7 +5,7 @@ describe Ably::Models::PaginatedResource do
5
5
  let(:paginated_resource_class) { Ably::Models::PaginatedResource }
6
6
  let(:headers) { Hash.new }
7
7
  let(:client) do
8
- instance_double('Ably::Rest::Client').tap do |client|
8
+ instance_double('Ably::Rest::Client', logger: true).tap do |client|
9
9
  allow(client).to receive(:get).and_return(http_response)
10
10
  end
11
11
  end
@@ -90,7 +90,7 @@ describe Ably::Models::PaginatedResource do
90
90
  }
91
91
  end
92
92
  let(:paged_client) do
93
- instance_double('Ably::Rest::Client').tap do |client|
93
+ instance_double('Ably::Rest::Client', logger: true).tap do |client|
94
94
  allow(client).to receive(:get).and_return(http_response_page2)
95
95
  end
96
96
  end
@@ -137,9 +137,9 @@ describe Ably::Models::PaginatedResource do
137
137
  end
138
138
 
139
139
  context '#next_page' do
140
- it 'returns a deferrable object' do
140
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
141
141
  run_reactor do
142
- expect(subject.next_page).to be_a(EventMachine::Deferrable)
142
+ expect(subject.next_page).to be_a(Ably::Util::SafeDeferrable)
143
143
  stop_reactor
144
144
  end
145
145
  end