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.
- checksums.yaml +4 -4
- data/.editorconfig +14 -0
- data/.travis.yml +4 -4
- data/CHANGELOG.md +26 -3
- data/Rakefile +32 -0
- data/SPEC.md +920 -565
- data/ably.gemspec +9 -4
- data/lib/ably/auth.rb +28 -2
- data/lib/ably/exceptions.rb +8 -2
- data/lib/ably/models/channel_state_change.rb +1 -1
- data/lib/ably/models/connection_state_change.rb +1 -1
- data/lib/ably/models/device_details.rb +87 -0
- data/lib/ably/models/device_push_details.rb +86 -0
- data/lib/ably/models/error_info.rb +23 -2
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +4 -4
- data/lib/ably/models/protocol_message.rb +32 -2
- data/lib/ably/models/push_channel_subscription.rb +89 -0
- data/lib/ably/modules/conversions.rb +1 -1
- data/lib/ably/modules/encodeable.rb +1 -1
- data/lib/ably/modules/exception_codes.rb +128 -0
- data/lib/ably/modules/model_common.rb +15 -2
- data/lib/ably/modules/state_machine.rb +1 -1
- data/lib/ably/realtime.rb +1 -0
- data/lib/ably/realtime/auth.rb +1 -1
- data/lib/ably/realtime/channel.rb +24 -102
- data/lib/ably/realtime/channel/channel_manager.rb +2 -6
- data/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
- data/lib/ably/realtime/channel/publisher.rb +74 -0
- data/lib/ably/realtime/channel/push_channel.rb +62 -0
- data/lib/ably/realtime/client.rb +87 -0
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +6 -2
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
- data/lib/ably/realtime/connection.rb +8 -5
- data/lib/ably/realtime/connection/connection_manager.rb +7 -7
- data/lib/ably/realtime/connection/websocket_transport.rb +1 -1
- data/lib/ably/realtime/presence.rb +4 -4
- data/lib/ably/realtime/presence/members_map.rb +3 -3
- data/lib/ably/realtime/push.rb +40 -0
- data/lib/ably/realtime/push/admin.rb +61 -0
- data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
- data/lib/ably/realtime/push/device_registrations.rb +105 -0
- data/lib/ably/rest.rb +1 -0
- data/lib/ably/rest/channel.rb +33 -5
- data/lib/ably/rest/channel/push_channel.rb +62 -0
- data/lib/ably/rest/client.rb +137 -28
- data/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
- data/lib/ably/rest/presence.rb +1 -0
- data/lib/ably/rest/push.rb +42 -0
- data/lib/ably/rest/push/admin.rb +54 -0
- data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
- data/lib/ably/rest/push/device_registrations.rb +103 -0
- data/lib/ably/version.rb +7 -2
- data/spec/acceptance/realtime/auth_spec.rb +6 -8
- data/spec/acceptance/realtime/channel_spec.rb +166 -51
- data/spec/acceptance/realtime/client_spec.rb +149 -0
- data/spec/acceptance/realtime/connection_failures_spec.rb +1 -1
- data/spec/acceptance/realtime/connection_spec.rb +4 -4
- data/spec/acceptance/realtime/message_spec.rb +19 -17
- data/spec/acceptance/realtime/presence_spec.rb +5 -5
- data/spec/acceptance/realtime/push_admin_spec.rb +696 -0
- data/spec/acceptance/realtime/push_spec.rb +27 -0
- data/spec/acceptance/rest/auth_spec.rb +4 -3
- data/spec/acceptance/rest/base_spec.rb +2 -2
- data/spec/acceptance/rest/client_spec.rb +129 -10
- data/spec/acceptance/rest/message_spec.rb +175 -4
- data/spec/acceptance/rest/push_admin_spec.rb +896 -0
- data/spec/acceptance/rest/push_spec.rb +25 -0
- data/spec/acceptance/rest/time_spec.rb +1 -1
- data/spec/run_parallel_tests +33 -0
- data/spec/unit/logger_spec.rb +10 -3
- data/spec/unit/models/device_details_spec.rb +102 -0
- data/spec/unit/models/device_push_details_spec.rb +101 -0
- data/spec/unit/models/error_info_spec.rb +51 -3
- data/spec/unit/models/message_spec.rb +17 -2
- data/spec/unit/models/presence_message_spec.rb +1 -1
- data/spec/unit/models/push_channel_subscription_spec.rb +86 -0
- data/spec/unit/realtime/client_spec.rb +12 -0
- data/spec/unit/realtime/push_channel_spec.rb +36 -0
- data/spec/unit/rest/channel_spec.rb +8 -1
- data/spec/unit/rest/client_spec.rb +30 -0
- data/spec/unit/rest/push_channel_spec.rb +36 -0
- 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
|