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
@@ -925,7 +925,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
|
|
925
925
|
last_message = nil
|
926
926
|
channel = client.channels.get("foo")
|
927
927
|
|
928
|
-
|
928
|
+
channel.attach do
|
929
929
|
connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
|
930
930
|
if protocol_message.action == :message
|
931
931
|
last_message = protocol_message
|
@@ -1472,7 +1472,7 @@ describe Ably::Realtime::Connection, :event_machine do
|
|
1472
1472
|
channel.attach do
|
1473
1473
|
channel.once(:suspended) do
|
1474
1474
|
channel.publish('test').errback do |error|
|
1475
|
-
expect(error).to be_a(Ably::Exceptions::
|
1475
|
+
expect(error).to be_a(Ably::Exceptions::ChannelInactive)
|
1476
1476
|
stop_reactor
|
1477
1477
|
end
|
1478
1478
|
end
|
@@ -1732,7 +1732,7 @@ describe Ably::Realtime::Connection, :event_machine do
|
|
1732
1732
|
it 'sends the protocol version param v (#G4, #RTN2f)' do
|
1733
1733
|
expect(EventMachine).to receive(:connect) do |host, port, transport, object, url|
|
1734
1734
|
uri = URI.parse(url)
|
1735
|
-
expect(CGI::parse(uri.query)['v'][0]).to eql('1.
|
1735
|
+
expect(CGI::parse(uri.query)['v'][0]).to eql('1.1')
|
1736
1736
|
stop_reactor
|
1737
1737
|
end
|
1738
1738
|
client
|
@@ -1741,7 +1741,7 @@ describe Ably::Realtime::Connection, :event_machine do
|
|
1741
1741
|
it 'sends the lib version param lib (#RTN2g)' do
|
1742
1742
|
expect(EventMachine).to receive(:connect) do |host, port, transport, object, url|
|
1743
1743
|
uri = URI.parse(url)
|
1744
|
-
expect(CGI::parse(uri.query)['lib'][0]).to match(/^ruby-1\.
|
1744
|
+
expect(CGI::parse(uri.query)['lib'][0]).to match(/^ruby-1\.1\.\d+(-[\w\.]+)?+$/)
|
1745
1745
|
stop_reactor
|
1746
1746
|
end
|
1747
1747
|
client
|
@@ -1761,7 +1761,7 @@ describe Ably::Realtime::Connection, :event_machine do
|
|
1761
1761
|
it 'sends the lib version param lib with the variant (#RTN2g + #RSC7b)' do
|
1762
1762
|
expect(EventMachine).to receive(:connect) do |host, port, transport, object, url|
|
1763
1763
|
uri = URI.parse(url)
|
1764
|
-
expect(CGI::parse(uri.query)['lib'][0]).to match(/^ruby-#{variant}-1\.
|
1764
|
+
expect(CGI::parse(uri.query)['lib'][0]).to match(/^ruby-#{variant}-1\.1\.\d+(-[\w\.]+)?$/)
|
1765
1765
|
stop_reactor
|
1766
1766
|
end
|
1767
1767
|
client
|
@@ -116,7 +116,7 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
|
|
116
116
|
context 'Integer' do
|
117
117
|
let(:data) { 1 }
|
118
118
|
|
119
|
-
it 'is raises an UnsupportedDataType
|
119
|
+
it 'is raises an UnsupportedDataType 40013 exception' do
|
120
120
|
expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
|
121
121
|
stop_reactor
|
122
122
|
end
|
@@ -125,7 +125,7 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
|
|
125
125
|
context 'Float' do
|
126
126
|
let(:data) { 1.1 }
|
127
127
|
|
128
|
-
it 'is raises an UnsupportedDataType
|
128
|
+
it 'is raises an UnsupportedDataType 40013 exception' do
|
129
129
|
expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
|
130
130
|
stop_reactor
|
131
131
|
end
|
@@ -134,7 +134,7 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
|
|
134
134
|
context 'Boolean' do
|
135
135
|
let(:data) { true }
|
136
136
|
|
137
|
-
it 'is raises an UnsupportedDataType
|
137
|
+
it 'is raises an UnsupportedDataType 40013 exception' do
|
138
138
|
expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
|
139
139
|
stop_reactor
|
140
140
|
end
|
@@ -143,7 +143,7 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
|
|
143
143
|
context 'False' do
|
144
144
|
let(:data) { false }
|
145
145
|
|
146
|
-
it 'is raises an UnsupportedDataType
|
146
|
+
it 'is raises an UnsupportedDataType 40013 exception' do
|
147
147
|
expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
|
148
148
|
stop_reactor
|
149
149
|
end
|
@@ -413,30 +413,32 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
|
|
413
413
|
let(:encrypted_channel) { client.channel(channel_name, cipher: cipher_options) }
|
414
414
|
|
415
415
|
it 'encrypts message automatically before they are pushed to the server (#RTL7d)' do
|
416
|
-
encrypted_channel.
|
416
|
+
encrypted_channel.attach do
|
417
|
+
encrypted_channel.__incoming_msgbus__.unsubscribe # remove all subscribe callbacks that could decrypt the message
|
417
418
|
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
419
|
+
encrypted_channel.__incoming_msgbus__.subscribe(:message) do |message|
|
420
|
+
if protocol == :json
|
421
|
+
expect(message['encoding']).to eql(encrypted_encoding)
|
422
|
+
expect(message['data']).to eql(encrypted_data)
|
423
|
+
else
|
424
|
+
# Messages received over binary protocol will not have Base64 encoded data
|
425
|
+
expect(message['encoding']).to eql(encrypted_encoding.gsub(%r{/base64$}, ''))
|
426
|
+
expect(message['data']).to eql(encrypted_data_decoded)
|
427
|
+
end
|
428
|
+
stop_reactor
|
426
429
|
end
|
427
|
-
stop_reactor
|
428
|
-
end
|
429
430
|
|
430
|
-
|
431
|
+
encrypted_channel.publish 'example', encoded_data_decoded
|
432
|
+
end
|
431
433
|
end
|
432
434
|
|
433
435
|
it 'sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)' do
|
434
|
-
encrypted_channel.publish 'example', encoded_data_decoded
|
435
436
|
encrypted_channel.subscribe do |message|
|
436
437
|
expect(message.data).to eql(encoded_data_decoded)
|
437
438
|
expect(message.encoding).to be_nil
|
438
439
|
stop_reactor
|
439
440
|
end
|
441
|
+
encrypted_channel.publish 'example', encoded_data_decoded
|
440
442
|
end
|
441
443
|
end
|
442
444
|
end
|
@@ -256,7 +256,7 @@ describe Ably::Realtime::Presence, :event_machine do
|
|
256
256
|
context 'Integer' do
|
257
257
|
let(:data) { 1 }
|
258
258
|
|
259
|
-
it 'raises an UnsupportedDataType
|
259
|
+
it 'raises an UnsupportedDataType 40013 exception' do
|
260
260
|
expect { presence_action(method_name, data) }.to raise_error(Ably::Exceptions::UnsupportedDataType)
|
261
261
|
stop_reactor
|
262
262
|
end
|
@@ -265,7 +265,7 @@ describe Ably::Realtime::Presence, :event_machine do
|
|
265
265
|
context 'Float' do
|
266
266
|
let(:data) { 1.1 }
|
267
267
|
|
268
|
-
it 'raises an UnsupportedDataType
|
268
|
+
it 'raises an UnsupportedDataType 40013 exception' do
|
269
269
|
expect { presence_action(method_name, data) }.to raise_error(Ably::Exceptions::UnsupportedDataType)
|
270
270
|
stop_reactor
|
271
271
|
end
|
@@ -274,7 +274,7 @@ describe Ably::Realtime::Presence, :event_machine do
|
|
274
274
|
context 'Boolean' do
|
275
275
|
let(:data) { true }
|
276
276
|
|
277
|
-
it 'raises an UnsupportedDataType
|
277
|
+
it 'raises an UnsupportedDataType 40013 exception' do
|
278
278
|
expect { presence_action(method_name, data) }.to raise_error(Ably::Exceptions::UnsupportedDataType)
|
279
279
|
stop_reactor
|
280
280
|
end
|
@@ -283,7 +283,7 @@ describe Ably::Realtime::Presence, :event_machine do
|
|
283
283
|
context 'False' do
|
284
284
|
let(:data) { false }
|
285
285
|
|
286
|
-
it 'raises an UnsupportedDataType
|
286
|
+
it 'raises an UnsupportedDataType 40013 exception' do
|
287
287
|
expect { presence_action(method_name, data) }.to raise_error(Ably::Exceptions::UnsupportedDataType)
|
288
288
|
stop_reactor
|
289
289
|
end
|
@@ -2299,7 +2299,7 @@ describe Ably::Realtime::Presence, :event_machine do
|
|
2299
2299
|
presence_events << [presence_message.client_id, presence_message.action.to_sym]
|
2300
2300
|
if presence_message.action == :leave
|
2301
2301
|
expect(presence_message.id).to be_nil
|
2302
|
-
expect(presence_message.timestamp.to_f * 1000).to be_within(
|
2302
|
+
expect(presence_message.timestamp.to_f * 1000).to be_within(200).of(Time.now.to_f * 1000)
|
2303
2303
|
end
|
2304
2304
|
end
|
2305
2305
|
|
@@ -0,0 +1,696 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
# These tests are a subset of Ably::Rest::Push::Admin in async EM style
|
5
|
+
# The more robust complete test suite is in rest/push_admin_spec.rb
|
6
|
+
describe Ably::Realtime::Push::Admin, :event_machine do
|
7
|
+
include Ably::Modules::Conversions
|
8
|
+
|
9
|
+
vary_by_protocol do
|
10
|
+
let(:default_options) { { key: api_key, environment: environment, protocol: protocol} }
|
11
|
+
let(:client_options) { default_options }
|
12
|
+
let(:client) do
|
13
|
+
Ably::Realtime::Client.new(client_options)
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:basic_notification_payload) do
|
17
|
+
{
|
18
|
+
notification: {
|
19
|
+
title: 'Test message',
|
20
|
+
body: 'Test message body'
|
21
|
+
}
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
let(:basic_recipient) do
|
26
|
+
{
|
27
|
+
transport_type: 'apns',
|
28
|
+
deviceToken: 'foo.bar'
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#publish' do
|
33
|
+
subject { client.push.admin }
|
34
|
+
|
35
|
+
it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
|
36
|
+
publish_deferrable = subject.publish(basic_recipient, basic_notification_payload)
|
37
|
+
expect(publish_deferrable).to be_a(Ably::Util::SafeDeferrable)
|
38
|
+
publish_deferrable.callback do
|
39
|
+
stop_reactor
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'invalid arguments' do
|
44
|
+
it 'raises an exception with a nil recipient' do
|
45
|
+
expect { subject.publish(nil, {}) }.to raise_error ArgumentError, /Expecting a Hash/
|
46
|
+
stop_reactor
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'raises an exception with a empty recipient' do
|
50
|
+
expect { subject.publish({}, {}) }.to raise_error ArgumentError, /empty/
|
51
|
+
stop_reactor
|
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
|
+
stop_reactor
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'raises an exception with a empty recipient' do
|
60
|
+
expect { subject.publish(basic_recipient, {}) }.to raise_error ArgumentError, /empty/
|
61
|
+
stop_reactor
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'invalid recipient' do
|
66
|
+
it 'raises an error after receiving a 40x realtime response' do
|
67
|
+
skip 'validation on raw push is not enabled in realtime'
|
68
|
+
subject.publish({ invalid_recipient_details: 'foo.bar' }, basic_notification_payload).errback do |error|
|
69
|
+
expect(error.message).to match(/Invalid recipient/)
|
70
|
+
stop_reactor
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'invalid push data' do
|
76
|
+
it 'raises an error after receiving a 40x realtime response' do
|
77
|
+
skip 'validation on raw push is not enabled in realtime'
|
78
|
+
subject.publish(basic_recipient, { invalid_property_only: true }).errback do |error|
|
79
|
+
expect(error.message).to match(/Invalid push notification data/)
|
80
|
+
stop_reactor
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'recipient variable case', webmock: true do
|
86
|
+
let(:recipient_payload) do
|
87
|
+
{
|
88
|
+
camel_case: {
|
89
|
+
second_level_camel_case: 'val'
|
90
|
+
}
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
let(:content_type) do
|
95
|
+
if protocol == :msgpack
|
96
|
+
'application/x-msgpack'
|
97
|
+
else
|
98
|
+
'application/json'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def request_body(request, protocol)
|
103
|
+
if protocol == :msgpack
|
104
|
+
MessagePack.unpack(request.body)
|
105
|
+
else
|
106
|
+
JSON.parse(request.body)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def serialize(object, protocol)
|
111
|
+
if protocol == :msgpack
|
112
|
+
MessagePack.pack(object)
|
113
|
+
else
|
114
|
+
JSON.dump(object)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
let!(:publish_stub) do
|
119
|
+
stub_request(:post, "#{client.rest_client.endpoint}/push/publish").
|
120
|
+
with do |request|
|
121
|
+
expect(request_body(request, protocol)['recipient']['camelCase']['secondLevelCamelCase']).to eql('val')
|
122
|
+
expect(request_body(request, protocol)['recipient']).to_not have_key('camel_case')
|
123
|
+
true
|
124
|
+
end.to_return(
|
125
|
+
:status => 201,
|
126
|
+
:body => serialize({}, protocol),
|
127
|
+
:headers => { 'Content-Type' => content_type }
|
128
|
+
)
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'is converted to snakeCase' do
|
132
|
+
subject.publish(recipient_payload, basic_notification_payload) do
|
133
|
+
expect(publish_stub).to have_been_requested
|
134
|
+
stop_reactor
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'accepts valid push data and recipient' do
|
140
|
+
subject.publish(basic_recipient, basic_notification_payload) do
|
141
|
+
stop_reactor
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
context 'using test environment channel recipient (#RSH1a)' do
|
146
|
+
let(:channel) { random_str }
|
147
|
+
let(:recipient) do
|
148
|
+
{
|
149
|
+
'transportType' => 'ablyChannel',
|
150
|
+
'channel' => channel,
|
151
|
+
'ablyKey' => api_key,
|
152
|
+
'ablyUrl' => client.rest_client.endpoint.to_s
|
153
|
+
}
|
154
|
+
end
|
155
|
+
let(:notification_payload) do
|
156
|
+
{
|
157
|
+
notification: {
|
158
|
+
title: random_str,
|
159
|
+
},
|
160
|
+
data: {
|
161
|
+
foo: random_str
|
162
|
+
}
|
163
|
+
}
|
164
|
+
end
|
165
|
+
let(:push_channel) do
|
166
|
+
client.channels.get(channel)
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'triggers a push notification' do
|
170
|
+
push_channel.attach do
|
171
|
+
push_channel.subscribe do |message|
|
172
|
+
expect(message.name).to eql('__ably_push__')
|
173
|
+
expect(JSON.parse(message.data)['data']).to eql(JSON.parse(notification_payload[:data].to_json))
|
174
|
+
stop_reactor
|
175
|
+
end
|
176
|
+
subject.publish recipient, notification_payload
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
describe '#device_registrations' do
|
183
|
+
subject { client.push.admin.device_registrations }
|
184
|
+
let(:rest_device_registrations) {
|
185
|
+
client.rest_client.push.admin.device_registrations
|
186
|
+
}
|
187
|
+
|
188
|
+
context 'without permissions' do
|
189
|
+
let(:client_options) do
|
190
|
+
default_options.merge(
|
191
|
+
use_token_auth: true,
|
192
|
+
default_token_params: { capability: { :foo => ['subscribe'] } },
|
193
|
+
log_level: :fatal,
|
194
|
+
)
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'raises a permissions not authorized exception' do
|
198
|
+
subject.get('does-not-exist').errback do |err|
|
199
|
+
expect(err).to be_a(Ably::Exceptions::UnauthorizedRequest)
|
200
|
+
subject.list.errback do |err|
|
201
|
+
expect(err).to be_a(Ably::Exceptions::UnauthorizedRequest)
|
202
|
+
subject.remove('does-not-exist').errback do |err|
|
203
|
+
expect(err).to be_a(Ably::Exceptions::UnauthorizedRequest)
|
204
|
+
subject.remove_where(device_id: 'does-not-exist').errback do |err|
|
205
|
+
expect(err).to be_a(Ably::Exceptions::UnauthorizedRequest)
|
206
|
+
stop_reactor
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
describe '#list' do
|
215
|
+
let(:client_id) { random_str }
|
216
|
+
let(:fixture_count) { 6 }
|
217
|
+
|
218
|
+
before do
|
219
|
+
fixture_count.times.map do |index|
|
220
|
+
Thread.new do # Parallelise the setup
|
221
|
+
rest_device_registrations.save({
|
222
|
+
id: "device-#{client_id}-#{index}",
|
223
|
+
platform: 'ios',
|
224
|
+
form_factor: 'phone',
|
225
|
+
client_id: client_id,
|
226
|
+
push: {
|
227
|
+
recipient: {
|
228
|
+
transport_type: 'gcm',
|
229
|
+
registration_token: 'secret_token',
|
230
|
+
}
|
231
|
+
}
|
232
|
+
})
|
233
|
+
end
|
234
|
+
end.each(&:join) # Wait for all threads to complete
|
235
|
+
end
|
236
|
+
|
237
|
+
after do
|
238
|
+
rest_device_registrations.remove_where client_id: client_id
|
239
|
+
end
|
240
|
+
|
241
|
+
it 'returns a PaginatedResult object containing DeviceDetails objects' do
|
242
|
+
subject.list do |page|
|
243
|
+
expect(page).to be_a(Ably::Models::PaginatedResult)
|
244
|
+
expect(page.items.first).to be_a(Ably::Models::DeviceDetails)
|
245
|
+
stop_reactor
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
it 'supports paging' do
|
250
|
+
subject.list(limit: 3, client_id: client_id) do |page|
|
251
|
+
expect(page).to be_a(Ably::Models::PaginatedResult)
|
252
|
+
|
253
|
+
expect(page.items.count).to eql(3)
|
254
|
+
page.next do |page|
|
255
|
+
expect(page.items.count).to eql(3)
|
256
|
+
page.next do |page|
|
257
|
+
expect(page.items.count).to eql(0)
|
258
|
+
expect(page).to be_last
|
259
|
+
stop_reactor
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
it 'raises an exception if params are invalid' do
|
266
|
+
expect { subject.list("invalid_arg") }.to raise_error(ArgumentError)
|
267
|
+
stop_reactor
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
describe '#get' do
|
272
|
+
let(:fixture_count) { 2 }
|
273
|
+
let(:client_id) { random_str }
|
274
|
+
|
275
|
+
before do
|
276
|
+
fixture_count.times.map do |index|
|
277
|
+
Thread.new do # Parallelise the setup
|
278
|
+
rest_device_registrations.save({
|
279
|
+
id: "device-#{client_id}-#{index}",
|
280
|
+
platform: 'ios',
|
281
|
+
form_factor: 'phone',
|
282
|
+
client_id: client_id,
|
283
|
+
push: {
|
284
|
+
recipient: {
|
285
|
+
transport_type: 'gcm',
|
286
|
+
registration_token: 'secret_token',
|
287
|
+
}
|
288
|
+
}
|
289
|
+
})
|
290
|
+
end
|
291
|
+
end.each(&:join) # Wait for all threads to complete
|
292
|
+
end
|
293
|
+
|
294
|
+
after do
|
295
|
+
rest_device_registrations.remove_where client_id: client_id
|
296
|
+
end
|
297
|
+
|
298
|
+
it 'returns a DeviceDetails object if a device ID string is provided' do
|
299
|
+
subject.get("device-#{client_id}-0").callback do |device|
|
300
|
+
expect(device).to be_a(Ably::Models::DeviceDetails)
|
301
|
+
expect(device.platform).to eql('ios')
|
302
|
+
expect(device.client_id).to eql(client_id)
|
303
|
+
expect(device.push.recipient.fetch(:transport_type)).to eql('gcm')
|
304
|
+
stop_reactor
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
context 'with a failed request' do
|
309
|
+
let(:client_options) do
|
310
|
+
default_options.merge(
|
311
|
+
log_level: :fatal,
|
312
|
+
)
|
313
|
+
end
|
314
|
+
|
315
|
+
it 'raises a ResourceMissing exception if device ID does not exist' do
|
316
|
+
subject.get("device-does-not-exist").errback do |err|
|
317
|
+
expect(err).to be_a(Ably::Exceptions::ResourceMissing)
|
318
|
+
stop_reactor
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
describe '#save' do
|
325
|
+
let(:device_id) { random_str }
|
326
|
+
let(:client_id) { random_str }
|
327
|
+
let(:transport_token) { random_str }
|
328
|
+
|
329
|
+
let(:device_details) do
|
330
|
+
{
|
331
|
+
id: device_id,
|
332
|
+
platform: 'android',
|
333
|
+
form_factor: 'phone',
|
334
|
+
client_id: client_id,
|
335
|
+
metadata: {
|
336
|
+
foo: 'bar',
|
337
|
+
deep: {
|
338
|
+
val: true
|
339
|
+
}
|
340
|
+
},
|
341
|
+
push: {
|
342
|
+
recipient: {
|
343
|
+
transport_type: 'apns',
|
344
|
+
device_token: transport_token,
|
345
|
+
foo_bar: 'string',
|
346
|
+
},
|
347
|
+
error_reason: {
|
348
|
+
message: "this will be ignored"
|
349
|
+
},
|
350
|
+
}
|
351
|
+
}
|
352
|
+
end
|
353
|
+
|
354
|
+
after do
|
355
|
+
rest_device_registrations.remove_where client_id: client_id
|
356
|
+
end
|
357
|
+
|
358
|
+
it 'saves the new DeviceDetails Hash object' do
|
359
|
+
subject.save(device_details) do
|
360
|
+
subject.get(device_details.fetch(:id)) do |device_retrieved|
|
361
|
+
expect(device_retrieved).to be_a(Ably::Models::DeviceDetails)
|
362
|
+
expect(device_retrieved.id).to eql(device_id)
|
363
|
+
expect(device_retrieved.platform).to eql('android')
|
364
|
+
stop_reactor
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
context 'with a failed request' do
|
370
|
+
let(:client_options) do
|
371
|
+
default_options.merge(
|
372
|
+
log_level: :fatal,
|
373
|
+
)
|
374
|
+
end
|
375
|
+
|
376
|
+
it 'fails if data is invalid' do
|
377
|
+
subject.save(id: random_str, foo: 'bar').errback do |err|
|
378
|
+
expect(err).to be_a(Ably::Exceptions::InvalidRequest)
|
379
|
+
stop_reactor
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
describe '#remove_where' do
|
386
|
+
let(:device_id) { random_str }
|
387
|
+
let(:client_id) { random_str }
|
388
|
+
|
389
|
+
before do
|
390
|
+
rest_device_registrations.save({
|
391
|
+
id: "device-#{client_id}-0",
|
392
|
+
platform: 'ios',
|
393
|
+
form_factor: 'phone',
|
394
|
+
client_id: client_id,
|
395
|
+
push: {
|
396
|
+
recipient: {
|
397
|
+
transport_type: 'gcm',
|
398
|
+
registrationToken: 'secret_token',
|
399
|
+
}
|
400
|
+
}
|
401
|
+
})
|
402
|
+
end
|
403
|
+
|
404
|
+
after do
|
405
|
+
rest_device_registrations.remove_where client_id: client_id
|
406
|
+
end
|
407
|
+
|
408
|
+
it 'removes all matching device registrations by client_id' do
|
409
|
+
subject.remove_where(client_id: client_id, full_wait: true) do
|
410
|
+
subject.list do |page|
|
411
|
+
expect(page.items.count).to eql(0)
|
412
|
+
stop_reactor
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
describe '#remove' do
|
419
|
+
let(:device_id) { random_str }
|
420
|
+
let(:client_id) { random_str }
|
421
|
+
|
422
|
+
before do
|
423
|
+
rest_device_registrations.save({
|
424
|
+
id: "device-#{client_id}-0",
|
425
|
+
platform: 'ios',
|
426
|
+
form_factor: 'phone',
|
427
|
+
client_id: client_id,
|
428
|
+
push: {
|
429
|
+
recipient: {
|
430
|
+
transport_type: 'gcm',
|
431
|
+
registration_token: 'secret_token',
|
432
|
+
}
|
433
|
+
}
|
434
|
+
})
|
435
|
+
end
|
436
|
+
|
437
|
+
after do
|
438
|
+
rest_device_registrations.remove_where client_id: client_id
|
439
|
+
end
|
440
|
+
|
441
|
+
it 'removes the provided device id string' do
|
442
|
+
subject.remove("device-#{client_id}-0") do
|
443
|
+
subject.list do |page|
|
444
|
+
expect(page.items.count).to eql(0)
|
445
|
+
stop_reactor
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
describe '#channel_subscriptions' do
|
453
|
+
let(:client_id) { random_str }
|
454
|
+
let(:device_id) { random_str }
|
455
|
+
let(:device_id_2) { random_str }
|
456
|
+
let(:default_device_attr) {
|
457
|
+
{
|
458
|
+
platform: 'ios',
|
459
|
+
form_factor: 'phone',
|
460
|
+
client_id: client_id,
|
461
|
+
push: {
|
462
|
+
recipient: {
|
463
|
+
transport_type: 'gcm',
|
464
|
+
registration_token: 'secret_token',
|
465
|
+
}
|
466
|
+
}
|
467
|
+
}
|
468
|
+
}
|
469
|
+
|
470
|
+
let(:rest_device_registrations) {
|
471
|
+
client.rest_client.push.admin.device_registrations
|
472
|
+
}
|
473
|
+
|
474
|
+
let(:rest_channel_subscriptions) {
|
475
|
+
client.rest_client.push.admin.channel_subscriptions
|
476
|
+
}
|
477
|
+
|
478
|
+
subject {
|
479
|
+
client.push.admin.channel_subscriptions
|
480
|
+
}
|
481
|
+
|
482
|
+
# Set up 2 devices with the same client_id
|
483
|
+
# and two device with the unique device_id and no client_id
|
484
|
+
before do
|
485
|
+
[
|
486
|
+
lambda { rest_device_registrations.save(default_device_attr.merge(id: device_id)) },
|
487
|
+
lambda { rest_device_registrations.save(default_device_attr.merge(id: device_id_2)) },
|
488
|
+
lambda { rest_device_registrations.save(default_device_attr.merge(client_id: client_id, id: random_str)) },
|
489
|
+
lambda { rest_device_registrations.save(default_device_attr.merge(client_id: client_id, id: random_str)) }
|
490
|
+
].map do |proc|
|
491
|
+
Thread.new { proc.call }
|
492
|
+
end.each(&:join) # Wait for all threads to complete
|
493
|
+
end
|
494
|
+
|
495
|
+
after do
|
496
|
+
rest_device_registrations.remove_where client_id: client_id
|
497
|
+
rest_device_registrations.remove_where device_id: device_id
|
498
|
+
end
|
499
|
+
|
500
|
+
describe '#list' do
|
501
|
+
let(:fixture_count) { 6 }
|
502
|
+
|
503
|
+
before do
|
504
|
+
fixture_count.times.map do |index|
|
505
|
+
Thread.new { rest_channel_subscriptions.save(channel: "pushenabled:#{random_str}", client_id: client_id) }
|
506
|
+
end + fixture_count.times.map do |index|
|
507
|
+
Thread.new { rest_channel_subscriptions.save(channel: "pushenabled:#{random_str}", device_id: device_id) }
|
508
|
+
end.each(&:join) # Wait for all threads to complete
|
509
|
+
end
|
510
|
+
|
511
|
+
it 'returns a PaginatedResult object containing DeviceDetails objects' do
|
512
|
+
subject.list(client_id: client_id) do |page|
|
513
|
+
expect(page).to be_a(Ably::Models::PaginatedResult)
|
514
|
+
expect(page.items.first).to be_a(Ably::Models::PushChannelSubscription)
|
515
|
+
stop_reactor
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
it 'supports paging' do
|
520
|
+
subject.list(limit: 3, device_id: device_id) do |page|
|
521
|
+
expect(page).to be_a(Ably::Models::PaginatedResult)
|
522
|
+
|
523
|
+
expect(page.items.count).to eql(3)
|
524
|
+
page.next do |page|
|
525
|
+
expect(page.items.count).to eql(3)
|
526
|
+
page.next do |page|
|
527
|
+
expect(page.items.count).to eql(0)
|
528
|
+
expect(page).to be_last
|
529
|
+
stop_reactor
|
530
|
+
end
|
531
|
+
end
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
it 'raises an exception if none of the required filters are provided' do
|
536
|
+
expect { subject.list({ limit: 100 }) }.to raise_error(ArgumentError)
|
537
|
+
stop_reactor
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
describe '#list_channels' do
|
542
|
+
let(:fixture_count) { 6 }
|
543
|
+
|
544
|
+
before(:context) do
|
545
|
+
reload_test_app # TODO: Review if necessary later, currently other tests may affect list_channels
|
546
|
+
end
|
547
|
+
|
548
|
+
before do
|
549
|
+
fixture_count.times.map do |index|
|
550
|
+
Thread.new do
|
551
|
+
rest_channel_subscriptions.save(channel: "pushenabled:#{index}:#{random_str}", client_id: client_id)
|
552
|
+
end
|
553
|
+
end.each(&:join) # Wait for all threads to complete
|
554
|
+
end
|
555
|
+
|
556
|
+
after do
|
557
|
+
rest_channel_subscriptions.remove_where client_id: client_id, full_wait: true # undocumented arg to do deletes synchronously
|
558
|
+
end
|
559
|
+
|
560
|
+
it 'returns a PaginatedResult object containing String objects' do
|
561
|
+
subject.list_channels do |page|
|
562
|
+
expect(page).to be_a(Ably::Models::PaginatedResult)
|
563
|
+
expect(page.items.first).to be_a(String)
|
564
|
+
expect(page.items.length).to eql(fixture_count)
|
565
|
+
stop_reactor
|
566
|
+
end
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
describe '#save' do
|
571
|
+
let(:channel) { "pushenabled:#{random_str}" }
|
572
|
+
let(:client_id) { random_str }
|
573
|
+
let(:device_id) { random_str }
|
574
|
+
|
575
|
+
it 'saves the new client_id PushChannelSubscription Hash object' do
|
576
|
+
subject.save(channel: channel, client_id: client_id) do
|
577
|
+
subject.list(client_id: client_id) do |page|
|
578
|
+
channel_sub = page.items.first
|
579
|
+
expect(channel_sub).to be_a(Ably::Models::PushChannelSubscription)
|
580
|
+
expect(channel_sub.channel).to eql(channel)
|
581
|
+
stop_reactor
|
582
|
+
end
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
it 'raises an exception for invalid params' do
|
587
|
+
expect { subject.save(channel: '', client_id: '') }.to raise_error ArgumentError
|
588
|
+
expect { subject.save({}) }.to raise_error ArgumentError
|
589
|
+
stop_reactor
|
590
|
+
end
|
591
|
+
|
592
|
+
context 'failed requests' do
|
593
|
+
let(:client_options) do
|
594
|
+
default_options.merge(
|
595
|
+
log_level: :fatal,
|
596
|
+
)
|
597
|
+
end
|
598
|
+
|
599
|
+
it 'fails for invalid requests' do
|
600
|
+
subject.save(channel: 'not-enabled-channel', device_id: 'foo').errback do |err|
|
601
|
+
expect(err).to be_a(Ably::Exceptions::UnauthorizedRequest)
|
602
|
+
subject.save(channel: 'pushenabled:foo', device_id: 'not-registered-so-will-fail').errback do |err|
|
603
|
+
expect(err).to be_a(Ably::Exceptions::InvalidRequest)
|
604
|
+
stop_reactor
|
605
|
+
end
|
606
|
+
end
|
607
|
+
end
|
608
|
+
end
|
609
|
+
end
|
610
|
+
|
611
|
+
describe '#remove_where' do
|
612
|
+
let(:client_id) { random_str }
|
613
|
+
let(:device_id) { random_str }
|
614
|
+
let(:fixed_channel) { "pushenabled:#{random_str}" }
|
615
|
+
|
616
|
+
let(:fixture_count) { 6 }
|
617
|
+
|
618
|
+
before do
|
619
|
+
fixture_count.times.map do |index|
|
620
|
+
Thread.new do
|
621
|
+
rest_channel_subscriptions.save(channel: "pushenabled:#{random_str}", client_id: client_id)
|
622
|
+
end
|
623
|
+
end.each(&:join) # Wait for all threads to complete
|
624
|
+
end
|
625
|
+
|
626
|
+
it 'removes matching client_ids' do
|
627
|
+
subject.list(client_id: client_id) do |page|
|
628
|
+
expect(page.items.count).to eql(fixture_count)
|
629
|
+
subject.remove_where(client_id: client_id, full_wait: true) do
|
630
|
+
subject.list(client_id: client_id) do |page|
|
631
|
+
expect(page.items.count).to eql(0)
|
632
|
+
stop_reactor
|
633
|
+
end
|
634
|
+
end
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
context 'failed requests' do
|
639
|
+
let(:client_options) do
|
640
|
+
default_options.merge(
|
641
|
+
log_level: :fatal,
|
642
|
+
)
|
643
|
+
end
|
644
|
+
|
645
|
+
it 'device_id and client_id filters in the same request are not supported' do
|
646
|
+
subject.remove_where(device_id: device_id, client_id: client_id).errback do |err|
|
647
|
+
expect(err).to be_a(Ably::Exceptions::InvalidRequest)
|
648
|
+
stop_reactor
|
649
|
+
end
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
it 'succeeds on no match' do
|
654
|
+
subject.remove_where(device_id: random_str, full_wait: true) do
|
655
|
+
subject.list(client_id: client_id) do |page|
|
656
|
+
expect(page.items.count).to eql(fixture_count)
|
657
|
+
stop_reactor
|
658
|
+
end
|
659
|
+
end
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
describe '#remove' do
|
664
|
+
let(:channel) { "pushenabled:#{random_str}" }
|
665
|
+
let(:channel2) { "pushenabled:#{random_str}" }
|
666
|
+
let(:client_id) { random_str }
|
667
|
+
let(:device_id) { random_str }
|
668
|
+
|
669
|
+
before do
|
670
|
+
rest_channel_subscriptions.save(channel: channel, client_id: client_id)
|
671
|
+
end
|
672
|
+
|
673
|
+
it 'removes match for Hash object by channel and client_id' do
|
674
|
+
subject.list(client_id: client_id) do |page|
|
675
|
+
expect(page.items.count).to eql(1)
|
676
|
+
subject.remove(channel: channel, client_id: client_id, full_wait: true) do
|
677
|
+
subject.list(client_id: client_id) do |page|
|
678
|
+
expect(page.items.count).to eql(0)
|
679
|
+
stop_reactor
|
680
|
+
end
|
681
|
+
end
|
682
|
+
end
|
683
|
+
end
|
684
|
+
|
685
|
+
it 'succeeds even if there is no match' do
|
686
|
+
subject.remove(device_id: 'does-not-exist', channel: random_str) do
|
687
|
+
subject.list(device_id: 'does-not-exist') do |page|
|
688
|
+
expect(page.items.count).to eql(0)
|
689
|
+
stop_reactor
|
690
|
+
end
|
691
|
+
end
|
692
|
+
end
|
693
|
+
end
|
694
|
+
end
|
695
|
+
end
|
696
|
+
end
|