ably 1.0.7 → 1.1.4.rc

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