ably-rest 1.0.6 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -14
  3. data/lib/submodules/ably-ruby/.editorconfig +14 -0
  4. data/lib/submodules/ably-ruby/.travis.yml +4 -4
  5. data/lib/submodules/ably-ruby/CHANGELOG.md +43 -2
  6. data/lib/submodules/ably-ruby/README.md +3 -2
  7. data/lib/submodules/ably-ruby/Rakefile +32 -0
  8. data/lib/submodules/ably-ruby/SPEC.md +1277 -835
  9. data/lib/submodules/ably-ruby/ably.gemspec +9 -4
  10. data/lib/submodules/ably-ruby/lib/ably/auth.rb +30 -4
  11. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +8 -2
  12. data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +1 -1
  13. data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +1 -1
  14. data/lib/submodules/ably-ruby/lib/ably/models/device_details.rb +87 -0
  15. data/lib/submodules/ably-ruby/lib/ably/models/device_push_details.rb +86 -0
  16. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +23 -2
  17. data/lib/submodules/ably-ruby/lib/ably/models/idiomatic_ruby_wrapper.rb +4 -4
  18. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +32 -2
  19. data/lib/submodules/ably-ruby/lib/ably/models/push_channel_subscription.rb +89 -0
  20. data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +1 -1
  21. data/lib/submodules/ably-ruby/lib/ably/modules/encodeable.rb +1 -1
  22. data/lib/submodules/ably-ruby/lib/ably/modules/exception_codes.rb +128 -0
  23. data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +15 -2
  24. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +1 -1
  25. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +1 -0
  26. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +1 -1
  27. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +24 -102
  28. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +2 -6
  29. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/publisher.rb +74 -0
  31. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/push_channel.rb +62 -0
  32. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +87 -0
  33. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +6 -2
  34. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  35. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +8 -5
  36. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +7 -7
  37. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +1 -1
  38. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +4 -4
  39. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +3 -3
  40. data/lib/submodules/ably-ruby/lib/ably/realtime/push.rb +40 -0
  41. data/lib/submodules/ably-ruby/lib/ably/realtime/push/admin.rb +61 -0
  42. data/lib/submodules/ably-ruby/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  43. data/lib/submodules/ably-ruby/lib/ably/realtime/push/device_registrations.rb +105 -0
  44. data/lib/submodules/ably-ruby/lib/ably/rest.rb +1 -0
  45. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +33 -5
  46. data/lib/submodules/ably-ruby/lib/ably/rest/channel/push_channel.rb +62 -0
  47. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +138 -28
  48. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
  49. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -0
  50. data/lib/submodules/ably-ruby/lib/ably/rest/push.rb +42 -0
  51. data/lib/submodules/ably-ruby/lib/ably/rest/push/admin.rb +54 -0
  52. data/lib/submodules/ably-ruby/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  53. data/lib/submodules/ably-ruby/lib/ably/rest/push/device_registrations.rb +103 -0
  54. data/lib/submodules/ably-ruby/lib/ably/version.rb +7 -2
  55. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +233 -8
  56. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +166 -51
  57. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +149 -0
  58. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +1 -1
  59. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +4 -4
  60. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +19 -17
  61. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +5 -5
  62. data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_admin_spec.rb +696 -0
  63. data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_spec.rb +27 -0
  64. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +41 -3
  65. data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +2 -2
  66. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +129 -10
  67. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +175 -4
  68. data/lib/submodules/ably-ruby/spec/acceptance/rest/push_admin_spec.rb +896 -0
  69. data/lib/submodules/ably-ruby/spec/acceptance/rest/push_spec.rb +25 -0
  70. data/lib/submodules/ably-ruby/spec/acceptance/rest/time_spec.rb +1 -1
  71. data/lib/submodules/ably-ruby/spec/run_parallel_tests +33 -0
  72. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +10 -3
  73. data/lib/submodules/ably-ruby/spec/unit/models/device_details_spec.rb +102 -0
  74. data/lib/submodules/ably-ruby/spec/unit/models/device_push_details_spec.rb +101 -0
  75. data/lib/submodules/ably-ruby/spec/unit/models/error_info_spec.rb +51 -3
  76. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +17 -2
  77. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +1 -1
  78. data/lib/submodules/ably-ruby/spec/unit/models/push_channel_subscription_spec.rb +86 -0
  79. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +12 -0
  80. data/lib/submodules/ably-ruby/spec/unit/realtime/push_channel_spec.rb +36 -0
  81. data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +8 -1
  82. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +30 -0
  83. data/lib/submodules/ably-ruby/spec/unit/rest/push_channel_spec.rb +36 -0
  84. metadata +29 -4
@@ -0,0 +1,896 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Ably::Rest::Push::Admin do
5
+ include Ably::Modules::Conversions
6
+
7
+ vary_by_protocol do
8
+ let(:default_options) { { key: api_key, environment: environment, protocol: protocol} }
9
+ let(:client_options) { default_options }
10
+ let(:client) do
11
+ Ably::Rest::Client.new(client_options)
12
+ end
13
+
14
+ let(:basic_notification_payload) do
15
+ {
16
+ notification: {
17
+ title: 'Test message',
18
+ body: 'Test message body'
19
+ }
20
+ }
21
+ end
22
+
23
+ let(:basic_recipient) do
24
+ {
25
+ transport_type: 'apns',
26
+ deviceToken: 'foo.bar'
27
+ }
28
+ end
29
+
30
+ describe '#publish' do
31
+ subject { client.push.admin }
32
+
33
+ context 'without publish permissions' do
34
+ let(:capability) { { :foo => ['subscribe'] } }
35
+
36
+ before do
37
+ client.auth.authorize(capability: capability)
38
+ end
39
+
40
+ it 'raises a permissions issue exception' do
41
+ expect { subject.publish(basic_recipient, basic_notification_payload) }.to raise_error Ably::Exceptions::UnauthorizedRequest
42
+ end
43
+ end
44
+
45
+ context 'invalid arguments (#RHS1a)' do
46
+ it 'raises an exception with a nil recipient' do
47
+ expect { subject.publish(nil, {}) }.to raise_error ArgumentError, /Expecting a Hash/
48
+ end
49
+
50
+ it 'raises an exception with a empty recipient' do
51
+ expect { subject.publish({}, {}) }.to raise_error ArgumentError, /empty/
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
+ end
57
+
58
+ it 'raises an exception with a empty recipient' do
59
+ expect { subject.publish(basic_recipient, {}) }.to raise_error ArgumentError, /empty/
60
+ end
61
+ end
62
+
63
+ context 'invalid recipient (#RSH1a)' do
64
+ it 'raises an error after receiving a 40x realtime response' do
65
+ expect { subject.publish({ invalid_recipient_details: 'foo.bar' }, basic_notification_payload) }.to raise_error Ably::Exceptions::InvalidRequest
66
+ end
67
+ end
68
+
69
+ context 'invalid push data (#RSH1a)' do
70
+ it 'raises an error after receiving a 40x realtime response' do
71
+ expect { subject.publish(basic_recipient, { invalid_property_only: true }) }.to raise_error Ably::Exceptions::InvalidRequest
72
+ end
73
+ end
74
+
75
+ context 'recipient variable case', webmock: true do
76
+ let(:recipient_payload) do
77
+ {
78
+ camel_case: {
79
+ second_level_camel_case: 'val'
80
+ }
81
+ }
82
+ end
83
+
84
+ let(:content_type) do
85
+ if protocol == :msgpack
86
+ 'application/x-msgpack'
87
+ else
88
+ 'application/json'
89
+ end
90
+ end
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
+ let!(:publish_stub) do
109
+ stub_request(:post, "#{client.endpoint}/push/publish").
110
+ 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')
113
+ true
114
+ end.to_return(
115
+ :status => 201,
116
+ :body => serialize({}, protocol),
117
+ :headers => { 'Content-Type' => content_type }
118
+ )
119
+ end
120
+
121
+ it 'is converted to snakeCase' do
122
+ subject.publish(recipient_payload, basic_notification_payload)
123
+ expect(publish_stub).to have_been_requested
124
+ end
125
+ end
126
+
127
+ it 'accepts valid push data and recipient (#RSH1a)' do
128
+ subject.publish(basic_recipient, basic_notification_payload)
129
+ end
130
+
131
+ context 'using test environment channel recipient (#RSH1a)' do
132
+ let(:channel) { random_str }
133
+ let(:recipient) do
134
+ {
135
+ 'transportType' => 'ablyChannel',
136
+ 'channel' => channel,
137
+ 'ablyKey' => api_key,
138
+ 'ablyUrl' => client.endpoint.to_s
139
+ }
140
+ end
141
+ let(:notification_payload) do
142
+ {
143
+ notification: {
144
+ title: random_str,
145
+ },
146
+ data: {
147
+ foo: random_str
148
+ }
149
+ }
150
+ end
151
+
152
+ it 'triggers a push notification' do
153
+ subject.publish(recipient, notification_payload)
154
+ sleep 5
155
+ notification_published_on_channel = client.channels.get(channel).history.items.first
156
+ expect(notification_published_on_channel.name).to eql('__ably_push__')
157
+ expect(JSON.parse(notification_published_on_channel.data)['data']).to eql(JSON.parse(notification_payload[:data].to_json))
158
+ end
159
+ end
160
+ end
161
+
162
+ describe '#device_registrations (#RSH1b)' do
163
+ subject { client.push.admin.device_registrations }
164
+
165
+ context 'without permissions' do
166
+ let(:capability) { { :foo => ['subscribe'] } }
167
+
168
+ before do
169
+ client.auth.authorize(capability: capability)
170
+ end
171
+
172
+ it 'raises a permissions not authorized exception' do
173
+ expect { subject.get('does-not-exist') }.to raise_error Ably::Exceptions::UnauthorizedRequest
174
+ expect { subject.list }.to raise_error Ably::Exceptions::UnauthorizedRequest
175
+ expect { subject.remove('does-not-exist') }.to raise_error Ably::Exceptions::UnauthorizedRequest
176
+ expect { subject.remove_where(device_id: 'does-not-exist') }.to raise_error Ably::Exceptions::UnauthorizedRequest
177
+ end
178
+ end
179
+
180
+ describe '#list (#RSH1b2)' do
181
+ let(:client_id) { random_str }
182
+ let(:fixture_count) { 6 }
183
+
184
+ before do
185
+ fixture_count.times.map do |index|
186
+ Thread.new do
187
+ subject.save({
188
+ id: "device-#{client_id}-#{index}",
189
+ platform: 'ios',
190
+ form_factor: 'phone',
191
+ client_id: client_id,
192
+ push: {
193
+ recipient: {
194
+ transport_type: 'gcm',
195
+ registration_token: 'secret_token',
196
+ }
197
+ }
198
+ })
199
+ end
200
+ end.each(&:join) # Wait for all threads to complete
201
+ end
202
+
203
+ after do
204
+ subject.remove_where client_id: client_id, full_wait: true
205
+ end
206
+
207
+ it 'returns a PaginatedResult object containing DeviceDetails objects' do
208
+ page = subject.list
209
+ expect(page).to be_a(Ably::Models::PaginatedResult)
210
+ expect(page.items.first).to be_a(Ably::Models::DeviceDetails)
211
+ end
212
+
213
+ it 'returns an empty PaginatedResult if not params match' do
214
+ page = subject.list(client_id: 'does-not-exist')
215
+ expect(page).to be_a(Ably::Models::PaginatedResult)
216
+ expect(page.items).to be_empty
217
+ end
218
+
219
+ it 'supports paging' do
220
+ page = subject.list(limit: 3, client_id: client_id)
221
+ expect(page).to be_a(Ably::Models::PaginatedResult)
222
+
223
+ expect(page.items.count).to eql(3)
224
+ page = page.next
225
+ expect(page.items.count).to eql(3)
226
+ page = page.next
227
+ expect(page.items.count).to eql(0)
228
+ expect(page).to be_last
229
+ end
230
+
231
+ it 'provides filtering' do
232
+ page = subject.list(client_id: client_id)
233
+ expect(page.items.length).to eql(fixture_count)
234
+
235
+ page = subject.list(device_id: "device-#{client_id}-0")
236
+ expect(page.items.length).to eql(1)
237
+
238
+ page = subject.list(client_id: random_str)
239
+ expect(page.items.length).to eql(0)
240
+ end
241
+ end
242
+
243
+ describe '#get (#RSH1b1)' do
244
+ let(:fixture_count) { 2 }
245
+ let(:client_id) { random_str }
246
+
247
+ before do
248
+ fixture_count.times.map do |index|
249
+ Thread.new do
250
+ subject.save({
251
+ id: "device-#{client_id}-#{index}",
252
+ platform: 'ios',
253
+ form_factor: 'phone',
254
+ client_id: client_id,
255
+ push: {
256
+ recipient: {
257
+ transport_type: 'gcm',
258
+ registration_token: 'secret_token',
259
+ }
260
+ }
261
+ })
262
+ end
263
+ end.each(&:join) # Wait for all threads to complete
264
+ end
265
+
266
+ after do
267
+ subject.remove_where client_id: client_id, full_wait: true
268
+ end
269
+
270
+ it 'returns a DeviceDetails object if a device ID string is provided' do
271
+ device = subject.get("device-#{client_id}-0")
272
+ expect(device).to be_a(Ably::Models::DeviceDetails)
273
+ expect(device.platform).to eql('ios')
274
+ expect(device.client_id).to eql(client_id)
275
+ expect(device.push.recipient.fetch(:transport_type)).to eql('gcm')
276
+ end
277
+
278
+ it 'returns a DeviceDetails object if a DeviceDetails object is provided' do
279
+ device = subject.get(Ably::Models::DeviceDetails.new(id: "device-#{client_id}-1"))
280
+ expect(device).to be_a(Ably::Models::DeviceDetails)
281
+ expect(device.platform).to eql('ios')
282
+ expect(device.client_id).to eql(client_id)
283
+ expect(device.push.recipient.fetch(:transport_type)).to eql('gcm')
284
+ end
285
+
286
+ it 'raises a ResourceMissing exception if device ID does not exist' do
287
+ expect { subject.get("device-does-not-exist") }.to raise_error(Ably::Exceptions::ResourceMissing)
288
+ end
289
+ end
290
+
291
+ describe '#save (#RSH1b3)' do
292
+ let(:device_id) { random_str }
293
+ let(:client_id) { random_str }
294
+ let(:transport_token) { random_str }
295
+
296
+ let(:device_details) do
297
+ {
298
+ id: device_id,
299
+ platform: 'android',
300
+ form_factor: 'phone',
301
+ client_id: client_id,
302
+ metadata: {
303
+ foo: 'bar',
304
+ deep: {
305
+ val: true
306
+ }
307
+ },
308
+ push: {
309
+ recipient: {
310
+ transport_type: 'apns',
311
+ device_token: transport_token,
312
+ foo_bar: 'string',
313
+ },
314
+ error_reason: {
315
+ message: "this will be ignored"
316
+ },
317
+ }
318
+ }
319
+ end
320
+
321
+ after do
322
+ subject.remove_where client_id: client_id, full_wait: true
323
+ end
324
+
325
+ it 'saves the new DeviceDetails Hash object' do
326
+ subject.save(device_details)
327
+
328
+ device_retrieved = subject.get(device_details.fetch(:id))
329
+ expect(device_retrieved).to be_a(Ably::Models::DeviceDetails)
330
+
331
+ expect(device_retrieved.id).to eql(device_id)
332
+ expect(device_retrieved.platform).to eql('android')
333
+ expect(device_retrieved.form_factor).to eql('phone')
334
+ expect(device_retrieved.client_id).to eql(client_id)
335
+ expect(device_retrieved.metadata.keys.length).to eql(2)
336
+ expect(device_retrieved.metadata[:foo]).to eql('bar')
337
+ expect(device_retrieved.metadata['deep']['val']).to eql(true)
338
+ end
339
+
340
+ it 'saves the associated DevicePushDetails' do
341
+ subject.save(device_details)
342
+
343
+ device_retrieved = subject.list(device_id: device_details.fetch(:id)).items.first
344
+
345
+ expect(device_retrieved.push).to be_a(Ably::Models::DevicePushDetails)
346
+ expect(device_retrieved.push.recipient.fetch(:transport_type)).to eql('apns')
347
+ expect(device_retrieved.push.recipient['deviceToken']).to eql(transport_token)
348
+ expect(device_retrieved.push.recipient['foo_bar']).to eql('string')
349
+ end
350
+
351
+ context 'with GCM target' do
352
+ let(:device_token) { random_str }
353
+
354
+ it 'saves the associated DevicePushDetails' do
355
+ subject.save(device_details.merge(
356
+ push: {
357
+ recipient: {
358
+ transport_type: 'gcm',
359
+ registrationToken: device_token
360
+ }
361
+ }
362
+ ))
363
+
364
+ device_retrieved = subject.get(device_details.fetch(:id))
365
+
366
+ expect(device_retrieved.push.recipient.fetch('transportType')).to eql('gcm')
367
+ expect(device_retrieved.push.recipient[:registration_token]).to eql(device_token)
368
+ end
369
+ end
370
+
371
+ context 'with web target' do
372
+ let(:target_url) { 'http://foo.com/bar' }
373
+ let(:encryption_key) { random_str }
374
+
375
+ it 'saves the associated DevicePushDetails' do
376
+ subject.save(device_details.merge(
377
+ push: {
378
+ recipient: {
379
+ transport_type: 'web',
380
+ targetUrl: target_url,
381
+ encryptionKey: encryption_key
382
+ }
383
+ }
384
+ ))
385
+
386
+ device_retrieved = subject.get(device_details.fetch(:id))
387
+
388
+ expect(device_retrieved.push.recipient[:transport_type]).to eql('web')
389
+ expect(device_retrieved.push.recipient['targetUrl']).to eql(target_url)
390
+ expect(device_retrieved.push.recipient['encryptionKey']).to eql(encryption_key)
391
+ end
392
+ end
393
+
394
+ it 'does not allow some fields to be configured' do
395
+ subject.save(device_details)
396
+
397
+ device_retrieved = subject.get(device_details.fetch(:id))
398
+
399
+ expect(device_retrieved.push.state).to eql('ACTIVE')
400
+
401
+ expect(device_retrieved.device_secret).to be_nil
402
+
403
+ # Errors are exclusively configure by Ably
404
+ expect(device_retrieved.push.error_reason).to be_nil
405
+ end
406
+
407
+ it 'allows device_secret to be configured' do
408
+ device_secret = random_str
409
+ subject.save(device_details.merge(device_secret: device_secret))
410
+
411
+ device_retrieved = subject.get(device_details.fetch(:id))
412
+
413
+ expect(device_retrieved.device_secret).to eql(device_secret)
414
+ end
415
+
416
+ it 'saves the new DeviceDetails object' do
417
+ subject.save(DeviceDetails(device_details))
418
+
419
+ device_retrieved = subject.get(device_details.fetch(:id))
420
+ expect(device_retrieved.id).to eql(device_id)
421
+ expect(device_retrieved.metadata[:foo]).to eql('bar')
422
+ expect(device_retrieved.push.recipient[:transport_type]).to eql('apns')
423
+ end
424
+
425
+ it 'allows arbitrary number of subsequent saves' do
426
+ 3.times do
427
+ subject.save(DeviceDetails(device_details))
428
+ end
429
+
430
+ device_retrieved = subject.get(device_details.fetch(:id))
431
+ expect(device_retrieved.metadata[:foo]).to eql('bar')
432
+
433
+ subject.save(DeviceDetails(device_details.merge(metadata: { foo: 'changed'})))
434
+ device_retrieved = subject.get(device_details.fetch(:id))
435
+ expect(device_retrieved.metadata[:foo]).to eql('changed')
436
+ end
437
+
438
+ it 'fails if data is invalid' do
439
+ expect { subject.save(id: random_str, foo: 'bar') }.to raise_error Ably::Exceptions::InvalidRequest
440
+ end
441
+ end
442
+
443
+ describe '#remove_where (#RSH1b5)' do
444
+ let(:device_id) { random_str }
445
+ let(:client_id) { random_str }
446
+
447
+ before do
448
+ [
449
+ Thread.new do
450
+ subject.save({
451
+ id: "device-#{client_id}-0",
452
+ platform: 'ios',
453
+ form_factor: 'phone',
454
+ client_id: client_id,
455
+ push: {
456
+ recipient: {
457
+ transport_type: 'gcm',
458
+ registrationToken: 'secret_token',
459
+ }
460
+ }
461
+ })
462
+ end,
463
+ Thread.new do
464
+ subject.save({
465
+ id: "device-#{client_id}-1",
466
+ platform: 'ios',
467
+ form_factor: 'phone',
468
+ client_id: client_id,
469
+ push: {
470
+ recipient: {
471
+ transport_type: 'gcm',
472
+ registration_token: 'secret_token',
473
+ }
474
+ }
475
+ })
476
+ end
477
+ ].each(&:join) # Wait for all threads to complete
478
+ end
479
+
480
+ after do
481
+ subject.remove_where client_id: client_id, full_wait: true
482
+ end
483
+
484
+ it 'removes all matching device registrations by client_id' do
485
+ subject.remove_where(client_id: client_id, full_wait: true) # undocumented full_wait to compelte synchronously
486
+ expect(subject.list.items.count).to eql(0)
487
+ end
488
+
489
+ it 'removes device by device_id' do
490
+ subject.remove_where(device_id: "device-#{client_id}-1", full_wait: true) # undocumented full_wait to compelte synchronously
491
+ expect(subject.list.items.count).to eql(1)
492
+ end
493
+
494
+ it 'succeeds even if there is no match' do
495
+ subject.remove_where(device_id: 'does-not-exist', full_wait: true) # undocumented full_wait to compelte synchronously
496
+ expect(subject.list.items.count).to eql(2)
497
+ end
498
+ end
499
+
500
+ describe '#remove (#RSH1b4)' do
501
+ let(:device_id) { random_str }
502
+ let(:client_id) { random_str }
503
+
504
+ before do
505
+ [
506
+ Thread.new do
507
+ subject.save({
508
+ id: "device-#{client_id}-0",
509
+ platform: 'ios',
510
+ form_factor: 'phone',
511
+ client_id: client_id,
512
+ push: {
513
+ recipient: {
514
+ transport_type: 'gcm',
515
+ registration_token: 'secret_token',
516
+ }
517
+ }
518
+ })
519
+ end,
520
+ Thread.new do
521
+ subject.save({
522
+ id: "device-#{client_id}-1",
523
+ platform: 'ios',
524
+ form_factor: 'phone',
525
+ client_id: client_id,
526
+ push: {
527
+ recipient: {
528
+ transport_type: 'gcm',
529
+ registration_token: 'secret_token',
530
+ }
531
+ }
532
+ })
533
+ end
534
+ ].each(&:join) # Wait for all threads to complete
535
+ end
536
+
537
+ after do
538
+ subject.remove_where client_id: client_id, full_wait: true
539
+ end
540
+
541
+ it 'removes the provided device id string' do
542
+ subject.remove("device-#{client_id}-0")
543
+ expect(subject.list.items.count).to eql(1)
544
+ end
545
+
546
+ it 'removes the provided DeviceDetails' do
547
+ subject.remove(DeviceDetails(id: "device-#{client_id}-1"))
548
+ expect(subject.list.items.count).to eql(1)
549
+ end
550
+
551
+ it 'succeeds if the item does not exist' do
552
+ subject.remove('does-not-exist')
553
+ expect(subject.list.items.count).to eql(2)
554
+ end
555
+ end
556
+ end
557
+
558
+ describe '#channel_subscriptions (#RSH1c)' do
559
+ let(:client_id) { random_str }
560
+ let(:device_id) { random_str }
561
+ let(:device_id_2) { random_str }
562
+ let(:default_device_attr) {
563
+ {
564
+ platform: 'ios',
565
+ form_factor: 'phone',
566
+ client_id: client_id,
567
+ push: {
568
+ recipient: {
569
+ transport_type: 'gcm',
570
+ registration_token: 'secret_token',
571
+ }
572
+ }
573
+ }
574
+ }
575
+
576
+ let(:device_registrations) {
577
+ client.push.admin.device_registrations
578
+ }
579
+
580
+ subject {
581
+ client.push.admin.channel_subscriptions
582
+ }
583
+
584
+ # Set up 2 devices with the same client_id
585
+ # and two device with the unique device_id and no client_id
586
+ before do
587
+ [
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)) },
590
+ lambda { device_registrations.save(default_device_attr.merge(client_id: client_id, id: random_str)) },
591
+ lambda { device_registrations.save(default_device_attr.merge(client_id: client_id, id: random_str)) }
592
+ ].map do |proc|
593
+ Thread.new { proc.call }
594
+ end.each(&:join) # Wait for all threads to complete
595
+ end
596
+
597
+ after do
598
+ device_registrations.remove_where client_id: client_id
599
+ device_registrations.remove_where device_id: device_id
600
+ end
601
+
602
+ describe '#list (#RSH1c1)' do
603
+ let(:fixture_count) { 6 }
604
+
605
+ before do
606
+ fixture_count.times.map do |index|
607
+ Thread.new { subject.save(channel: "pushenabled:#{random_str}", client_id: client_id) }
608
+ end + fixture_count.times.map do |index|
609
+ Thread.new { subject.save(channel: "pushenabled:#{random_str}", device_id: device_id) }
610
+ end.each(&:join) # Wait for all threads to complete
611
+ end
612
+
613
+ it 'returns a PaginatedResult object containing DeviceDetails objects' do
614
+ page = subject.list(client_id: client_id)
615
+ expect(page).to be_a(Ably::Models::PaginatedResult)
616
+ expect(page.items.first).to be_a(Ably::Models::PushChannelSubscription)
617
+ end
618
+
619
+ it 'returns an empty PaginatedResult if params do not match' do
620
+ page = subject.list(client_id: 'does-not-exist')
621
+ expect(page).to be_a(Ably::Models::PaginatedResult)
622
+ expect(page.items).to be_empty
623
+ end
624
+
625
+ it 'supports paging' do
626
+ page = subject.list(limit: 3, device_id: device_id)
627
+ expect(page).to be_a(Ably::Models::PaginatedResult)
628
+
629
+ expect(page.items.count).to eql(3)
630
+ page = page.next
631
+ expect(page.items.count).to eql(3)
632
+ page = page.next
633
+ expect(page.items.count).to eql(0)
634
+ expect(page).to be_last
635
+ end
636
+
637
+ it 'provides filtering' do
638
+ page = subject.list(device_id: device_id)
639
+ expect(page.items.length).to eql(fixture_count)
640
+
641
+ page = subject.list(client_id: client_id)
642
+ expect(page.items.length).to eql(fixture_count)
643
+
644
+ random_channel = "pushenabled:#{random_str}"
645
+ subject.save(channel: random_channel, client_id: client_id)
646
+ page = subject.list(channel: random_channel)
647
+ expect(page.items.length).to eql(1)
648
+
649
+ page = subject.list(channel: random_channel, client_id: client_id)
650
+ expect(page.items.length).to eql(1)
651
+
652
+ page = subject.list(channel: random_channel, device_id: random_str)
653
+ expect(page.items.length).to eql(0)
654
+
655
+ page = subject.list(device_id: random_str)
656
+ expect(page.items.length).to eql(0)
657
+
658
+ page = subject.list(client_id: random_str)
659
+ expect(page.items.length).to eql(0)
660
+
661
+ page = subject.list(channel: random_str)
662
+ expect(page.items.length).to eql(0)
663
+ end
664
+
665
+ it 'raises an exception if none of the required filters are provided' do
666
+ expect { subject.list({ limit: 100 }) }.to raise_error(ArgumentError)
667
+ end
668
+ end
669
+
670
+ describe '#list_channels (#RSH1c2)' do
671
+ let(:fixture_count) { 6 }
672
+
673
+ before(:context) do
674
+ reload_test_app # TODO: Review if necessary late, currently other tests may affect list_channels
675
+ end
676
+
677
+ before do
678
+ fixture_count.times.map do |index|
679
+ Thread.new do
680
+ subject.save(channel: "pushenabled:#{index}:#{random_str}", client_id: client_id)
681
+ end
682
+ end.each(&:join) # Wait for all threads to complete
683
+ end
684
+
685
+ after do
686
+ subject.remove_where client_id: client_id, full_wait: true # undocumented arg to do deletes synchronously
687
+ end
688
+
689
+ it 'returns a PaginatedResult object containing String objects' do
690
+ page = subject.list_channels
691
+ expect(page).to be_a(Ably::Models::PaginatedResult)
692
+ expect(page.items.first).to be_a(String)
693
+ expect(page.items.length).to eql(fixture_count)
694
+ end
695
+
696
+ 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
+ subject.list_channels
701
+ page = subject.list_channels(limit: 3)
702
+ expect(page).to be_a(Ably::Models::PaginatedResult)
703
+
704
+ expect(page.items.count).to eql(3)
705
+ page = page.next
706
+ expect(page.items.count).to eql(3)
707
+ page = page.next
708
+ expect(page.items.count).to eql(0)
709
+ expect(page).to be_last
710
+ end
711
+
712
+ # This test is not necessary for client libraries, but was useful when building the Ruby
713
+ # lib to ensure the realtime implementation did not suffer from timing issues
714
+ it 'returns an accurate number of channels after devices are deleted' do
715
+ expect(subject.list_channels.items.length).to eql(fixture_count)
716
+ subject.save(channel: "pushenabled:#{random_str}", device_id: device_id)
717
+ subject.save(channel: "pushenabled:#{random_str}", device_id: device_id)
718
+ expect(subject.list_channels.items.length).to eql(fixture_count + 2)
719
+ expect(device_registrations.list(device_id: device_id).items.count).to eql(1)
720
+ device_registrations.remove_where device_id: device_id, full_wait: true # undocumented arg to do deletes synchronously
721
+ expect(device_registrations.list(device_id: device_id).items.count).to eql(0)
722
+ expect(subject.list_channels.items.length).to eql(fixture_count)
723
+ subject.remove_where client_id: client_id, full_wait: true # undocumented arg to do deletes synchronously
724
+ expect(subject.list_channels.items.length).to eql(0)
725
+ end
726
+ end
727
+
728
+ describe '#save (#RSH1c3)' do
729
+ let(:channel) { "pushenabled:#{random_str}" }
730
+ let(:client_id) { random_str }
731
+ let(:device_id) { random_str }
732
+
733
+ it 'saves the new client_id PushChannelSubscription Hash object' do
734
+ subject.save(channel: channel, client_id: client_id)
735
+
736
+ channel_sub = subject.list(client_id: client_id).items.first
737
+ expect(channel_sub).to be_a(Ably::Models::PushChannelSubscription)
738
+
739
+ expect(channel_sub.channel).to eql(channel)
740
+ expect(channel_sub.client_id).to eql(client_id)
741
+ expect(channel_sub.device_id).to be_nil
742
+ end
743
+
744
+ it 'saves the new device_id PushChannelSubscription Hash object' do
745
+ subject.save(channel: channel, device_id: device_id)
746
+
747
+ channel_sub = subject.list(device_id: device_id).items.first
748
+ expect(channel_sub).to be_a(Ably::Models::PushChannelSubscription)
749
+
750
+ expect(channel_sub.channel).to eql(channel)
751
+ expect(channel_sub.device_id).to eql(device_id)
752
+ expect(channel_sub.client_id).to be_nil
753
+ end
754
+
755
+ it 'saves the client_id PushChannelSubscription object' do
756
+ subject.save(PushChannelSubscription(channel: channel, client_id: client_id))
757
+
758
+ channel_sub = subject.list(client_id: client_id).items.first
759
+ expect(channel_sub).to be_a(Ably::Models::PushChannelSubscription)
760
+
761
+ expect(channel_sub.channel).to eql(channel)
762
+ expect(channel_sub.client_id).to eql(client_id)
763
+ expect(channel_sub.device_id).to be_nil
764
+ end
765
+
766
+ it 'saves the device_id PushChannelSubscription object' do
767
+ subject.save(PushChannelSubscription(channel: channel, device_id: device_id))
768
+
769
+ channel_sub = subject.list(device_id: device_id).items.first
770
+ expect(channel_sub).to be_a(Ably::Models::PushChannelSubscription)
771
+
772
+ expect(channel_sub.channel).to eql(channel)
773
+ expect(channel_sub.device_id).to eql(device_id)
774
+ expect(channel_sub.client_id).to be_nil
775
+ end
776
+
777
+ it 'allows arbitrary number of subsequent saves' do
778
+ 10.times do
779
+ subject.save(PushChannelSubscription(channel: channel, device_id: device_id))
780
+ end
781
+
782
+ channel_subs = subject.list(device_id: device_id).items
783
+ expect(channel_subs.length).to eql(1)
784
+ expect(channel_subs.first).to be_a(Ably::Models::PushChannelSubscription)
785
+ expect(channel_subs.first.channel).to eql(channel)
786
+ expect(channel_subs.first.device_id).to eql(device_id)
787
+ expect(channel_subs.first.client_id).to be_nil
788
+ end
789
+
790
+ it 'fails if data is invalid' do
791
+ expect { subject.save(channel: '', client_id: '') }.to raise_error ArgumentError
792
+ expect { subject.save({}) }.to raise_error ArgumentError
793
+ expect { subject.save(channel: 'not-enabled-channel', device_id: 'foo') }.to raise_error Ably::Exceptions::UnauthorizedRequest
794
+ expect { subject.save(channel: 'pushenabled:foo', device_id: 'not-registered-so-will-fail') }.to raise_error Ably::Exceptions::InvalidRequest
795
+ end
796
+ end
797
+
798
+ describe '#remove_where (#RSH1c5)' do
799
+ let(:client_id) { random_str }
800
+ let(:device_id) { random_str }
801
+ let(:fixed_channel) { "pushenabled:#{random_str}" }
802
+
803
+ let(:fixture_count) { 6 }
804
+
805
+ before do
806
+ fixture_count.times.map do |index|
807
+ [
808
+ lambda { subject.save(channel: "pushenabled:#{random_str}", client_id: client_id) },
809
+ lambda { subject.save(channel: "pushenabled:#{random_str}", device_id: device_id) },
810
+ lambda { subject.save(channel: fixed_channel, device_id: device_id_2) }
811
+ ]
812
+ end.flatten.map do |proc|
813
+ Thread.new { proc.call }
814
+ end.each(&:join) # Wait for all threads to complete
815
+ end
816
+
817
+ it 'removes matching channels' do
818
+ skip 'Delete by channel is not yet supported'
819
+ subject.remove_where channel: fixed_channel, full_wait: true
820
+ expect(subject.list(channel: fixed_channel).items.count).to eql(0)
821
+ expect(subject.list(client_id: client_id).items.count).to eql(0)
822
+ expect(subject.list(device_id: device_id).items.count).to eql(0)
823
+ end
824
+
825
+ it 'removes matching client_ids' do
826
+ subject.remove_where client_id: client_id, full_wait: true
827
+ expect(subject.list(client_id: client_id).items.count).to eql(0)
828
+ expect(subject.list(device_id: device_id).items.count).to eql(fixture_count)
829
+ end
830
+
831
+ it 'removes matching device_ids' do
832
+ subject.remove_where device_id: device_id, full_wait: true
833
+ expect(subject.list(device_id: device_id).items.count).to eql(0)
834
+ expect(subject.list(client_id: client_id).items.count).to eql(fixture_count)
835
+ end
836
+
837
+ it 'device_id and client_id filters in the same request are not suppoorted' do
838
+ expect { subject.remove_where(device_id: device_id, client_id: client_id) }.to raise_error(Ably::Exceptions::InvalidRequest)
839
+ end
840
+
841
+ it 'succeeds on no match' do
842
+ subject.remove_where device_id: random_str, full_wait: true
843
+ expect(subject.list(device_id: device_id).items.count).to eql(fixture_count)
844
+ subject.remove_where client_id: random_str
845
+ expect(subject.list(client_id: client_id).items.count).to eql(fixture_count)
846
+ end
847
+ end
848
+
849
+ describe '#remove (#RSH1c4)' do
850
+ let(:channel) { "pushenabled:#{random_str}" }
851
+ let(:channel2) { "pushenabled:#{random_str}" }
852
+ let(:client_id) { random_str }
853
+ let(:device_id) { random_str }
854
+
855
+ before do
856
+ [
857
+ lambda { subject.save(channel: channel, client_id: client_id) },
858
+ lambda { subject.save(channel: channel, device_id: device_id) },
859
+ lambda { subject.save(channel: channel2, client_id: client_id) }
860
+ ].map do |proc|
861
+ Thread.new { proc.call }
862
+ end.each(&:join) # Wait for all threads to complete
863
+ end
864
+
865
+ it 'removes match for Hash object by channel and client_id' do
866
+ subject.remove(channel: channel, client_id: client_id)
867
+ expect(subject.list(client_id: client_id).items.count).to eql(1)
868
+ end
869
+
870
+ it 'removes match for PushChannelSubscription object by channel and client_id' do
871
+ push_sub = subject.list(channel: channel, client_id: client_id).items.first
872
+ expect(push_sub).to be_a(Ably::Models::PushChannelSubscription)
873
+ subject.remove(push_sub)
874
+ expect(subject.list(client_id: client_id).items.count).to eql(1)
875
+ end
876
+
877
+ it 'removes match for Hash object by channel and device_id' do
878
+ subject.remove(channel: channel, device_id: device_id)
879
+ expect(subject.list(device_id: device_id).items.count).to eql(0)
880
+ end
881
+
882
+ it 'removes match for PushChannelSubscription object by channel and client_id' do
883
+ push_sub = subject.list(channel: channel, device_id: device_id).items.first
884
+ expect(push_sub).to be_a(Ably::Models::PushChannelSubscription)
885
+ subject.remove(push_sub)
886
+ expect(subject.list(device_id: device_id).items.count).to eql(0)
887
+ end
888
+
889
+ it 'succeeds even if there is no match' do
890
+ subject.remove(device_id: 'does-not-exist', channel: random_str)
891
+ expect(subject.list(device_id: 'does-not-exist').items.count).to eql(0)
892
+ end
893
+ end
894
+ end
895
+ end
896
+ end