ably 1.0.7 → 1.1.0

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +14 -0
  3. data/.travis.yml +4 -4
  4. data/CHANGELOG.md +26 -3
  5. data/Rakefile +32 -0
  6. data/SPEC.md +920 -565
  7. data/ably.gemspec +9 -4
  8. data/lib/ably/auth.rb +28 -2
  9. data/lib/ably/exceptions.rb +8 -2
  10. data/lib/ably/models/channel_state_change.rb +1 -1
  11. data/lib/ably/models/connection_state_change.rb +1 -1
  12. data/lib/ably/models/device_details.rb +87 -0
  13. data/lib/ably/models/device_push_details.rb +86 -0
  14. data/lib/ably/models/error_info.rb +23 -2
  15. data/lib/ably/models/idiomatic_ruby_wrapper.rb +4 -4
  16. data/lib/ably/models/protocol_message.rb +32 -2
  17. data/lib/ably/models/push_channel_subscription.rb +89 -0
  18. data/lib/ably/modules/conversions.rb +1 -1
  19. data/lib/ably/modules/encodeable.rb +1 -1
  20. data/lib/ably/modules/exception_codes.rb +128 -0
  21. data/lib/ably/modules/model_common.rb +15 -2
  22. data/lib/ably/modules/state_machine.rb +1 -1
  23. data/lib/ably/realtime.rb +1 -0
  24. data/lib/ably/realtime/auth.rb +1 -1
  25. data/lib/ably/realtime/channel.rb +24 -102
  26. data/lib/ably/realtime/channel/channel_manager.rb +2 -6
  27. data/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
  28. data/lib/ably/realtime/channel/publisher.rb +74 -0
  29. data/lib/ably/realtime/channel/push_channel.rb +62 -0
  30. data/lib/ably/realtime/client.rb +87 -0
  31. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +6 -2
  32. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  33. data/lib/ably/realtime/connection.rb +8 -5
  34. data/lib/ably/realtime/connection/connection_manager.rb +7 -7
  35. data/lib/ably/realtime/connection/websocket_transport.rb +1 -1
  36. data/lib/ably/realtime/presence.rb +4 -4
  37. data/lib/ably/realtime/presence/members_map.rb +3 -3
  38. data/lib/ably/realtime/push.rb +40 -0
  39. data/lib/ably/realtime/push/admin.rb +61 -0
  40. data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  41. data/lib/ably/realtime/push/device_registrations.rb +105 -0
  42. data/lib/ably/rest.rb +1 -0
  43. data/lib/ably/rest/channel.rb +33 -5
  44. data/lib/ably/rest/channel/push_channel.rb +62 -0
  45. data/lib/ably/rest/client.rb +137 -28
  46. data/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
  47. data/lib/ably/rest/presence.rb +1 -0
  48. data/lib/ably/rest/push.rb +42 -0
  49. data/lib/ably/rest/push/admin.rb +54 -0
  50. data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  51. data/lib/ably/rest/push/device_registrations.rb +103 -0
  52. data/lib/ably/version.rb +7 -2
  53. data/spec/acceptance/realtime/auth_spec.rb +6 -8
  54. data/spec/acceptance/realtime/channel_spec.rb +166 -51
  55. data/spec/acceptance/realtime/client_spec.rb +149 -0
  56. data/spec/acceptance/realtime/connection_failures_spec.rb +1 -1
  57. data/spec/acceptance/realtime/connection_spec.rb +4 -4
  58. data/spec/acceptance/realtime/message_spec.rb +19 -17
  59. data/spec/acceptance/realtime/presence_spec.rb +5 -5
  60. data/spec/acceptance/realtime/push_admin_spec.rb +696 -0
  61. data/spec/acceptance/realtime/push_spec.rb +27 -0
  62. data/spec/acceptance/rest/auth_spec.rb +4 -3
  63. data/spec/acceptance/rest/base_spec.rb +2 -2
  64. data/spec/acceptance/rest/client_spec.rb +129 -10
  65. data/spec/acceptance/rest/message_spec.rb +175 -4
  66. data/spec/acceptance/rest/push_admin_spec.rb +896 -0
  67. data/spec/acceptance/rest/push_spec.rb +25 -0
  68. data/spec/acceptance/rest/time_spec.rb +1 -1
  69. data/spec/run_parallel_tests +33 -0
  70. data/spec/unit/logger_spec.rb +10 -3
  71. data/spec/unit/models/device_details_spec.rb +102 -0
  72. data/spec/unit/models/device_push_details_spec.rb +101 -0
  73. data/spec/unit/models/error_info_spec.rb +51 -3
  74. data/spec/unit/models/message_spec.rb +17 -2
  75. data/spec/unit/models/presence_message_spec.rb +1 -1
  76. data/spec/unit/models/push_channel_subscription_spec.rb +86 -0
  77. data/spec/unit/realtime/client_spec.rb +12 -0
  78. data/spec/unit/realtime/push_channel_spec.rb +36 -0
  79. data/spec/unit/rest/channel_spec.rb +8 -1
  80. data/spec/unit/rest/client_spec.rb +30 -0
  81. data/spec/unit/rest/push_channel_spec.rb +36 -0
  82. metadata +71 -8
@@ -925,7 +925,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
925
925
  last_message = nil
926
926
  channel = client.channels.get("foo")
927
927
 
928
- connection.once(:connected) do
928
+ channel.attach do
929
929
  connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
930
930
  if protocol_message.action == :message
931
931
  last_message = protocol_message
@@ -1472,7 +1472,7 @@ describe Ably::Realtime::Connection, :event_machine do
1472
1472
  channel.attach do
1473
1473
  channel.once(:suspended) do
1474
1474
  channel.publish('test').errback do |error|
1475
- expect(error).to be_a(Ably::Exceptions::MessageQueueingDisabled)
1475
+ expect(error).to be_a(Ably::Exceptions::ChannelInactive)
1476
1476
  stop_reactor
1477
1477
  end
1478
1478
  end
@@ -1732,7 +1732,7 @@ describe Ably::Realtime::Connection, :event_machine do
1732
1732
  it 'sends the protocol version param v (#G4, #RTN2f)' do
1733
1733
  expect(EventMachine).to receive(:connect) do |host, port, transport, object, url|
1734
1734
  uri = URI.parse(url)
1735
- expect(CGI::parse(uri.query)['v'][0]).to eql('1.0')
1735
+ expect(CGI::parse(uri.query)['v'][0]).to eql('1.1')
1736
1736
  stop_reactor
1737
1737
  end
1738
1738
  client
@@ -1741,7 +1741,7 @@ describe Ably::Realtime::Connection, :event_machine do
1741
1741
  it 'sends the lib version param lib (#RTN2g)' do
1742
1742
  expect(EventMachine).to receive(:connect) do |host, port, transport, object, url|
1743
1743
  uri = URI.parse(url)
1744
- expect(CGI::parse(uri.query)['lib'][0]).to match(/^ruby-1\.0\.\d+(-[\w\.]+)?+$/)
1744
+ expect(CGI::parse(uri.query)['lib'][0]).to match(/^ruby-1\.1\.\d+(-[\w\.]+)?+$/)
1745
1745
  stop_reactor
1746
1746
  end
1747
1747
  client
@@ -1761,7 +1761,7 @@ describe Ably::Realtime::Connection, :event_machine do
1761
1761
  it 'sends the lib version param lib with the variant (#RTN2g + #RSC7b)' do
1762
1762
  expect(EventMachine).to receive(:connect) do |host, port, transport, object, url|
1763
1763
  uri = URI.parse(url)
1764
- expect(CGI::parse(uri.query)['lib'][0]).to match(/^ruby-#{variant}-1\.0\.\d+(-[\w\.]+)?$/)
1764
+ expect(CGI::parse(uri.query)['lib'][0]).to match(/^ruby-#{variant}-1\.1\.\d+(-[\w\.]+)?$/)
1765
1765
  stop_reactor
1766
1766
  end
1767
1767
  client
@@ -116,7 +116,7 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
116
116
  context 'Integer' do
117
117
  let(:data) { 1 }
118
118
 
119
- it 'is raises an UnsupportedDataType 40011 exception' do
119
+ it 'is raises an UnsupportedDataType 40013 exception' do
120
120
  expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
121
121
  stop_reactor
122
122
  end
@@ -125,7 +125,7 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
125
125
  context 'Float' do
126
126
  let(:data) { 1.1 }
127
127
 
128
- it 'is raises an UnsupportedDataType 40011 exception' do
128
+ it 'is raises an UnsupportedDataType 40013 exception' do
129
129
  expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
130
130
  stop_reactor
131
131
  end
@@ -134,7 +134,7 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
134
134
  context 'Boolean' do
135
135
  let(:data) { true }
136
136
 
137
- it 'is raises an UnsupportedDataType 40011 exception' do
137
+ it 'is raises an UnsupportedDataType 40013 exception' do
138
138
  expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
139
139
  stop_reactor
140
140
  end
@@ -143,7 +143,7 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
143
143
  context 'False' do
144
144
  let(:data) { false }
145
145
 
146
- it 'is raises an UnsupportedDataType 40011 exception' do
146
+ it 'is raises an UnsupportedDataType 40013 exception' do
147
147
  expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
148
148
  stop_reactor
149
149
  end
@@ -413,30 +413,32 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
413
413
  let(:encrypted_channel) { client.channel(channel_name, cipher: cipher_options) }
414
414
 
415
415
  it 'encrypts message automatically before they are pushed to the server (#RTL7d)' do
416
- encrypted_channel.__incoming_msgbus__.unsubscribe # remove all subscribe callbacks that could decrypt the message
416
+ encrypted_channel.attach do
417
+ encrypted_channel.__incoming_msgbus__.unsubscribe # remove all subscribe callbacks that could decrypt the message
417
418
 
418
- encrypted_channel.__incoming_msgbus__.subscribe(:message) do |message|
419
- if protocol == :json
420
- expect(message['encoding']).to eql(encrypted_encoding)
421
- expect(message['data']).to eql(encrypted_data)
422
- else
423
- # Messages received over binary protocol will not have Base64 encoded data
424
- expect(message['encoding']).to eql(encrypted_encoding.gsub(%r{/base64$}, ''))
425
- expect(message['data']).to eql(encrypted_data_decoded)
419
+ encrypted_channel.__incoming_msgbus__.subscribe(:message) do |message|
420
+ if protocol == :json
421
+ expect(message['encoding']).to eql(encrypted_encoding)
422
+ expect(message['data']).to eql(encrypted_data)
423
+ else
424
+ # Messages received over binary protocol will not have Base64 encoded data
425
+ expect(message['encoding']).to eql(encrypted_encoding.gsub(%r{/base64$}, ''))
426
+ expect(message['data']).to eql(encrypted_data_decoded)
427
+ end
428
+ stop_reactor
426
429
  end
427
- stop_reactor
428
- end
429
430
 
430
- encrypted_channel.publish 'example', encoded_data_decoded
431
+ encrypted_channel.publish 'example', encoded_data_decoded
432
+ end
431
433
  end
432
434
 
433
435
  it 'sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)' do
434
- encrypted_channel.publish 'example', encoded_data_decoded
435
436
  encrypted_channel.subscribe do |message|
436
437
  expect(message.data).to eql(encoded_data_decoded)
437
438
  expect(message.encoding).to be_nil
438
439
  stop_reactor
439
440
  end
441
+ encrypted_channel.publish 'example', encoded_data_decoded
440
442
  end
441
443
  end
442
444
  end
@@ -256,7 +256,7 @@ describe Ably::Realtime::Presence, :event_machine do
256
256
  context 'Integer' do
257
257
  let(:data) { 1 }
258
258
 
259
- it 'raises an UnsupportedDataType 40011 exception' do
259
+ it 'raises an UnsupportedDataType 40013 exception' do
260
260
  expect { presence_action(method_name, data) }.to raise_error(Ably::Exceptions::UnsupportedDataType)
261
261
  stop_reactor
262
262
  end
@@ -265,7 +265,7 @@ describe Ably::Realtime::Presence, :event_machine do
265
265
  context 'Float' do
266
266
  let(:data) { 1.1 }
267
267
 
268
- it 'raises an UnsupportedDataType 40011 exception' do
268
+ it 'raises an UnsupportedDataType 40013 exception' do
269
269
  expect { presence_action(method_name, data) }.to raise_error(Ably::Exceptions::UnsupportedDataType)
270
270
  stop_reactor
271
271
  end
@@ -274,7 +274,7 @@ describe Ably::Realtime::Presence, :event_machine do
274
274
  context 'Boolean' do
275
275
  let(:data) { true }
276
276
 
277
- it 'raises an UnsupportedDataType 40011 exception' do
277
+ it 'raises an UnsupportedDataType 40013 exception' do
278
278
  expect { presence_action(method_name, data) }.to raise_error(Ably::Exceptions::UnsupportedDataType)
279
279
  stop_reactor
280
280
  end
@@ -283,7 +283,7 @@ describe Ably::Realtime::Presence, :event_machine do
283
283
  context 'False' do
284
284
  let(:data) { false }
285
285
 
286
- it 'raises an UnsupportedDataType 40011 exception' do
286
+ it 'raises an UnsupportedDataType 40013 exception' do
287
287
  expect { presence_action(method_name, data) }.to raise_error(Ably::Exceptions::UnsupportedDataType)
288
288
  stop_reactor
289
289
  end
@@ -2299,7 +2299,7 @@ describe Ably::Realtime::Presence, :event_machine do
2299
2299
  presence_events << [presence_message.client_id, presence_message.action.to_sym]
2300
2300
  if presence_message.action == :leave
2301
2301
  expect(presence_message.id).to be_nil
2302
- expect(presence_message.timestamp.to_f * 1000).to be_within(20).of(Time.now.to_f * 1000)
2302
+ expect(presence_message.timestamp.to_f * 1000).to be_within(200).of(Time.now.to_f * 1000)
2303
2303
  end
2304
2304
  end
2305
2305
 
@@ -0,0 +1,696 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ # These tests are a subset of Ably::Rest::Push::Admin in async EM style
5
+ # The more robust complete test suite is in rest/push_admin_spec.rb
6
+ describe Ably::Realtime::Push::Admin, :event_machine do
7
+ include Ably::Modules::Conversions
8
+
9
+ vary_by_protocol do
10
+ let(:default_options) { { key: api_key, environment: environment, protocol: protocol} }
11
+ let(:client_options) { default_options }
12
+ let(:client) do
13
+ Ably::Realtime::Client.new(client_options)
14
+ end
15
+
16
+ let(:basic_notification_payload) do
17
+ {
18
+ notification: {
19
+ title: 'Test message',
20
+ body: 'Test message body'
21
+ }
22
+ }
23
+ end
24
+
25
+ let(:basic_recipient) do
26
+ {
27
+ transport_type: 'apns',
28
+ deviceToken: 'foo.bar'
29
+ }
30
+ end
31
+
32
+ describe '#publish' do
33
+ subject { client.push.admin }
34
+
35
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
36
+ publish_deferrable = subject.publish(basic_recipient, basic_notification_payload)
37
+ expect(publish_deferrable).to be_a(Ably::Util::SafeDeferrable)
38
+ publish_deferrable.callback do
39
+ stop_reactor
40
+ end
41
+ end
42
+
43
+ context 'invalid arguments' do
44
+ it 'raises an exception with a nil recipient' do
45
+ expect { subject.publish(nil, {}) }.to raise_error ArgumentError, /Expecting a Hash/
46
+ stop_reactor
47
+ end
48
+
49
+ it 'raises an exception with a empty recipient' do
50
+ expect { subject.publish({}, {}) }.to raise_error ArgumentError, /empty/
51
+ stop_reactor
52
+ end
53
+
54
+ it 'raises an exception with a nil recipient' do
55
+ expect { subject.publish(basic_recipient, nil) }.to raise_error ArgumentError, /Expecting a Hash/
56
+ stop_reactor
57
+ end
58
+
59
+ it 'raises an exception with a empty recipient' do
60
+ expect { subject.publish(basic_recipient, {}) }.to raise_error ArgumentError, /empty/
61
+ stop_reactor
62
+ end
63
+ end
64
+
65
+ context 'invalid recipient' do
66
+ it 'raises an error after receiving a 40x realtime response' do
67
+ skip 'validation on raw push is not enabled in realtime'
68
+ subject.publish({ invalid_recipient_details: 'foo.bar' }, basic_notification_payload).errback do |error|
69
+ expect(error.message).to match(/Invalid recipient/)
70
+ stop_reactor
71
+ end
72
+ end
73
+ end
74
+
75
+ context 'invalid push data' do
76
+ it 'raises an error after receiving a 40x realtime response' do
77
+ skip 'validation on raw push is not enabled in realtime'
78
+ subject.publish(basic_recipient, { invalid_property_only: true }).errback do |error|
79
+ expect(error.message).to match(/Invalid push notification data/)
80
+ stop_reactor
81
+ end
82
+ end
83
+ end
84
+
85
+ context 'recipient variable case', webmock: true do
86
+ let(:recipient_payload) do
87
+ {
88
+ camel_case: {
89
+ second_level_camel_case: 'val'
90
+ }
91
+ }
92
+ end
93
+
94
+ let(:content_type) do
95
+ if protocol == :msgpack
96
+ 'application/x-msgpack'
97
+ else
98
+ 'application/json'
99
+ end
100
+ end
101
+
102
+ def request_body(request, protocol)
103
+ if protocol == :msgpack
104
+ MessagePack.unpack(request.body)
105
+ else
106
+ JSON.parse(request.body)
107
+ end
108
+ end
109
+
110
+ def serialize(object, protocol)
111
+ if protocol == :msgpack
112
+ MessagePack.pack(object)
113
+ else
114
+ JSON.dump(object)
115
+ end
116
+ end
117
+
118
+ let!(:publish_stub) do
119
+ stub_request(:post, "#{client.rest_client.endpoint}/push/publish").
120
+ with do |request|
121
+ expect(request_body(request, protocol)['recipient']['camelCase']['secondLevelCamelCase']).to eql('val')
122
+ expect(request_body(request, protocol)['recipient']).to_not have_key('camel_case')
123
+ true
124
+ end.to_return(
125
+ :status => 201,
126
+ :body => serialize({}, protocol),
127
+ :headers => { 'Content-Type' => content_type }
128
+ )
129
+ end
130
+
131
+ it 'is converted to snakeCase' do
132
+ subject.publish(recipient_payload, basic_notification_payload) do
133
+ expect(publish_stub).to have_been_requested
134
+ stop_reactor
135
+ end
136
+ end
137
+ end
138
+
139
+ it 'accepts valid push data and recipient' do
140
+ subject.publish(basic_recipient, basic_notification_payload) do
141
+ stop_reactor
142
+ end
143
+ end
144
+
145
+ context 'using test environment channel recipient (#RSH1a)' do
146
+ let(:channel) { random_str }
147
+ let(:recipient) do
148
+ {
149
+ 'transportType' => 'ablyChannel',
150
+ 'channel' => channel,
151
+ 'ablyKey' => api_key,
152
+ 'ablyUrl' => client.rest_client.endpoint.to_s
153
+ }
154
+ end
155
+ let(:notification_payload) do
156
+ {
157
+ notification: {
158
+ title: random_str,
159
+ },
160
+ data: {
161
+ foo: random_str
162
+ }
163
+ }
164
+ end
165
+ let(:push_channel) do
166
+ client.channels.get(channel)
167
+ end
168
+
169
+ it 'triggers a push notification' do
170
+ push_channel.attach do
171
+ push_channel.subscribe do |message|
172
+ expect(message.name).to eql('__ably_push__')
173
+ expect(JSON.parse(message.data)['data']).to eql(JSON.parse(notification_payload[:data].to_json))
174
+ stop_reactor
175
+ end
176
+ subject.publish recipient, notification_payload
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ describe '#device_registrations' do
183
+ subject { client.push.admin.device_registrations }
184
+ let(:rest_device_registrations) {
185
+ client.rest_client.push.admin.device_registrations
186
+ }
187
+
188
+ context 'without permissions' do
189
+ let(:client_options) do
190
+ default_options.merge(
191
+ use_token_auth: true,
192
+ default_token_params: { capability: { :foo => ['subscribe'] } },
193
+ log_level: :fatal,
194
+ )
195
+ end
196
+
197
+ it 'raises a permissions not authorized exception' do
198
+ subject.get('does-not-exist').errback do |err|
199
+ expect(err).to be_a(Ably::Exceptions::UnauthorizedRequest)
200
+ subject.list.errback do |err|
201
+ expect(err).to be_a(Ably::Exceptions::UnauthorizedRequest)
202
+ subject.remove('does-not-exist').errback do |err|
203
+ expect(err).to be_a(Ably::Exceptions::UnauthorizedRequest)
204
+ subject.remove_where(device_id: 'does-not-exist').errback do |err|
205
+ expect(err).to be_a(Ably::Exceptions::UnauthorizedRequest)
206
+ stop_reactor
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+
214
+ describe '#list' do
215
+ let(:client_id) { random_str }
216
+ let(:fixture_count) { 6 }
217
+
218
+ before do
219
+ fixture_count.times.map do |index|
220
+ Thread.new do # Parallelise the setup
221
+ rest_device_registrations.save({
222
+ id: "device-#{client_id}-#{index}",
223
+ platform: 'ios',
224
+ form_factor: 'phone',
225
+ client_id: client_id,
226
+ push: {
227
+ recipient: {
228
+ transport_type: 'gcm',
229
+ registration_token: 'secret_token',
230
+ }
231
+ }
232
+ })
233
+ end
234
+ end.each(&:join) # Wait for all threads to complete
235
+ end
236
+
237
+ after do
238
+ rest_device_registrations.remove_where client_id: client_id
239
+ end
240
+
241
+ it 'returns a PaginatedResult object containing DeviceDetails objects' do
242
+ subject.list do |page|
243
+ expect(page).to be_a(Ably::Models::PaginatedResult)
244
+ expect(page.items.first).to be_a(Ably::Models::DeviceDetails)
245
+ stop_reactor
246
+ end
247
+ end
248
+
249
+ it 'supports paging' do
250
+ subject.list(limit: 3, client_id: client_id) do |page|
251
+ expect(page).to be_a(Ably::Models::PaginatedResult)
252
+
253
+ expect(page.items.count).to eql(3)
254
+ page.next do |page|
255
+ expect(page.items.count).to eql(3)
256
+ page.next do |page|
257
+ expect(page.items.count).to eql(0)
258
+ expect(page).to be_last
259
+ stop_reactor
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ it 'raises an exception if params are invalid' do
266
+ expect { subject.list("invalid_arg") }.to raise_error(ArgumentError)
267
+ stop_reactor
268
+ end
269
+ end
270
+
271
+ describe '#get' do
272
+ let(:fixture_count) { 2 }
273
+ let(:client_id) { random_str }
274
+
275
+ before do
276
+ fixture_count.times.map do |index|
277
+ Thread.new do # Parallelise the setup
278
+ rest_device_registrations.save({
279
+ id: "device-#{client_id}-#{index}",
280
+ platform: 'ios',
281
+ form_factor: 'phone',
282
+ client_id: client_id,
283
+ push: {
284
+ recipient: {
285
+ transport_type: 'gcm',
286
+ registration_token: 'secret_token',
287
+ }
288
+ }
289
+ })
290
+ end
291
+ end.each(&:join) # Wait for all threads to complete
292
+ end
293
+
294
+ after do
295
+ rest_device_registrations.remove_where client_id: client_id
296
+ end
297
+
298
+ it 'returns a DeviceDetails object if a device ID string is provided' do
299
+ subject.get("device-#{client_id}-0").callback do |device|
300
+ expect(device).to be_a(Ably::Models::DeviceDetails)
301
+ expect(device.platform).to eql('ios')
302
+ expect(device.client_id).to eql(client_id)
303
+ expect(device.push.recipient.fetch(:transport_type)).to eql('gcm')
304
+ stop_reactor
305
+ end
306
+ end
307
+
308
+ context 'with a failed request' do
309
+ let(:client_options) do
310
+ default_options.merge(
311
+ log_level: :fatal,
312
+ )
313
+ end
314
+
315
+ it 'raises a ResourceMissing exception if device ID does not exist' do
316
+ subject.get("device-does-not-exist").errback do |err|
317
+ expect(err).to be_a(Ably::Exceptions::ResourceMissing)
318
+ stop_reactor
319
+ end
320
+ end
321
+ end
322
+ end
323
+
324
+ describe '#save' do
325
+ let(:device_id) { random_str }
326
+ let(:client_id) { random_str }
327
+ let(:transport_token) { random_str }
328
+
329
+ let(:device_details) do
330
+ {
331
+ id: device_id,
332
+ platform: 'android',
333
+ form_factor: 'phone',
334
+ client_id: client_id,
335
+ metadata: {
336
+ foo: 'bar',
337
+ deep: {
338
+ val: true
339
+ }
340
+ },
341
+ push: {
342
+ recipient: {
343
+ transport_type: 'apns',
344
+ device_token: transport_token,
345
+ foo_bar: 'string',
346
+ },
347
+ error_reason: {
348
+ message: "this will be ignored"
349
+ },
350
+ }
351
+ }
352
+ end
353
+
354
+ after do
355
+ rest_device_registrations.remove_where client_id: client_id
356
+ end
357
+
358
+ it 'saves the new DeviceDetails Hash object' do
359
+ subject.save(device_details) do
360
+ subject.get(device_details.fetch(:id)) do |device_retrieved|
361
+ expect(device_retrieved).to be_a(Ably::Models::DeviceDetails)
362
+ expect(device_retrieved.id).to eql(device_id)
363
+ expect(device_retrieved.platform).to eql('android')
364
+ stop_reactor
365
+ end
366
+ end
367
+ end
368
+
369
+ context 'with a failed request' do
370
+ let(:client_options) do
371
+ default_options.merge(
372
+ log_level: :fatal,
373
+ )
374
+ end
375
+
376
+ it 'fails if data is invalid' do
377
+ subject.save(id: random_str, foo: 'bar').errback do |err|
378
+ expect(err).to be_a(Ably::Exceptions::InvalidRequest)
379
+ stop_reactor
380
+ end
381
+ end
382
+ end
383
+ end
384
+
385
+ describe '#remove_where' do
386
+ let(:device_id) { random_str }
387
+ let(:client_id) { random_str }
388
+
389
+ before do
390
+ rest_device_registrations.save({
391
+ id: "device-#{client_id}-0",
392
+ platform: 'ios',
393
+ form_factor: 'phone',
394
+ client_id: client_id,
395
+ push: {
396
+ recipient: {
397
+ transport_type: 'gcm',
398
+ registrationToken: 'secret_token',
399
+ }
400
+ }
401
+ })
402
+ end
403
+
404
+ after do
405
+ rest_device_registrations.remove_where client_id: client_id
406
+ end
407
+
408
+ it 'removes all matching device registrations by client_id' do
409
+ subject.remove_where(client_id: client_id, full_wait: true) do
410
+ subject.list do |page|
411
+ expect(page.items.count).to eql(0)
412
+ stop_reactor
413
+ end
414
+ end
415
+ end
416
+ end
417
+
418
+ describe '#remove' do
419
+ let(:device_id) { random_str }
420
+ let(:client_id) { random_str }
421
+
422
+ before do
423
+ rest_device_registrations.save({
424
+ id: "device-#{client_id}-0",
425
+ platform: 'ios',
426
+ form_factor: 'phone',
427
+ client_id: client_id,
428
+ push: {
429
+ recipient: {
430
+ transport_type: 'gcm',
431
+ registration_token: 'secret_token',
432
+ }
433
+ }
434
+ })
435
+ end
436
+
437
+ after do
438
+ rest_device_registrations.remove_where client_id: client_id
439
+ end
440
+
441
+ it 'removes the provided device id string' do
442
+ subject.remove("device-#{client_id}-0") do
443
+ subject.list do |page|
444
+ expect(page.items.count).to eql(0)
445
+ stop_reactor
446
+ end
447
+ end
448
+ end
449
+ end
450
+ end
451
+
452
+ describe '#channel_subscriptions' do
453
+ let(:client_id) { random_str }
454
+ let(:device_id) { random_str }
455
+ let(:device_id_2) { random_str }
456
+ let(:default_device_attr) {
457
+ {
458
+ platform: 'ios',
459
+ form_factor: 'phone',
460
+ client_id: client_id,
461
+ push: {
462
+ recipient: {
463
+ transport_type: 'gcm',
464
+ registration_token: 'secret_token',
465
+ }
466
+ }
467
+ }
468
+ }
469
+
470
+ let(:rest_device_registrations) {
471
+ client.rest_client.push.admin.device_registrations
472
+ }
473
+
474
+ let(:rest_channel_subscriptions) {
475
+ client.rest_client.push.admin.channel_subscriptions
476
+ }
477
+
478
+ subject {
479
+ client.push.admin.channel_subscriptions
480
+ }
481
+
482
+ # Set up 2 devices with the same client_id
483
+ # and two device with the unique device_id and no client_id
484
+ before do
485
+ [
486
+ lambda { rest_device_registrations.save(default_device_attr.merge(id: device_id)) },
487
+ lambda { rest_device_registrations.save(default_device_attr.merge(id: device_id_2)) },
488
+ lambda { rest_device_registrations.save(default_device_attr.merge(client_id: client_id, id: random_str)) },
489
+ lambda { rest_device_registrations.save(default_device_attr.merge(client_id: client_id, id: random_str)) }
490
+ ].map do |proc|
491
+ Thread.new { proc.call }
492
+ end.each(&:join) # Wait for all threads to complete
493
+ end
494
+
495
+ after do
496
+ rest_device_registrations.remove_where client_id: client_id
497
+ rest_device_registrations.remove_where device_id: device_id
498
+ end
499
+
500
+ describe '#list' do
501
+ let(:fixture_count) { 6 }
502
+
503
+ before do
504
+ fixture_count.times.map do |index|
505
+ Thread.new { rest_channel_subscriptions.save(channel: "pushenabled:#{random_str}", client_id: client_id) }
506
+ end + fixture_count.times.map do |index|
507
+ Thread.new { rest_channel_subscriptions.save(channel: "pushenabled:#{random_str}", device_id: device_id) }
508
+ end.each(&:join) # Wait for all threads to complete
509
+ end
510
+
511
+ it 'returns a PaginatedResult object containing DeviceDetails objects' do
512
+ subject.list(client_id: client_id) do |page|
513
+ expect(page).to be_a(Ably::Models::PaginatedResult)
514
+ expect(page.items.first).to be_a(Ably::Models::PushChannelSubscription)
515
+ stop_reactor
516
+ end
517
+ end
518
+
519
+ it 'supports paging' do
520
+ subject.list(limit: 3, device_id: device_id) do |page|
521
+ expect(page).to be_a(Ably::Models::PaginatedResult)
522
+
523
+ expect(page.items.count).to eql(3)
524
+ page.next do |page|
525
+ expect(page.items.count).to eql(3)
526
+ page.next do |page|
527
+ expect(page.items.count).to eql(0)
528
+ expect(page).to be_last
529
+ stop_reactor
530
+ end
531
+ end
532
+ end
533
+ end
534
+
535
+ it 'raises an exception if none of the required filters are provided' do
536
+ expect { subject.list({ limit: 100 }) }.to raise_error(ArgumentError)
537
+ stop_reactor
538
+ end
539
+ end
540
+
541
+ describe '#list_channels' do
542
+ let(:fixture_count) { 6 }
543
+
544
+ before(:context) do
545
+ reload_test_app # TODO: Review if necessary later, currently other tests may affect list_channels
546
+ end
547
+
548
+ before do
549
+ fixture_count.times.map do |index|
550
+ Thread.new do
551
+ rest_channel_subscriptions.save(channel: "pushenabled:#{index}:#{random_str}", client_id: client_id)
552
+ end
553
+ end.each(&:join) # Wait for all threads to complete
554
+ end
555
+
556
+ after do
557
+ rest_channel_subscriptions.remove_where client_id: client_id, full_wait: true # undocumented arg to do deletes synchronously
558
+ end
559
+
560
+ it 'returns a PaginatedResult object containing String objects' do
561
+ subject.list_channels do |page|
562
+ expect(page).to be_a(Ably::Models::PaginatedResult)
563
+ expect(page.items.first).to be_a(String)
564
+ expect(page.items.length).to eql(fixture_count)
565
+ stop_reactor
566
+ end
567
+ end
568
+ end
569
+
570
+ describe '#save' do
571
+ let(:channel) { "pushenabled:#{random_str}" }
572
+ let(:client_id) { random_str }
573
+ let(:device_id) { random_str }
574
+
575
+ it 'saves the new client_id PushChannelSubscription Hash object' do
576
+ subject.save(channel: channel, client_id: client_id) do
577
+ subject.list(client_id: client_id) do |page|
578
+ channel_sub = page.items.first
579
+ expect(channel_sub).to be_a(Ably::Models::PushChannelSubscription)
580
+ expect(channel_sub.channel).to eql(channel)
581
+ stop_reactor
582
+ end
583
+ end
584
+ end
585
+
586
+ it 'raises an exception for invalid params' do
587
+ expect { subject.save(channel: '', client_id: '') }.to raise_error ArgumentError
588
+ expect { subject.save({}) }.to raise_error ArgumentError
589
+ stop_reactor
590
+ end
591
+
592
+ context 'failed requests' do
593
+ let(:client_options) do
594
+ default_options.merge(
595
+ log_level: :fatal,
596
+ )
597
+ end
598
+
599
+ it 'fails for invalid requests' do
600
+ subject.save(channel: 'not-enabled-channel', device_id: 'foo').errback do |err|
601
+ expect(err).to be_a(Ably::Exceptions::UnauthorizedRequest)
602
+ subject.save(channel: 'pushenabled:foo', device_id: 'not-registered-so-will-fail').errback do |err|
603
+ expect(err).to be_a(Ably::Exceptions::InvalidRequest)
604
+ stop_reactor
605
+ end
606
+ end
607
+ end
608
+ end
609
+ end
610
+
611
+ describe '#remove_where' do
612
+ let(:client_id) { random_str }
613
+ let(:device_id) { random_str }
614
+ let(:fixed_channel) { "pushenabled:#{random_str}" }
615
+
616
+ let(:fixture_count) { 6 }
617
+
618
+ before do
619
+ fixture_count.times.map do |index|
620
+ Thread.new do
621
+ rest_channel_subscriptions.save(channel: "pushenabled:#{random_str}", client_id: client_id)
622
+ end
623
+ end.each(&:join) # Wait for all threads to complete
624
+ end
625
+
626
+ it 'removes matching client_ids' do
627
+ subject.list(client_id: client_id) do |page|
628
+ expect(page.items.count).to eql(fixture_count)
629
+ subject.remove_where(client_id: client_id, full_wait: true) do
630
+ subject.list(client_id: client_id) do |page|
631
+ expect(page.items.count).to eql(0)
632
+ stop_reactor
633
+ end
634
+ end
635
+ end
636
+ end
637
+
638
+ context 'failed requests' do
639
+ let(:client_options) do
640
+ default_options.merge(
641
+ log_level: :fatal,
642
+ )
643
+ end
644
+
645
+ it 'device_id and client_id filters in the same request are not supported' do
646
+ subject.remove_where(device_id: device_id, client_id: client_id).errback do |err|
647
+ expect(err).to be_a(Ably::Exceptions::InvalidRequest)
648
+ stop_reactor
649
+ end
650
+ end
651
+ end
652
+
653
+ it 'succeeds on no match' do
654
+ subject.remove_where(device_id: random_str, full_wait: true) do
655
+ subject.list(client_id: client_id) do |page|
656
+ expect(page.items.count).to eql(fixture_count)
657
+ stop_reactor
658
+ end
659
+ end
660
+ end
661
+ end
662
+
663
+ describe '#remove' do
664
+ let(:channel) { "pushenabled:#{random_str}" }
665
+ let(:channel2) { "pushenabled:#{random_str}" }
666
+ let(:client_id) { random_str }
667
+ let(:device_id) { random_str }
668
+
669
+ before do
670
+ rest_channel_subscriptions.save(channel: channel, client_id: client_id)
671
+ end
672
+
673
+ it 'removes match for Hash object by channel and client_id' do
674
+ subject.list(client_id: client_id) do |page|
675
+ expect(page.items.count).to eql(1)
676
+ subject.remove(channel: channel, client_id: client_id, full_wait: true) do
677
+ subject.list(client_id: client_id) do |page|
678
+ expect(page.items.count).to eql(0)
679
+ stop_reactor
680
+ end
681
+ end
682
+ end
683
+ end
684
+
685
+ it 'succeeds even if there is no match' do
686
+ subject.remove(device_id: 'does-not-exist', channel: random_str) do
687
+ subject.list(device_id: 'does-not-exist') do |page|
688
+ expect(page.items.count).to eql(0)
689
+ stop_reactor
690
+ end
691
+ end
692
+ end
693
+ end
694
+ end
695
+ end
696
+ end