ably-rest 1.0.5 → 1.1.3
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 +5 -5
- data/.travis.yml +6 -3
- data/CHANGELOG.md +1 -1
- data/LICENSE +1 -1
- data/README.md +26 -7
- data/SPEC.md +2003 -1605
- data/ably-rest.gemspec +4 -2
- data/lib/submodules/ably-ruby/.editorconfig +14 -0
- data/lib/submodules/ably-ruby/.travis.yml +10 -8
- data/lib/submodules/ably-ruby/CHANGELOG.md +97 -1
- data/lib/submodules/ably-ruby/LICENSE +1 -3
- data/lib/submodules/ably-ruby/README.md +12 -7
- data/lib/submodules/ably-ruby/Rakefile +32 -0
- data/lib/submodules/ably-ruby/SPEC.md +1277 -835
- data/lib/submodules/ably-ruby/ably.gemspec +17 -11
- data/lib/submodules/ably-ruby/lib/ably/auth.rb +34 -8
- data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +10 -4
- data/lib/submodules/ably-ruby/lib/ably/logger.rb +8 -2
- data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +1 -1
- data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +1 -1
- data/lib/submodules/ably-ruby/lib/ably/models/device_details.rb +87 -0
- data/lib/submodules/ably-ruby/lib/ably/models/device_push_details.rb +86 -0
- data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +23 -2
- data/lib/submodules/ably-ruby/lib/ably/models/idiomatic_ruby_wrapper.rb +12 -12
- data/lib/submodules/ably-ruby/lib/ably/models/message.rb +6 -4
- data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +6 -4
- data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +32 -2
- data/lib/submodules/ably-ruby/lib/ably/models/push_channel_subscription.rb +89 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +2 -2
- data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +2 -2
- data/lib/submodules/ably-ruby/lib/ably/modules/encodeable.rb +2 -2
- data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +2 -2
- data/lib/submodules/ably-ruby/lib/ably/modules/exception_codes.rb +128 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +15 -2
- data/lib/submodules/ably-ruby/lib/ably/modules/safe_deferrable.rb +1 -1
- data/lib/submodules/ably-ruby/lib/ably/modules/safe_yield.rb +1 -1
- data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +5 -5
- data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +2 -2
- data/lib/submodules/ably-ruby/lib/ably/realtime.rb +1 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +2 -2
- data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +27 -105
- data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +4 -8
- data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
- data/lib/submodules/ably-ruby/lib/ably/realtime/channel/publisher.rb +74 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/channel/push_channel.rb +62 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +91 -3
- data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +9 -4
- data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
- data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +45 -26
- data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +25 -9
- data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +2 -2
- data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +7 -7
- data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +9 -9
- data/lib/submodules/ably-ruby/lib/ably/realtime/push.rb +40 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/push/admin.rb +61 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/push/device_registrations.rb +105 -0
- data/lib/submodules/ably-ruby/lib/ably/rest.rb +1 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +54 -18
- data/lib/submodules/ably-ruby/lib/ably/rest/channel/push_channel.rb +62 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +171 -41
- data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
- data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/push.rb +42 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/push/admin.rb +54 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/push/channel_subscriptions.rb +121 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/push/device_registrations.rb +103 -0
- data/lib/submodules/ably-ruby/lib/ably/version.rb +7 -2
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +253 -49
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +33 -21
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +180 -62
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +155 -2
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +293 -13
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +142 -39
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +38 -36
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +12 -3
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +207 -173
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_admin_spec.rb +736 -0
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_spec.rb +27 -0
- data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +62 -51
- data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +2 -2
- data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +79 -4
- data/lib/submodules/ably-ruby/spec/acceptance/rest/channels_spec.rb +6 -0
- data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +318 -74
- data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +158 -6
- data/lib/submodules/ably-ruby/spec/acceptance/rest/push_admin_spec.rb +952 -0
- data/lib/submodules/ably-ruby/spec/acceptance/rest/push_spec.rb +25 -0
- data/lib/submodules/ably-ruby/spec/acceptance/rest/time_spec.rb +1 -1
- data/lib/submodules/ably-ruby/spec/run_parallel_tests +33 -0
- data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +1 -9
- data/lib/submodules/ably-ruby/spec/spec_helper.rb +3 -1
- data/lib/submodules/ably-ruby/spec/support/debug_failure_helper.rb +9 -5
- data/lib/submodules/ably-ruby/spec/support/event_emitter_helper.rb +31 -0
- data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +1 -1
- data/lib/submodules/ably-ruby/spec/support/test_app.rb +2 -2
- data/lib/submodules/ably-ruby/spec/support/test_logger_helper.rb +42 -0
- data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +11 -12
- data/lib/submodules/ably-ruby/spec/unit/models/device_details_spec.rb +102 -0
- data/lib/submodules/ably-ruby/spec/unit/models/device_push_details_spec.rb +101 -0
- data/lib/submodules/ably-ruby/spec/unit/models/error_info_spec.rb +51 -3
- data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +17 -2
- data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +1 -1
- data/lib/submodules/ably-ruby/spec/unit/models/push_channel_subscription_spec.rb +86 -0
- data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +2 -2
- data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +1 -1
- data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +3 -3
- data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +10 -10
- data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +1 -1
- data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +13 -1
- data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +2 -2
- data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +1 -1
- data/lib/submodules/ably-ruby/spec/unit/realtime/push_channel_spec.rb +36 -0
- data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +30 -1
- data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +30 -0
- data/lib/submodules/ably-ruby/spec/unit/rest/push_channel_spec.rb +36 -0
- data/lib/submodules/ably-ruby/spec/unit/util/pub_sub_spec.rb +3 -3
- data/spec/spec_helper.rb +1 -0
- metadata +51 -10
@@ -1,5 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require 'spec_helper'
|
3
|
+
require 'base64'
|
3
4
|
require 'securerandom'
|
4
5
|
|
5
6
|
describe Ably::Rest::Channel, 'messages' do
|
@@ -74,8 +75,7 @@ describe Ably::Rest::Channel, 'messages' do
|
|
74
75
|
end
|
75
76
|
|
76
77
|
context 'JSON Array' do
|
77
|
-
|
78
|
-
let(:data) { { 'push' => { 'data' => { 'key' => [ true, false, 55, 'string', { 'Hash' => true }, ['array'] ] } } } }
|
78
|
+
let(:data) { { 'push' => { 'data' => { 'key' => [ true, false, 55, nil, 'string', { 'Hash' => true }, ['array'] ] } } } }
|
79
79
|
|
80
80
|
it 'is encoded and decoded to the same deep multi-type object' do
|
81
81
|
channel.publish 'event', {}, extras: data
|
@@ -91,11 +91,163 @@ describe Ably::Rest::Channel, 'messages' do
|
|
91
91
|
end
|
92
92
|
end
|
93
93
|
|
94
|
+
context 'idempotency (#RSL1k)' do
|
95
|
+
let(:id) { random_str }
|
96
|
+
let(:name) { 'event' }
|
97
|
+
let(:data) { random_str }
|
98
|
+
|
99
|
+
context 'when ID is not included (#RSL1k2)' do
|
100
|
+
context 'with Message object' do
|
101
|
+
let(:message) { Ably::Models::Message.new(data: data) }
|
102
|
+
|
103
|
+
it 'publishes the same message three times' do
|
104
|
+
3.times { channel.publish [message] }
|
105
|
+
expect(channel.history.items.length).to eql(3)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context 'with #publish arguments only' do
|
110
|
+
it 'publishes the same message three times' do
|
111
|
+
3.times { channel.publish 'event', data }
|
112
|
+
expect(channel.history.items.length).to eql(3)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'when ID is included (#RSL1k2, #RSL1k5)' do
|
118
|
+
context 'with Message object' do
|
119
|
+
let(:message) { Ably::Models::Message.new(id: id, data: data) }
|
120
|
+
|
121
|
+
specify 'three REST publishes result in only one message being published' do
|
122
|
+
3.times { channel.publish [message] }
|
123
|
+
expect(channel.history.items.length).to eql(1)
|
124
|
+
expect(channel.history.items[0].id).to eql(id)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'with #publish arguments only' do
|
129
|
+
it 'three REST publishes result in only one message being published' do
|
130
|
+
3.times { channel.publish 'event', data, id: id }
|
131
|
+
expect(channel.history.items.length).to eql(1)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
specify 'the ID provided is used for the published messages' do
|
136
|
+
channel.publish 'event', data, id: id
|
137
|
+
expect(channel.history.items[0].id).to eql(id)
|
138
|
+
end
|
139
|
+
|
140
|
+
specify 'for multiple messages in one publish operation (#RSL1k3)' do
|
141
|
+
message_arr = 3.times.map { Ably::Models::Message.new(id: id, data: data) }
|
142
|
+
expect { channel.publish message_arr }.to raise_error do |error|
|
143
|
+
expect(error.code).to eql(40031) # Invalid publish request (invalid client-specified id), see https://github.com/ably/ably-common/pull/30
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
specify 'for multiple messages in one publish operation with IDs following the required format described in RSL1k1 (#RSL1k3)' do
|
148
|
+
message_arr = 3.times.map { |index| Ably::Models::Message.new(id: "#{id}:#{index}", data: data) }
|
149
|
+
channel.publish message_arr
|
150
|
+
expect(channel.history.items[2].id).to eql("#{id}:0")
|
151
|
+
expect(channel.history.items[0].id).to eql("#{id}:2")
|
152
|
+
expect(channel.history.items.length).to eql(3)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
specify 'idempotent publishing is disabled by default with 1.1 (#TO3n)' do
|
157
|
+
client = Ably::Rest::Client.new(key: api_key, protocol: protocol)
|
158
|
+
expect(client.idempotent_rest_publishing).to be_falsey
|
159
|
+
end
|
160
|
+
|
161
|
+
specify 'idempotent publishing is enabled by default with 1.2 (#TO3n)' do
|
162
|
+
stub_const 'Ably::VERSION', '1.2.0'
|
163
|
+
client = Ably::Rest::Client.new(key: api_key, protocol: protocol)
|
164
|
+
expect(client.idempotent_rest_publishing).to be_truthy
|
165
|
+
end
|
166
|
+
|
167
|
+
context 'when idempotent publishing is enabled in the client library ClientOptions (#TO3n)' do
|
168
|
+
let(:client_options) { default_client_options.merge(idempotent_rest_publishing: true, log_level: :error, fallback_hosts: ["#{environment}-realtime.ably.io"]) }
|
169
|
+
|
170
|
+
context 'when there is a network failure triggering an automatic retry (#RSL1k4)' do
|
171
|
+
def mock_for_two_publish_failures
|
172
|
+
@failed_http_posts = 0
|
173
|
+
allow(client).to receive(:can_fallback_to_alternate_ably_host?).and_return(true)
|
174
|
+
allow_any_instance_of(Faraday::Connection).to receive(:post) do |*args|
|
175
|
+
@failed_http_posts += 1
|
176
|
+
if @failed_http_posts == 2
|
177
|
+
# Ensure the 3rd requests operates as normal
|
178
|
+
allow_any_instance_of(Faraday::Connection).to receive(:post).and_call_original
|
179
|
+
end
|
180
|
+
raise Faraday::ClientError.new('Fake client error')
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
context 'with Message object' do
|
185
|
+
let(:message) { Ably::Models::Message.new(data: data) }
|
186
|
+
before { mock_for_two_publish_failures }
|
187
|
+
|
188
|
+
specify 'two REST publish retries result in only one message being published' do
|
189
|
+
channel.publish [message]
|
190
|
+
expect(channel.history.items.length).to eql(1)
|
191
|
+
expect(@failed_http_posts).to eql(2)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
context 'with #publish arguments only' do
|
196
|
+
before { mock_for_two_publish_failures }
|
197
|
+
|
198
|
+
specify 'two REST publish retries result in only one message being published' do
|
199
|
+
channel.publish 'event', data
|
200
|
+
expect(channel.history.items.length).to eql(1)
|
201
|
+
expect(@failed_http_posts).to eql(2)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
context 'with explicitly provided message ID' do
|
206
|
+
let(:id) { random_str }
|
207
|
+
|
208
|
+
before { mock_for_two_publish_failures }
|
209
|
+
|
210
|
+
specify 'two REST publish retries result in only one message being published' do
|
211
|
+
channel.publish 'event', data, id: id
|
212
|
+
expect(channel.history.items.length).to eql(1)
|
213
|
+
expect(channel.history.items[0].id).to eql(id)
|
214
|
+
expect(@failed_http_posts).to eql(2)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
specify 'for multiple messages in one publish operation' do
|
219
|
+
message_arr = 3.times.map { Ably::Models::Message.new(data: data) }
|
220
|
+
3.times { channel.publish message_arr }
|
221
|
+
expect(channel.history.items.length).to eql(message_arr.length * 3)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
specify 'the ID is populated with a random ID and serial 0 from this lib (#RSL1k1)' do
|
226
|
+
channel.publish 'event'
|
227
|
+
expect(channel.history.items[0].id).to match(/^[A-Za-z0-9\+\/]+:0$/)
|
228
|
+
base_64_id = channel.history.items[0].id.split(':')[0]
|
229
|
+
expect(Base64.decode64(base_64_id).length).to eql(9)
|
230
|
+
end
|
231
|
+
|
232
|
+
context 'when publishing a batch of messages' do
|
233
|
+
specify 'the ID is populated with a single random ID and sequence of serials from this lib (#RSL1k1)' do
|
234
|
+
message = { name: 'event' }
|
235
|
+
channel.publish [message, message, message]
|
236
|
+
expect(channel.history.items.length).to eql(3)
|
237
|
+
expect(channel.history.items[0].id).to match(/^[A-Za-z0-9\+\/]+:2$/)
|
238
|
+
expect(channel.history.items[2].id).to match(/^[A-Za-z0-9\+\/]+:0$/)
|
239
|
+
base_64_id = channel.history.items[0].id.split(':')[0]
|
240
|
+
expect(Base64.decode64(base_64_id).length).to eql(9)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
94
246
|
context 'with unsupported data payload content type' do
|
95
247
|
context 'Integer' do
|
96
248
|
let(:data) { 1 }
|
97
249
|
|
98
|
-
it 'is raises an UnsupportedDataType
|
250
|
+
it 'is raises an UnsupportedDataType 40013 exception' do
|
99
251
|
expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
|
100
252
|
end
|
101
253
|
end
|
@@ -103,7 +255,7 @@ describe Ably::Rest::Channel, 'messages' do
|
|
103
255
|
context 'Float' do
|
104
256
|
let(:data) { 1.1 }
|
105
257
|
|
106
|
-
it 'is raises an UnsupportedDataType
|
258
|
+
it 'is raises an UnsupportedDataType 40013 exception' do
|
107
259
|
expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
|
108
260
|
end
|
109
261
|
end
|
@@ -111,7 +263,7 @@ describe Ably::Rest::Channel, 'messages' do
|
|
111
263
|
context 'Boolean' do
|
112
264
|
let(:data) { true }
|
113
265
|
|
114
|
-
it 'is raises an UnsupportedDataType
|
266
|
+
it 'is raises an UnsupportedDataType 40013 exception' do
|
115
267
|
expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
|
116
268
|
end
|
117
269
|
end
|
@@ -119,7 +271,7 @@ describe Ably::Rest::Channel, 'messages' do
|
|
119
271
|
context 'False' do
|
120
272
|
let(:data) { false }
|
121
273
|
|
122
|
-
it 'is raises an UnsupportedDataType
|
274
|
+
it 'is raises an UnsupportedDataType 40013 exception' do
|
123
275
|
expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
|
124
276
|
end
|
125
277
|
end
|
@@ -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
|