ably 1.1.1 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check.yml +27 -0
  3. data/CHANGELOG.md +60 -2
  4. data/COPYRIGHT +1 -0
  5. data/LICENSE +172 -11
  6. data/MAINTAINERS.md +1 -0
  7. data/README.md +2 -14
  8. data/SPEC.md +1020 -922
  9. data/ably.gemspec +5 -5
  10. data/lib/ably/auth.rb +12 -2
  11. data/lib/ably/exceptions.rb +2 -2
  12. data/lib/ably/logger.rb +7 -1
  13. data/lib/ably/modules/ably.rb +11 -1
  14. data/lib/ably/modules/state_machine.rb +1 -1
  15. data/lib/ably/realtime/channel.rb +7 -11
  16. data/lib/ably/realtime/channel/channel_manager.rb +2 -2
  17. data/lib/ably/realtime/channel/channel_properties.rb +24 -0
  18. data/lib/ably/realtime/client.rb +9 -0
  19. data/lib/ably/realtime/connection.rb +7 -4
  20. data/lib/ably/realtime/connection/connection_manager.rb +19 -1
  21. data/lib/ably/realtime/connection/websocket_transport.rb +67 -1
  22. data/lib/ably/realtime/presence.rb +0 -14
  23. data/lib/ably/rest/channel.rb +25 -17
  24. data/lib/ably/rest/client.rb +35 -17
  25. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +4 -1
  26. data/lib/ably/version.rb +1 -1
  27. data/spec/acceptance/realtime/auth_spec.rb +5 -4
  28. data/spec/acceptance/realtime/channel_spec.rb +21 -8
  29. data/spec/acceptance/realtime/client_spec.rb +80 -20
  30. data/spec/acceptance/realtime/connection_failures_spec.rb +90 -9
  31. data/spec/acceptance/realtime/connection_spec.rb +47 -19
  32. data/spec/acceptance/realtime/message_spec.rb +2 -4
  33. data/spec/acceptance/realtime/presence_history_spec.rb +0 -58
  34. data/spec/acceptance/realtime/presence_spec.rb +54 -0
  35. data/spec/acceptance/realtime/push_admin_spec.rb +43 -21
  36. data/spec/acceptance/rest/auth_spec.rb +6 -75
  37. data/spec/acceptance/rest/base_spec.rb +8 -4
  38. data/spec/acceptance/rest/channel_spec.rb +42 -4
  39. data/spec/acceptance/rest/client_spec.rb +121 -26
  40. data/spec/acceptance/rest/message_spec.rb +1 -2
  41. data/spec/acceptance/rest/push_admin_spec.rb +67 -27
  42. data/spec/shared/client_initializer_behaviour.rb +131 -8
  43. data/spec/spec_helper.rb +1 -0
  44. data/spec/support/debug_failure_helper.rb +9 -5
  45. data/spec/support/serialization_helper.rb +21 -0
  46. data/spec/support/test_app.rb +2 -2
  47. data/spec/unit/modules/enum_spec.rb +1 -1
  48. data/spec/unit/realtime/client_spec.rb +20 -7
  49. data/spec/unit/realtime/connection_spec.rb +1 -1
  50. metadata +22 -17
  51. data/.travis.yml +0 -19
@@ -75,8 +75,7 @@ describe Ably::Rest::Channel, 'messages' do
75
75
  end
76
76
 
77
77
  context 'JSON Array' do
78
- # TODO: Add nil type back in
79
- let(:data) { { 'push' => { 'data' => { 'key' => [ true, false, 55, 'string', { 'Hash' => true }, ['array'] ] } } } }
78
+ let(:data) { { 'push' => { 'data' => { 'key' => [ true, false, 55, nil, 'string', { 'Hash' => true }, ['array'] ] } } } }
80
79
 
81
80
  it 'is encoded and decoded to the same deep multi-type object' do
82
81
  channel.publish 'event', {}, extras: data
@@ -89,31 +89,15 @@ describe Ably::Rest::Push::Admin do
89
89
  end
90
90
  end
91
91
 
92
- def request_body(request, protocol)
93
- if protocol == :msgpack
94
- MessagePack.unpack(request.body)
95
- else
96
- JSON.parse(request.body)
97
- end
98
- end
99
-
100
- def serialize(object, protocol)
101
- if protocol == :msgpack
102
- MessagePack.pack(object)
103
- else
104
- JSON.dump(object)
105
- end
106
- end
107
-
108
92
  let!(:publish_stub) do
109
93
  stub_request(:post, "#{client.endpoint}/push/publish").
110
94
  with do |request|
111
- expect(request_body(request, protocol)['recipient']['camelCase']['secondLevelCamelCase']).to eql('val')
112
- expect(request_body(request, protocol)['recipient']).to_not have_key('camel_case')
95
+ expect(deserialize_body(request.body, protocol)['recipient']['camelCase']['secondLevelCamelCase']).to eql('val')
96
+ expect(deserialize_body(request.body, protocol)['recipient']).to_not have_key('camel_case')
113
97
  true
114
98
  end.to_return(
115
99
  :status => 201,
116
- :body => serialize({}, protocol),
100
+ :body => serialize_body({}, protocol),
117
101
  :headers => { 'Content-Type' => content_type }
118
102
  )
119
103
  end
@@ -181,6 +165,12 @@ describe Ably::Rest::Push::Admin do
181
165
  let(:client_id) { random_str }
182
166
  let(:fixture_count) { 6 }
183
167
 
168
+ before(:all) do
169
+ # As push tests often use the global scope (devices),
170
+ # we need to ensure tests cannot conflict
171
+ reload_test_app
172
+ end
173
+
184
174
  before do
185
175
  fixture_count.times.map do |index|
186
176
  Thread.new do
@@ -244,6 +234,12 @@ describe Ably::Rest::Push::Admin do
244
234
  let(:fixture_count) { 2 }
245
235
  let(:client_id) { random_str }
246
236
 
237
+ before(:all) do
238
+ # As push tests often use the global scope (devices),
239
+ # we need to ensure tests cannot conflict
240
+ reload_test_app
241
+ end
242
+
247
243
  before do
248
244
  fixture_count.times.map do |index|
249
245
  Thread.new do
@@ -318,6 +314,12 @@ describe Ably::Rest::Push::Admin do
318
314
  }
319
315
  end
320
316
 
317
+ before(:all) do
318
+ # As push tests often use the global scope (devices),
319
+ # we need to ensure tests cannot conflict
320
+ reload_test_app
321
+ end
322
+
321
323
  after do
322
324
  subject.remove_where client_id: client_id, full_wait: true
323
325
  end
@@ -444,6 +446,12 @@ describe Ably::Rest::Push::Admin do
444
446
  let(:device_id) { random_str }
445
447
  let(:client_id) { random_str }
446
448
 
449
+ before(:all) do
450
+ # As push tests often use the global scope (devices),
451
+ # we need to ensure tests cannot conflict
452
+ reload_test_app
453
+ end
454
+
447
455
  before do
448
456
  [
449
457
  Thread.new do
@@ -501,6 +509,12 @@ describe Ably::Rest::Push::Admin do
501
509
  let(:device_id) { random_str }
502
510
  let(:client_id) { random_str }
503
511
 
512
+ before(:all) do
513
+ # As push tests often use the global scope (devices),
514
+ # we need to ensure tests cannot conflict
515
+ reload_test_app
516
+ end
517
+
504
518
  before do
505
519
  [
506
520
  Thread.new do
@@ -585,8 +599,8 @@ describe Ably::Rest::Push::Admin do
585
599
  # and two device with the unique device_id and no client_id
586
600
  before do
587
601
  [
588
- lambda { device_registrations.save(default_device_attr.merge(id: device_id)) },
589
- lambda { device_registrations.save(default_device_attr.merge(id: device_id_2)) },
602
+ lambda { device_registrations.save(default_device_attr.merge(id: device_id, client_id: nil)) },
603
+ lambda { device_registrations.save(default_device_attr.merge(id: device_id_2, client_id: nil)) },
590
604
  lambda { device_registrations.save(default_device_attr.merge(client_id: client_id, id: random_str)) },
591
605
  lambda { device_registrations.save(default_device_attr.merge(client_id: client_id, id: random_str)) }
592
606
  ].map do |proc|
@@ -602,6 +616,12 @@ describe Ably::Rest::Push::Admin do
602
616
  describe '#list (#RSH1c1)' do
603
617
  let(:fixture_count) { 6 }
604
618
 
619
+ before(:all) do
620
+ # As push tests often use the global scope (devices),
621
+ # we need to ensure tests cannot conflict
622
+ reload_test_app
623
+ end
624
+
605
625
  before do
606
626
  fixture_count.times.map do |index|
607
627
  Thread.new { subject.save(channel: "pushenabled:#{random_str}", client_id: client_id) }
@@ -670,11 +690,14 @@ describe Ably::Rest::Push::Admin do
670
690
  describe '#list_channels (#RSH1c2)' do
671
691
  let(:fixture_count) { 6 }
672
692
 
673
- before(:context) do
674
- reload_test_app # TODO: Review if necessary late, currently other tests may affect list_channels
693
+ before(:all) do
694
+ # As push tests often use the global scope (devices),
695
+ # we need to ensure tests cannot conflict
696
+ reload_test_app
675
697
  end
676
698
 
677
699
  before do
700
+ # Create 6 channel subscriptions to the client ID for this test
678
701
  fixture_count.times.map do |index|
679
702
  Thread.new do
680
703
  subject.save(channel: "pushenabled:#{index}:#{random_str}", client_id: client_id)
@@ -694,9 +717,6 @@ describe Ably::Rest::Push::Admin do
694
717
  end
695
718
 
696
719
  it 'supports paging' do
697
- skip 'Channel lists with limits is not reliable immediately after fixture creation'
698
- # TODO: Remove this once list channels with limits is reliable immediately after fixtures created
699
- # See https://github.com/ably/realtime/issues/1882
700
720
  subject.list_channels
701
721
  page = subject.list_channels(limit: 3)
702
722
  expect(page).to be_a(Ably::Models::PaginatedResult)
@@ -730,6 +750,12 @@ describe Ably::Rest::Push::Admin do
730
750
  let(:client_id) { random_str }
731
751
  let(:device_id) { random_str }
732
752
 
753
+ before(:all) do
754
+ # As push tests often use the global scope (devices),
755
+ # we need to ensure tests cannot conflict
756
+ reload_test_app
757
+ end
758
+
733
759
  it 'saves the new client_id PushChannelSubscription Hash object' do
734
760
  subject.save(channel: channel, client_id: client_id)
735
761
 
@@ -802,6 +828,12 @@ describe Ably::Rest::Push::Admin do
802
828
 
803
829
  let(:fixture_count) { 6 }
804
830
 
831
+ before(:all) do
832
+ # As push tests often use the global scope (devices),
833
+ # we need to ensure tests cannot conflict
834
+ reload_test_app
835
+ end
836
+
805
837
  before do
806
838
  fixture_count.times.map do |index|
807
839
  [
@@ -814,8 +846,10 @@ describe Ably::Rest::Push::Admin do
814
846
  end.each(&:join) # Wait for all threads to complete
815
847
  end
816
848
 
849
+ # TODO: Reinstate once delete subscriptions by channel is possible
850
+ # See https://github.com/ably/realtime/issues/1359
817
851
  it 'removes matching channels' do
818
- skip 'Delete by channel is not yet supported'
852
+ skip 'deleting subscriptions is not yet supported realtime#1359'
819
853
  subject.remove_where channel: fixed_channel, full_wait: true
820
854
  expect(subject.list(channel: fixed_channel).items.count).to eql(0)
821
855
  expect(subject.list(client_id: client_id).items.count).to eql(0)
@@ -852,6 +886,12 @@ describe Ably::Rest::Push::Admin do
852
886
  let(:client_id) { random_str }
853
887
  let(:device_id) { random_str }
854
888
 
889
+ before(:all) do
890
+ # As push tests often use the global scope (devices),
891
+ # we need to ensure tests cannot conflict
892
+ reload_test_app
893
+ end
894
+
855
895
  before do
856
896
  [
857
897
  lambda { subject.save(channel: channel, client_id: client_id) },
@@ -69,14 +69,6 @@ shared_examples 'a client initializer' do
69
69
  expect { subject }.to raise_error(ArgumentError, /key and key_name or key_secret are mutually exclusive/)
70
70
  end
71
71
  end
72
-
73
- context 'client_id as only option' do
74
- let(:client_options) { { client_id: 'valid' } }
75
-
76
- it 'requires a valid key' do
77
- expect { subject }.to raise_error(ArgumentError, /client_id cannot be provided without a complete API key or means to authenticate/)
78
- end
79
- end
80
72
  end
81
73
 
82
74
  context 'with valid arguments' do
@@ -273,6 +265,137 @@ shared_examples 'a client initializer' do
273
265
  end
274
266
  end
275
267
  end
268
+
269
+ context 'environment' do
270
+ context 'when set without custom fallback hosts configured' do
271
+ let(:environment) { 'foo' }
272
+ let(:client_options) { default_options.merge(environment: environment) }
273
+ let(:default_fallbacks) { %w(a b c d e).map { |id| "#{environment}-#{id}-fallback.ably-realtime.com" } }
274
+
275
+ it 'sets the environment attribute' do
276
+ expect(subject.environment).to eql(environment)
277
+ end
278
+
279
+ it 'uses the default fallback hosts (#TBC, see https://github.com/ably/wiki/issues/361)' do
280
+ expect(subject.fallback_hosts.sort).to eql(default_fallbacks)
281
+ end
282
+ end
283
+
284
+ context 'when set with custom fallback hosts configured' do
285
+ let(:environment) { 'foo' }
286
+ let(:custom_fallbacks) { %w(a b c).map { |id| "#{environment}-#{id}.foo.com" } }
287
+ let(:client_options) { default_options.merge(environment: environment, fallback_hosts: custom_fallbacks) }
288
+
289
+ it 'sets the environment attribute' do
290
+ expect(subject.environment).to eql(environment)
291
+ end
292
+
293
+ it 'uses the custom provided fallback hosts (#RSC15a)' do
294
+ expect(subject.fallback_hosts.sort).to eql(custom_fallbacks)
295
+ end
296
+ end
297
+
298
+ context 'when set with fallback_hosts_use_default' do
299
+ let(:environment) { 'foo' }
300
+ let(:custom_fallbacks) { %w(a b c).map { |id| "#{environment}-#{id}.foo.com" } }
301
+ let(:default_production_fallbacks) { %w(a b c d e).map { |id| "#{id}.ably-realtime.com" } }
302
+ let(:client_options) { default_options.merge(environment: environment, fallback_hosts_use_default: true) }
303
+
304
+ it 'sets the environment attribute' do
305
+ expect(subject.environment).to eql(environment)
306
+ end
307
+
308
+ it 'uses the production default fallback hosts (#RTN17b)' do
309
+ expect(subject.fallback_hosts.sort).to eql(default_production_fallbacks)
310
+ end
311
+ end
312
+ end
313
+
314
+ context 'rest_host' do
315
+ context 'when set without custom fallback hosts configured' do
316
+ let(:custom_rest_host) { 'foo.com' }
317
+ let(:client_options) { default_options.merge(rest_host: custom_rest_host) }
318
+
319
+ it 'sets the custom_host attribute' do
320
+ expect(subject.custom_host).to eql(custom_rest_host)
321
+ end
322
+
323
+ it 'has no default fallback hosts' do
324
+ expect(subject.fallback_hosts).to be_empty
325
+ end
326
+ end
327
+
328
+ context 'when set with environment and without custom fallback hosts configured' do
329
+ let(:environment) { 'foobar' }
330
+ let(:custom_rest_host) { 'foo.com' }
331
+ let(:client_options) { default_options.merge(environment: environment, rest_host: custom_rest_host) }
332
+
333
+ it 'sets the environment attribute' do
334
+ expect(subject.environment).to eql(environment)
335
+ end
336
+
337
+ it 'sets the custom_host attribute' do
338
+ expect(subject.custom_host).to eql(custom_rest_host)
339
+ end
340
+
341
+ it 'has no default fallback hosts' do
342
+ expect(subject.fallback_hosts).to be_empty
343
+ end
344
+ end
345
+
346
+ context 'when set with custom fallback hosts configured' do
347
+ let(:custom_rest_host) { 'foo.com' }
348
+ let(:custom_fallbacks) { %w(a b c).map { |id| "#{environment}-#{id}.foo.com" } }
349
+ let(:client_options) { default_options.merge(rest_host: custom_rest_host, fallback_hosts: custom_fallbacks) }
350
+
351
+ it 'sets the custom_host attribute' do
352
+ expect(subject.custom_host).to eql(custom_rest_host)
353
+ end
354
+
355
+ it 'has no default fallback hosts' do
356
+ expect(subject.fallback_hosts.sort).to eql(custom_fallbacks)
357
+ end
358
+ end
359
+ end
360
+
361
+ context 'realtime_host' do
362
+ context 'when set without custom fallback hosts configured' do
363
+ let(:custom_realtime_host) { 'realtime.foo.com' }
364
+ let(:client_options) { default_options.merge(realtime_host: custom_realtime_host) }
365
+
366
+ # These tests are shared between realtime & rest clients
367
+ # So don't test for the attribute, instead test the options
368
+ it 'sets the realtime_host option' do
369
+ expect(subject.options[:realtime_host]).to eql(custom_realtime_host)
370
+ end
371
+
372
+ it 'has no default fallback hosts' do
373
+ expect(subject.fallback_hosts).to be_empty
374
+ end
375
+ end
376
+ end
377
+
378
+ context 'custom port' do
379
+ context 'when set without custom fallback hosts configured' do
380
+ let(:custom_port) { 555 }
381
+ let(:client_options) { default_options.merge(port: custom_port) }
382
+
383
+ it 'has no default fallback hosts' do
384
+ expect(subject.fallback_hosts).to be_empty
385
+ end
386
+ end
387
+ end
388
+
389
+ context 'custom TLS port' do
390
+ context 'when set without custom fallback hosts configured' do
391
+ let(:custom_port) { 555 }
392
+ let(:client_options) { default_options.merge(tls_port: custom_port) }
393
+
394
+ it 'has no default fallback hosts' do
395
+ expect(subject.fallback_hosts).to be_empty
396
+ end
397
+ end
398
+ end
276
399
  end
277
400
 
278
401
  context 'delegators' do
data/spec/spec_helper.rb CHANGED
@@ -19,6 +19,7 @@ require 'support/event_emitter_helper'
19
19
  require 'support/private_api_formatter'
20
20
  require 'support/protocol_helper'
21
21
  require 'support/random_helper'
22
+ require 'support/serialization_helper'
22
23
  require 'support/test_logger_helper'
23
24
 
24
25
  require 'rspec_config'
@@ -2,6 +2,8 @@ RSpec.configure do |config|
2
2
  config.before(:example) do |example|
3
3
  next if example.metadata[:prevent_log_stubbing]
4
4
 
5
+ log_mutex = Mutex.new
6
+
5
7
  @log_output = []
6
8
  %w(fatal error warn info debug).each do |method_name|
7
9
  allow_any_instance_of(Ably::Logger).to receive(method_name.to_sym).and_wrap_original do |method, *args, &block|
@@ -10,11 +12,13 @@ RSpec.configure do |config|
10
12
 
11
13
  prefix = "#{Time.now.strftime('%H:%M:%S.%L')} [\e[33m#{method_name}\e[0m] "
12
14
 
13
- begin
14
- args << block.call unless block.nil?
15
- @log_output << "#{prefix}#{args.compact.join(' ')}"
16
- rescue StandardError => e
17
- @log_output << "#{prefix}Failed to log block - #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}}"
15
+ log_mutex.synchronize do
16
+ begin
17
+ args << block.call unless block.nil?
18
+ @log_output << "#{prefix}#{args.compact.join(' ')}"
19
+ rescue StandardError => e
20
+ @log_output << "#{prefix}Failed to log block - #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}}"
21
+ end
18
22
  end
19
23
 
20
24
  # Call original
@@ -0,0 +1,21 @@
1
+ module SerializationHelper
2
+ def serialize_body(object, protocol)
3
+ if protocol == :msgpack
4
+ MessagePack.pack(object)
5
+ else
6
+ JSON.dump(object)
7
+ end
8
+ end
9
+
10
+ def deserialize_body(object, protocol)
11
+ if protocol == :msgpack
12
+ MessagePack.unpack(object)
13
+ else
14
+ JSON.parse(object)
15
+ end
16
+ end
17
+
18
+ RSpec.configure do |config|
19
+ config.include self
20
+ end
21
+ end
@@ -4,11 +4,11 @@ class TestApp
4
4
  TEST_RESOURCES_PATH = File.expand_path('../../../lib/submodules/ably-common/test-resources', __FILE__)
5
5
 
6
6
  # App configuration for test app
7
- # See https://github.com/ably/ably-common/blob/master/test-resources/test-app-setup.json
7
+ # See https://github.com/ably/ably-common/blob/main/test-resources/test-app-setup.json
8
8
  APP_SPEC = JSON.parse(File.read(File.join(TEST_RESOURCES_PATH, 'test-app-setup.json')))['post_apps']
9
9
 
10
10
  # Cipher details used for client_encoded presence data in test app
11
- # See https://github.com/ably/ably-common/blob/master/test-resources/test-app-setup.json
11
+ # See https://github.com/ably/ably-common/blob/main/test-resources/test-app-setup.json
12
12
  APP_SPEC_CIPHER = JSON.parse(File.read(File.join(TEST_RESOURCES_PATH, 'test-app-setup.json')))['cipher']
13
13
 
14
14
  # If an app has already been created and we need a new app, create a new test app
@@ -53,7 +53,7 @@ describe Ably::Modules::Enum, :api_private do
53
53
  end
54
54
 
55
55
  it 'allows different type comparison 2' do
56
- skip 'Unless we monkeypath Symbols, the == operator is never invoked'
56
+ skip 'Unless we monkeypatch Symbols, the == operator is never invoked'
57
57
  expect([:value_zero].include?(subject.ValueZero)).to eql(true)
58
58
  end
59
59
 
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  require 'shared/client_initializer_behaviour'
4
4
 
5
5
  describe Ably::Realtime::Client do
6
- subject do
6
+ subject(:realtime_client) do
7
7
  Ably::Realtime::Client.new(client_options)
8
8
  end
9
9
 
@@ -13,30 +13,43 @@ describe Ably::Realtime::Client do
13
13
  let(:client_options) { { key: 'appid.keyuid:keysecret', auto_connect: false } }
14
14
 
15
15
  it 'passes on the options to the initializer' do
16
- rest_client = instance_double('Ably::Rest::Client', auth: instance_double('Ably::Auth'), options: client_options)
16
+ rest_client = instance_double('Ably::Rest::Client', auth: instance_double('Ably::Auth'), options: client_options, environment: 'production', use_tls?: true, custom_tls_port: nil)
17
17
  expect(Ably::Rest::Client).to receive(:new).with(hash_including(client_options)).and_return(rest_client)
18
- subject
18
+ realtime_client
19
19
  end
20
20
 
21
21
  context 'for attribute' do
22
22
  [:environment, :use_tls?, :log_level, :custom_host].each do |attribute|
23
23
  specify "##{attribute}" do
24
- expect(subject.rest_client).to receive(attribute)
25
- subject.public_send attribute
24
+ expect(realtime_client.rest_client).to receive(attribute)
25
+ realtime_client.public_send attribute
26
26
  end
27
27
  end
28
28
  end
29
29
  end
30
30
 
31
+ context 'when :transport_params option is passed' do
32
+ let(:expected_transport_params) do
33
+ { 'heartbeats' => 'true', 'v' => '1.0', 'extra_param' => 'extra_param' }
34
+ end
35
+ let(:client_options) do
36
+ { key: 'appid.keyuid:keysecret', transport_params: { heartbeats: true, v: 1.0, extra_param: 'extra_param'} }
37
+ end
38
+
39
+ it 'converts options to strings' do
40
+ expect(realtime_client.transport_params).to eq(expected_transport_params)
41
+ end
42
+ end
43
+
31
44
  context 'push' do
32
45
  let(:client_options) { { key: 'appid.keyuid:keysecret' } }
33
46
 
34
47
  specify '#device is not supported and raises an exception' do
35
- expect { subject.device }.to raise_error Ably::Exceptions::PushNotificationsNotSupported
48
+ expect { realtime_client.device }.to raise_error Ably::Exceptions::PushNotificationsNotSupported
36
49
  end
37
50
 
38
51
  specify '#push returns a Push object' do
39
- expect(subject.push).to be_a(Ably::Realtime::Push)
52
+ expect(realtime_client.push).to be_a(Ably::Realtime::Push)
40
53
  end
41
54
  end
42
55