ably 1.0.7 → 1.1.4.rc

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