ably 1.1.0 → 1.1.4

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check.yml +27 -0
  3. data/CHANGELOG.md +68 -2
  4. data/COPYRIGHT +1 -0
  5. data/LICENSE +172 -11
  6. data/MAINTAINERS.md +1 -0
  7. data/README.md +3 -7
  8. data/SPEC.md +944 -914
  9. data/ably.gemspec +7 -7
  10. data/lib/ably/auth.rb +12 -2
  11. data/lib/ably/exceptions.rb +2 -2
  12. data/lib/ably/logger.rb +7 -1
  13. data/lib/ably/modules/state_machine.rb +1 -1
  14. data/lib/ably/realtime/channel.rb +7 -11
  15. data/lib/ably/realtime/channel/channel_manager.rb +2 -2
  16. data/lib/ably/realtime/channel/channel_properties.rb +24 -0
  17. data/lib/ably/realtime/client.rb +12 -3
  18. data/lib/ably/realtime/connection.rb +31 -19
  19. data/lib/ably/realtime/connection/connection_manager.rb +19 -3
  20. data/lib/ably/realtime/connection/websocket_transport.rb +67 -1
  21. data/lib/ably/realtime/presence.rb +0 -14
  22. data/lib/ably/rest/channel.rb +25 -17
  23. data/lib/ably/rest/client.rb +22 -11
  24. data/lib/ably/version.rb +1 -1
  25. data/spec/acceptance/realtime/auth_spec.rb +16 -13
  26. data/spec/acceptance/realtime/channel_history_spec.rb +26 -20
  27. data/spec/acceptance/realtime/channel_spec.rb +21 -8
  28. data/spec/acceptance/realtime/client_spec.rb +80 -20
  29. data/spec/acceptance/realtime/connection_failures_spec.rb +71 -5
  30. data/spec/acceptance/realtime/connection_spec.rb +153 -26
  31. data/spec/acceptance/realtime/message_spec.rb +17 -17
  32. data/spec/acceptance/realtime/presence_history_spec.rb +0 -58
  33. data/spec/acceptance/realtime/presence_spec.rb +250 -162
  34. data/spec/acceptance/realtime/push_admin_spec.rb +49 -25
  35. data/spec/acceptance/rest/auth_spec.rb +6 -75
  36. data/spec/acceptance/rest/channel_spec.rb +79 -4
  37. data/spec/acceptance/rest/channels_spec.rb +6 -0
  38. data/spec/acceptance/rest/client_spec.rb +72 -12
  39. data/spec/acceptance/rest/message_spec.rb +8 -27
  40. data/spec/acceptance/rest/push_admin_spec.rb +67 -27
  41. data/spec/shared/client_initializer_behaviour.rb +0 -8
  42. data/spec/spec_helper.rb +2 -1
  43. data/spec/support/debug_failure_helper.rb +9 -5
  44. data/spec/support/serialization_helper.rb +21 -0
  45. data/spec/support/test_app.rb +2 -2
  46. data/spec/unit/modules/enum_spec.rb +1 -1
  47. data/spec/unit/realtime/client_spec.rb +20 -7
  48. data/spec/unit/realtime/connection_spec.rb +1 -1
  49. metadata +40 -29
  50. data/.travis.yml +0 -16
@@ -63,20 +63,22 @@ describe Ably::Realtime::Push::Admin, :event_machine do
63
63
  end
64
64
 
65
65
  context 'invalid recipient' do
66
+ let(:default_options) { { key: api_key, environment: environment, protocol: protocol, log_level: :fatal } }
67
+
66
68
  it 'raises an error after receiving a 40x realtime response' do
67
- skip 'validation on raw push is not enabled in realtime'
68
69
  subject.publish({ invalid_recipient_details: 'foo.bar' }, basic_notification_payload).errback do |error|
69
- expect(error.message).to match(/Invalid recipient/)
70
+ expect(error.message).to match(/recipient must contain/)
70
71
  stop_reactor
71
72
  end
72
73
  end
73
74
  end
74
75
 
75
76
  context 'invalid push data' do
77
+ let(:default_options) { { key: api_key, environment: environment, protocol: protocol, log_level: :fatal } }
78
+
76
79
  it 'raises an error after receiving a 40x realtime response' do
77
- skip 'validation on raw push is not enabled in realtime'
78
80
  subject.publish(basic_recipient, { invalid_property_only: true }).errback do |error|
79
- expect(error.message).to match(/Invalid push notification data/)
81
+ expect(error.message).to match(/Unexpected field/)
80
82
  stop_reactor
81
83
  end
82
84
  end
@@ -99,31 +101,15 @@ describe Ably::Realtime::Push::Admin, :event_machine do
99
101
  end
100
102
  end
101
103
 
102
- def request_body(request, protocol)
103
- if protocol == :msgpack
104
- MessagePack.unpack(request.body)
105
- else
106
- JSON.parse(request.body)
107
- end
108
- end
109
-
110
- def serialize(object, protocol)
111
- if protocol == :msgpack
112
- MessagePack.pack(object)
113
- else
114
- JSON.dump(object)
115
- end
116
- end
117
-
118
104
  let!(:publish_stub) do
119
105
  stub_request(:post, "#{client.rest_client.endpoint}/push/publish").
120
106
  with do |request|
121
- expect(request_body(request, protocol)['recipient']['camelCase']['secondLevelCamelCase']).to eql('val')
122
- expect(request_body(request, protocol)['recipient']).to_not have_key('camel_case')
107
+ expect(deserialize_body(request.body, protocol)['recipient']['camelCase']['secondLevelCamelCase']).to eql('val')
108
+ expect(deserialize_body(request.body, protocol)['recipient']).to_not have_key('camel_case')
123
109
  true
124
110
  end.to_return(
125
111
  :status => 201,
126
- :body => serialize({}, protocol),
112
+ :body => serialize_body({}, protocol),
127
113
  :headers => { 'Content-Type' => content_type }
128
114
  )
129
115
  end
@@ -215,6 +201,12 @@ describe Ably::Realtime::Push::Admin, :event_machine do
215
201
  let(:client_id) { random_str }
216
202
  let(:fixture_count) { 6 }
217
203
 
204
+ before(:all) do
205
+ # As push tests often use the global scope (devices),
206
+ # we need to ensure tests cannot conflict
207
+ reload_test_app
208
+ end
209
+
218
210
  before do
219
211
  fixture_count.times.map do |index|
220
212
  Thread.new do # Parallelise the setup
@@ -272,6 +264,12 @@ describe Ably::Realtime::Push::Admin, :event_machine do
272
264
  let(:fixture_count) { 2 }
273
265
  let(:client_id) { random_str }
274
266
 
267
+ before(:all) do
268
+ # As push tests often use the global scope (devices),
269
+ # we need to ensure tests cannot conflict
270
+ reload_test_app
271
+ end
272
+
275
273
  before do
276
274
  fixture_count.times.map do |index|
277
275
  Thread.new do # Parallelise the setup
@@ -351,6 +349,12 @@ describe Ably::Realtime::Push::Admin, :event_machine do
351
349
  }
352
350
  end
353
351
 
352
+ before(:all) do
353
+ # As push tests often use the global scope (devices),
354
+ # we need to ensure tests cannot conflict
355
+ reload_test_app
356
+ end
357
+
354
358
  after do
355
359
  rest_device_registrations.remove_where client_id: client_id
356
360
  end
@@ -386,6 +390,12 @@ describe Ably::Realtime::Push::Admin, :event_machine do
386
390
  let(:device_id) { random_str }
387
391
  let(:client_id) { random_str }
388
392
 
393
+ before(:all) do
394
+ # As push tests often use the global scope (devices),
395
+ # we need to ensure tests cannot conflict
396
+ reload_test_app
397
+ end
398
+
389
399
  before do
390
400
  rest_device_registrations.save({
391
401
  id: "device-#{client_id}-0",
@@ -419,6 +429,12 @@ describe Ably::Realtime::Push::Admin, :event_machine do
419
429
  let(:device_id) { random_str }
420
430
  let(:client_id) { random_str }
421
431
 
432
+ before(:all) do
433
+ # As push tests often use the global scope (devices),
434
+ # we need to ensure tests cannot conflict
435
+ reload_test_app
436
+ end
437
+
422
438
  before do
423
439
  rest_device_registrations.save({
424
440
  id: "device-#{client_id}-0",
@@ -479,6 +495,12 @@ describe Ably::Realtime::Push::Admin, :event_machine do
479
495
  client.push.admin.channel_subscriptions
480
496
  }
481
497
 
498
+ before(:all) do
499
+ # As push tests often use the global scope (devices),
500
+ # we need to ensure tests cannot conflict
501
+ reload_test_app
502
+ end
503
+
482
504
  # Set up 2 devices with the same client_id
483
505
  # and two device with the unique device_id and no client_id
484
506
  before do
@@ -541,8 +563,10 @@ describe Ably::Realtime::Push::Admin, :event_machine do
541
563
  describe '#list_channels' do
542
564
  let(:fixture_count) { 6 }
543
565
 
544
- before(:context) do
545
- reload_test_app # TODO: Review if necessary later, currently other tests may affect list_channels
566
+ before(:all) do
567
+ # As push tests often use the global scope (devices),
568
+ # we need to ensure tests cannot conflict
569
+ reload_test_app
546
570
  end
547
571
 
548
572
  before do
@@ -41,22 +41,10 @@ describe Ably::Auth do
41
41
  end
42
42
 
43
43
  def request_body_includes(request, protocol, key, val)
44
- body = if protocol == :msgpack
45
- MessagePack.unpack(request.body)
46
- else
47
- JSON.parse(request.body)
48
- end
44
+ body = deserialize_body(request.body, protocol)
49
45
  body[convert_to_mixed_case(key)].to_s == val.to_s
50
46
  end
51
47
 
52
- def serialize(object, protocol)
53
- if protocol == :msgpack
54
- MessagePack.pack(object)
55
- else
56
- JSON.dump(object)
57
- end
58
- end
59
-
60
48
  it 'has immutable options' do
61
49
  expect { auth.options['key_name'] = 'new_name' }.to raise_error RuntimeError, /can't modify frozen.*Hash/
62
50
  end
@@ -74,7 +62,7 @@ describe Ably::Auth do
74
62
 
75
63
  it 'creates a TokenRequest automatically and sends it to Ably to obtain a token', webmock: true do
76
64
  token_request_stub = stub_request(:post, "#{client.endpoint}/keys/#{key_name}/requestToken").
77
- to_return(status: 201, body: serialize({}, protocol), headers: { 'Content-Type' => content_type })
65
+ to_return(status: 201, body: serialize_body({}, protocol), headers: { 'Content-Type' => content_type })
78
66
  expect(auth).to receive(:create_token_request).and_call_original
79
67
  auth.request_token
80
68
 
@@ -107,7 +95,7 @@ describe Ably::Auth do
107
95
  request_body_includes(request, protocol, token_param, coerce_if_time_value(token_param, random, multiply: 1000))
108
96
  end.to_return(
109
97
  :status => 201,
110
- :body => serialize(token_response, protocol),
98
+ :body => serialize_body(token_response, protocol),
111
99
  :headers => { 'Content-Type' => content_type }
112
100
  )
113
101
  end
@@ -138,7 +126,7 @@ describe Ably::Auth do
138
126
  request_body_includes(request, protocol, 'mac', mac)
139
127
  end.to_return(
140
128
  :status => 201,
141
- :body => serialize(token_response, protocol),
129
+ :body => serialize_body(token_response, protocol),
142
130
  :headers => { 'Content-Type' => content_type })
143
131
  end
144
132
 
@@ -168,7 +156,7 @@ describe Ably::Auth do
168
156
  request_body_includes(request, protocol, 'mac', mac)
169
157
  end.to_return(
170
158
  :status => 201,
171
- :body => serialize(token_response, protocol),
159
+ :body => serialize_body(token_response, protocol),
172
160
  :headers => { 'Content-Type' => content_type })
173
161
  end
174
162
 
@@ -310,7 +298,7 @@ describe Ably::Auth do
310
298
  request_body_includes(request, protocol, 'key_name', key_name)
311
299
  end.to_return(
312
300
  :status => 201,
313
- :body => serialize(token_response, protocol),
301
+ :body => serialize_body(token_response, protocol),
314
302
  :headers => { 'Content-Type' => content_type }
315
303
  )
316
304
  end
@@ -1129,63 +1117,6 @@ describe Ably::Auth do
1129
1117
  end
1130
1118
  end
1131
1119
 
1132
- context 'when implicit as a result of using :client_id' do
1133
- let(:client_id) { '999' }
1134
- let(:client) do
1135
- Ably::Rest::Client.new(key: api_key, client_id: client_id, environment: environment, protocol: protocol)
1136
- end
1137
- let(:token) { 'unique-token' }
1138
- let(:token_response) do
1139
- {
1140
- token: token
1141
- }.to_json
1142
- end
1143
-
1144
- context 'and requests to the Ably server are mocked', :webmock do
1145
- let!(:request_token_stub) do
1146
- stub_request(:post, "#{client.endpoint}/keys/#{key_name}/requestToken").
1147
- to_return(:status => 201, :body => token_response, :headers => { 'Content-Type' => 'application/json' })
1148
- end
1149
- let!(:publish_message_stub) do
1150
- stub_request(:post, "#{client.endpoint}/channels/foo/publish").
1151
- with(headers: { 'Authorization' => "Bearer #{encode64(token)}" }).
1152
- to_return(status: 201, body: '{}', headers: { 'Content-Type' => 'application/json' })
1153
- end
1154
-
1155
- it 'will send a token request to the server' do
1156
- client.channel('foo').publish('event', 'data')
1157
- expect(request_token_stub).to have_been_requested
1158
- end
1159
- end
1160
-
1161
- describe 'a token is created' do
1162
- let(:token) { client.auth.current_token_details }
1163
-
1164
- it 'before a request is made' do
1165
- expect(token).to be_nil
1166
- end
1167
-
1168
- it 'when a message is published' do
1169
- expect(client.channel('foo').publish('event', 'data')).to be_truthy
1170
- end
1171
-
1172
- it 'with capability and TTL defaults (#TK2a, #TK2b)' do
1173
- client.channel('foo').publish('event', 'data')
1174
-
1175
- expect(token).to be_a(Ably::Models::TokenDetails)
1176
- capability_with_str_key = { "*" => ["*"] } # Ably default is all capabilities
1177
- capability = Hash[capability_with_str_key.keys.map(&:to_s).zip(capability_with_str_key.values)]
1178
- expect(token.capability).to eq(capability)
1179
- expect(token.expires.to_i).to be_within(2).of(Time.now.to_i + 60 * 60) # Ably default is 1hr
1180
- expect(token.client_id).to eq(client_id)
1181
- end
1182
-
1183
- specify '#client_id contains the client_id' do
1184
- expect(client.auth.client_id).to eql(client_id)
1185
- end
1186
- end
1187
- end
1188
-
1189
1120
  context 'when token expires' do
1190
1121
  before do
1191
1122
  stub_const 'Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER', 0 # allow token to be used even if about to expire
@@ -40,7 +40,7 @@ describe Ably::Rest::Channel do
40
40
 
41
41
  it 'publishes the message without a client_id' do
42
42
  expect(client).to receive(:post).
43
- with("/channels/#{channel_name}/publish", hash_excluding(client_id: client_id)).
43
+ with("/channels/#{channel_name}/publish", hash_excluding(client_id: client_id), {}).
44
44
  and_return(double('response', status: 201))
45
45
 
46
46
  expect(channel.publish(name, data)).to eql(true)
@@ -82,6 +82,44 @@ describe Ably::Rest::Channel do
82
82
  end
83
83
  end
84
84
 
85
+ context 'with a Message object' do
86
+ let(:name) { random_str }
87
+
88
+ let(:message) do
89
+ Ably::Models::Message(name: name, data: data)
90
+ end
91
+
92
+ it 'publishes the message' do
93
+ expect(client).to receive(:post).once.and_call_original
94
+ expect(channel.publish(message)).to eql(true)
95
+ expect(channel.history.items.first.name).to eql(name)
96
+ end
97
+ end
98
+
99
+ context 'with a Message object and query params' do
100
+ let(:message) do
101
+ Ably::Models::Message(name: name, data: data)
102
+ end
103
+
104
+ it 'should fail to publish the message (RSL1l1)' do
105
+ expect(client).to receive(:post).once.and_call_original
106
+ expect { channel.publish(message, { _forceNack: 'true' }) }.to raise_error(Ably::Exceptions::InvalidRequest, /40099/)
107
+ end
108
+ end
109
+
110
+ context 'with Messages and query params' do
111
+ let(:messages) do
112
+ 10.times.map do |index|
113
+ { name: index.to_s, data: { "index" => index + 10 } }
114
+ end
115
+ end
116
+
117
+ it 'should fail to publish the message (RSL1l1)' do
118
+ expect(client).to receive(:post).once.and_call_original
119
+ expect { channel.publish(messages, { _forceNack: 'true' }) }.to raise_error(Ably::Exceptions::InvalidRequest, /40099/)
120
+ end
121
+ end
122
+
85
123
  context 'without adequate permissions on the channel' do
86
124
  let(:capability) { { onlyChannel: ['subscribe'] } }
87
125
  let(:client_options) { default_options.merge(use_token_auth: true, default_token_params: { capability: capability }) }
@@ -96,7 +134,7 @@ describe Ably::Rest::Channel do
96
134
  let(:data) { random_str }
97
135
 
98
136
  it 'publishes the message without a name attribute in the payload' do
99
- expect(client).to receive(:post).with(anything, { "data" => data }).once.and_call_original
137
+ expect(client).to receive(:post).with(anything, { "data" => data }, {}).once.and_call_original
100
138
  expect(channel.publish(nil, data)).to eql(true)
101
139
  expect(channel.history.items.first.name).to be_nil
102
140
  expect(channel.history.items.first.data).to eql(data)
@@ -107,7 +145,7 @@ describe Ably::Rest::Channel do
107
145
  let(:name) { random_str }
108
146
 
109
147
  it 'publishes the message without a data attribute in the payload' do
110
- expect(client).to receive(:post).with(anything, { "name" => name }).once.and_call_original
148
+ expect(client).to receive(:post).with(anything, { "name" => name }, {}).once.and_call_original
111
149
  expect(channel.publish(name)).to eql(true)
112
150
  expect(channel.history.items.first.name).to eql(name)
113
151
  expect(channel.history.items.first.data).to be_nil
@@ -118,7 +156,7 @@ describe Ably::Rest::Channel do
118
156
  let(:name) { random_str }
119
157
 
120
158
  it 'publishes the message without any attributes in the payload' do
121
- expect(client).to receive(:post).with(anything, {}).once.and_call_original
159
+ expect(client).to receive(:post).with(anything, {}, {}).once.and_call_original
122
160
  expect(channel.publish(nil)).to eql(true)
123
161
  expect(channel.history.items.first.name).to be_nil
124
162
  expect(channel.history.items.first.data).to be_nil
@@ -275,6 +313,43 @@ describe Ably::Rest::Channel do
275
313
  end
276
314
  end
277
315
  end
316
+
317
+ context 'with a frozen message event name' do
318
+ let(:event_name) { random_str.freeze }
319
+
320
+ it 'succeeds and publishes with an implicit client_id' do
321
+ channel.publish([name: event_name])
322
+ channel.publish(event_name)
323
+
324
+ if !(RUBY_VERSION.match(/^1\./) || RUBY_VERSION.match(/^2\.[012]/))
325
+ channel.publish(+'foo-bar') # new style freeze, see https://github.com/ably/ably-ruby/issues/132
326
+ else
327
+ channel.publish('foo-bar'.freeze) # new + style not supported until Ruby 2.3
328
+ end
329
+
330
+ channel.history do |messages|
331
+ expect(messages.length).to eql(3)
332
+ expect(messages.first.name).to eql(event_name)
333
+ expect(messages[1].name).to eql(event_name)
334
+ expect(messages.last.name).to eql('foo-bar')
335
+ end
336
+ end
337
+ end
338
+
339
+ context 'with a frozen payload' do
340
+ let(:payload) { { foo: random_str.freeze }.freeze }
341
+
342
+ it 'succeeds and publishes with an implicit client_id' do
343
+ channel.publish([data: payload])
344
+ channel.publish(nil, payload)
345
+
346
+ channel.history do |messages|
347
+ expect(messages.length).to eql(2)
348
+ expect(messages.first.data).to eql(payload)
349
+ expect(messages.last.data).to eql(payload)
350
+ end
351
+ end
352
+ end
278
353
  end
279
354
 
280
355
  describe '#history' do
@@ -60,5 +60,11 @@ describe Ably::Rest::Channels do
60
60
  let(:channel_with_options) { client.channels[channel_name, options] }
61
61
  it_behaves_like 'a channel'
62
62
  end
63
+
64
+ describe 'using a frozen channel name' do
65
+ let(:channel) { client.channels[channel_name.freeze] }
66
+ let(:channel_with_options) { client.channels[channel_name.freeze, options] }
67
+ it_behaves_like 'a channel'
68
+ end
63
69
  end
64
70
  end
@@ -12,7 +12,7 @@ describe Ably::Rest::Client do
12
12
  http_defaults = Ably::Rest::Client::HTTP_DEFAULTS
13
13
 
14
14
  def encode64(text)
15
- Base64.encode64(text).gsub("\n", '')
15
+ Base64.urlsafe_encode64(text)
16
16
  end
17
17
 
18
18
  context '#initialize' do
@@ -56,14 +56,6 @@ describe Ably::Rest::Client do
56
56
  end
57
57
  end
58
58
 
59
- context 'with a :client_id configured' do
60
- let(:client) { Ably::Rest::Client.new(client_options.merge(key: api_key, client_id: random_str)) }
61
-
62
- it 'uses token authentication' do
63
- expect(client.auth).to be_using_token_auth
64
- end
65
- end
66
-
67
59
  context 'with a non string :client_id' do
68
60
  let(:client) { Ably::Rest::Client.new(client_options.merge(key: api_key, client_id: 1)) }
69
61
 
@@ -144,11 +136,12 @@ describe Ably::Rest::Client do
144
136
  let(:history_querystring) { history_params.map { |k, v| "#{k}=#{v}" }.join("&") }
145
137
 
146
138
  context 'with basic auth', webmock: true do
147
- let(:client_options) { default_options.merge(key: api_key) }
139
+ let(:client_options) { default_options.merge(key: api_key, client_id: client_id) }
148
140
 
149
141
  let!(:get_message_history_stub) do
150
- stub_request(:get, "https://#{environment}-#{Ably::Rest::Client::DOMAIN}/channels/#{channel_name}/messages?#{history_querystring}").
151
- to_return(body: [], headers: { 'Content-Type' => 'application/json' })
142
+ stub_request(:get, "https://#{environment}-#{Ably::Rest::Client::DOMAIN}/channels/#{channel_name}/messages?#{history_querystring}")
143
+ .with(headers: { 'X-Ably-ClientId' => encode64(client_id) })
144
+ .to_return(body: [], headers: { 'Content-Type' => 'application/json' })
152
145
  end
153
146
 
154
147
  it 'sends the API key in authentication part of the secure URL (the Authorization: Basic header is not used with the Faraday HTTP library by default)' do
@@ -1091,6 +1084,8 @@ describe Ably::Rest::Client do
1091
1084
 
1092
1085
  context '#request (#RSC19*)' do
1093
1086
  let(:client_options) { default_options.merge(key: api_key) }
1087
+ let(:device_id) { random_str }
1088
+ let(:endpoint) { client.endpoint }
1094
1089
 
1095
1090
  context 'get' do
1096
1091
  it 'returns an HttpPaginatedResponse object' do
@@ -1130,6 +1125,71 @@ describe Ably::Rest::Client do
1130
1125
  end
1131
1126
  end
1132
1127
  end
1128
+
1129
+ context 'post', :webmock do
1130
+ before do
1131
+ stub_request(:delete, "#{endpoint}/push/deviceRegistrations/#{device_id}/resetUpdateToken").
1132
+ to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
1133
+ end
1134
+
1135
+ it 'supports post' do
1136
+ response = client.request(:delete, "push/deviceRegistrations/#{device_id}/resetUpdateToken")
1137
+
1138
+ expect(response).to be_success
1139
+ end
1140
+ end
1141
+
1142
+ context 'delete', :webmock do
1143
+ before do
1144
+ stub_request(:delete, "#{endpoint}/push/channelSubscriptions?deviceId=#{device_id}").
1145
+ to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
1146
+ end
1147
+
1148
+ it 'supports delete' do
1149
+ response = client.request(:delete, "/push/channelSubscriptions", { deviceId: device_id})
1150
+
1151
+ expect(response).to be_success
1152
+ end
1153
+ end
1154
+
1155
+ context 'patch', :webmock do
1156
+ let(:body_params) { { 'metadata' => { 'key' => 'value' } } }
1157
+
1158
+ before do
1159
+ stub_request(:patch, "#{endpoint}/push/deviceRegistrations/#{device_id}")
1160
+ .with(body: serialize_body(body_params, protocol))
1161
+ .to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
1162
+ end
1163
+
1164
+ it 'supports patch' do
1165
+ response = client.request(:patch, "/push/deviceRegistrations/#{device_id}", {}, body_params)
1166
+
1167
+ expect(response).to be_success
1168
+ end
1169
+ end
1170
+
1171
+ context 'put', :webmock do
1172
+ let(:body_params) do
1173
+ {
1174
+ 'id' => random_str,
1175
+ 'platform' => 'ios',
1176
+ 'formFactor' => 'phone',
1177
+ 'metadata' => { 'key' => 'value' }
1178
+ }
1179
+ end
1180
+
1181
+ before do
1182
+ stub_request(:put, "#{endpoint}/push/deviceRegistrations/#{device_id}")
1183
+ .with(body: serialize_body(body_params, protocol))
1184
+ .to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
1185
+ end
1186
+
1187
+ it 'supports put' do
1188
+ response = client.request(:put, "/push/deviceRegistrations/#{device_id}", {}, body_params)
1189
+
1190
+ expect(response).to be_success
1191
+ end
1192
+ end
1133
1193
  end
1134
1194
 
1135
1195
  context 'request_id generation' do