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
@@ -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