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.
Files changed (118) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +6 -3
  3. data/CHANGELOG.md +1 -1
  4. data/LICENSE +1 -1
  5. data/README.md +26 -7
  6. data/SPEC.md +2003 -1605
  7. data/ably-rest.gemspec +4 -2
  8. data/lib/submodules/ably-ruby/.editorconfig +14 -0
  9. data/lib/submodules/ably-ruby/.travis.yml +10 -8
  10. data/lib/submodules/ably-ruby/CHANGELOG.md +97 -1
  11. data/lib/submodules/ably-ruby/LICENSE +1 -3
  12. data/lib/submodules/ably-ruby/README.md +12 -7
  13. data/lib/submodules/ably-ruby/Rakefile +32 -0
  14. data/lib/submodules/ably-ruby/SPEC.md +1277 -835
  15. data/lib/submodules/ably-ruby/ably.gemspec +17 -11
  16. data/lib/submodules/ably-ruby/lib/ably/auth.rb +34 -8
  17. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +10 -4
  18. data/lib/submodules/ably-ruby/lib/ably/logger.rb +8 -2
  19. data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +1 -1
  20. data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +1 -1
  21. data/lib/submodules/ably-ruby/lib/ably/models/device_details.rb +87 -0
  22. data/lib/submodules/ably-ruby/lib/ably/models/device_push_details.rb +86 -0
  23. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +23 -2
  24. data/lib/submodules/ably-ruby/lib/ably/models/idiomatic_ruby_wrapper.rb +12 -12
  25. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +6 -4
  26. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +6 -4
  27. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +32 -2
  28. data/lib/submodules/ably-ruby/lib/ably/models/push_channel_subscription.rb +89 -0
  29. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +2 -2
  30. data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +2 -2
  31. data/lib/submodules/ably-ruby/lib/ably/modules/encodeable.rb +2 -2
  32. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +2 -2
  33. data/lib/submodules/ably-ruby/lib/ably/modules/exception_codes.rb +128 -0
  34. data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +15 -2
  35. data/lib/submodules/ably-ruby/lib/ably/modules/safe_deferrable.rb +1 -1
  36. data/lib/submodules/ably-ruby/lib/ably/modules/safe_yield.rb +1 -1
  37. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +5 -5
  38. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +2 -2
  39. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +1 -0
  40. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +2 -2
  41. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +27 -105
  42. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +4 -8
  43. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
  44. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/publisher.rb +74 -0
  45. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/push_channel.rb +62 -0
  46. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +91 -3
  47. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +9 -4
  48. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  49. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +45 -26
  50. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +25 -9
  51. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +2 -2
  52. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +7 -7
  53. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +9 -9
  54. data/lib/submodules/ably-ruby/lib/ably/realtime/push.rb +40 -0
  55. data/lib/submodules/ably-ruby/lib/ably/realtime/push/admin.rb +61 -0
  56. data/lib/submodules/ably-ruby/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  57. data/lib/submodules/ably-ruby/lib/ably/realtime/push/device_registrations.rb +105 -0
  58. data/lib/submodules/ably-ruby/lib/ably/rest.rb +1 -0
  59. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +54 -18
  60. data/lib/submodules/ably-ruby/lib/ably/rest/channel/push_channel.rb +62 -0
  61. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +171 -41
  62. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
  63. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -0
  64. data/lib/submodules/ably-ruby/lib/ably/rest/push.rb +42 -0
  65. data/lib/submodules/ably-ruby/lib/ably/rest/push/admin.rb +54 -0
  66. data/lib/submodules/ably-ruby/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  67. data/lib/submodules/ably-ruby/lib/ably/rest/push/device_registrations.rb +103 -0
  68. data/lib/submodules/ably-ruby/lib/ably/version.rb +7 -2
  69. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +253 -49
  70. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +33 -21
  71. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +180 -62
  72. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +155 -2
  73. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +293 -13
  74. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +142 -39
  75. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +38 -36
  76. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +12 -3
  77. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +207 -173
  78. data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_admin_spec.rb +736 -0
  79. data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_spec.rb +27 -0
  80. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +62 -51
  81. data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +2 -2
  82. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +79 -4
  83. data/lib/submodules/ably-ruby/spec/acceptance/rest/channels_spec.rb +6 -0
  84. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +318 -74
  85. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +158 -6
  86. data/lib/submodules/ably-ruby/spec/acceptance/rest/push_admin_spec.rb +952 -0
  87. data/lib/submodules/ably-ruby/spec/acceptance/rest/push_spec.rb +25 -0
  88. data/lib/submodules/ably-ruby/spec/acceptance/rest/time_spec.rb +1 -1
  89. data/lib/submodules/ably-ruby/spec/run_parallel_tests +33 -0
  90. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +1 -9
  91. data/lib/submodules/ably-ruby/spec/spec_helper.rb +3 -1
  92. data/lib/submodules/ably-ruby/spec/support/debug_failure_helper.rb +9 -5
  93. data/lib/submodules/ably-ruby/spec/support/event_emitter_helper.rb +31 -0
  94. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +1 -1
  95. data/lib/submodules/ably-ruby/spec/support/test_app.rb +2 -2
  96. data/lib/submodules/ably-ruby/spec/support/test_logger_helper.rb +42 -0
  97. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +11 -12
  98. data/lib/submodules/ably-ruby/spec/unit/models/device_details_spec.rb +102 -0
  99. data/lib/submodules/ably-ruby/spec/unit/models/device_push_details_spec.rb +101 -0
  100. data/lib/submodules/ably-ruby/spec/unit/models/error_info_spec.rb +51 -3
  101. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +17 -2
  102. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +1 -1
  103. data/lib/submodules/ably-ruby/spec/unit/models/push_channel_subscription_spec.rb +86 -0
  104. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +2 -2
  105. data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +1 -1
  106. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +3 -3
  107. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +10 -10
  108. data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +1 -1
  109. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +13 -1
  110. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +2 -2
  111. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +1 -1
  112. data/lib/submodules/ably-ruby/spec/unit/realtime/push_channel_spec.rb +36 -0
  113. data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +30 -1
  114. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +30 -0
  115. data/lib/submodules/ably-ruby/spec/unit/rest/push_channel_spec.rb +36 -0
  116. data/lib/submodules/ably-ruby/spec/unit/util/pub_sub_spec.rb +3 -3
  117. data/spec/spec_helper.rb +1 -0
  118. 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
- # TODO: Add nil type back in
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 40011 exception' do
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 40011 exception' do
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 40011 exception' do
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 40011 exception' do
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