ably 1.0.7 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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