ably 1.1.4.rc → 1.1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check.yml +41 -0
  3. data/CHANGELOG.md +85 -0
  4. data/COPYRIGHT +1 -0
  5. data/LICENSE +173 -10
  6. data/MAINTAINERS.md +1 -0
  7. data/README.md +24 -18
  8. data/SPEC.md +1020 -922
  9. data/ably.gemspec +13 -8
  10. data/lib/ably.rb +1 -0
  11. data/lib/ably/agent.rb +3 -0
  12. data/lib/ably/auth.rb +12 -2
  13. data/lib/ably/exceptions.rb +6 -0
  14. data/lib/ably/models/connection_details.rb +2 -0
  15. data/lib/ably/models/message.rb +14 -0
  16. data/lib/ably/models/presence_message.rb +14 -0
  17. data/lib/ably/models/protocol_message.rb +8 -0
  18. data/lib/ably/modules/ably.rb +11 -1
  19. data/lib/ably/realtime/channel.rb +7 -11
  20. data/lib/ably/realtime/channel/channel_manager.rb +3 -3
  21. data/lib/ably/realtime/channel/channel_properties.rb +24 -0
  22. data/lib/ably/realtime/channel/publisher.rb +5 -0
  23. data/lib/ably/realtime/client.rb +9 -0
  24. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +14 -6
  25. data/lib/ably/realtime/connection.rb +9 -5
  26. data/lib/ably/realtime/connection/websocket_transport.rb +67 -1
  27. data/lib/ably/realtime/presence.rb +0 -14
  28. data/lib/ably/rest/channel.rb +10 -3
  29. data/lib/ably/rest/client.rb +22 -21
  30. data/lib/ably/version.rb +1 -13
  31. data/spec/acceptance/realtime/auth_spec.rb +1 -1
  32. data/spec/acceptance/realtime/channel_history_spec.rb +25 -0
  33. data/spec/acceptance/realtime/channel_spec.rb +24 -0
  34. data/spec/acceptance/realtime/client_spec.rb +72 -16
  35. data/spec/acceptance/realtime/connection_failures_spec.rb +29 -12
  36. data/spec/acceptance/realtime/connection_spec.rb +31 -33
  37. data/spec/acceptance/realtime/presence_history_spec.rb +3 -59
  38. data/spec/acceptance/realtime/presence_spec.rb +66 -157
  39. data/spec/acceptance/realtime/push_admin_spec.rb +3 -19
  40. data/spec/acceptance/rest/auth_spec.rb +6 -75
  41. data/spec/acceptance/rest/base_spec.rb +8 -4
  42. data/spec/acceptance/rest/channel_spec.rb +13 -0
  43. data/spec/acceptance/rest/client_spec.rb +144 -45
  44. data/spec/acceptance/rest/push_admin_spec.rb +3 -19
  45. data/spec/shared/client_initializer_behaviour.rb +131 -8
  46. data/spec/shared/model_behaviour.rb +1 -1
  47. data/spec/spec_helper.rb +12 -2
  48. data/spec/support/serialization_helper.rb +21 -0
  49. data/spec/unit/models/message_spec.rb +59 -0
  50. data/spec/unit/models/presence_message_spec.rb +49 -0
  51. data/spec/unit/models/protocol_message_spec.rb +48 -0
  52. data/spec/unit/realtime/channel_spec.rb +1 -1
  53. data/spec/unit/realtime/client_spec.rb +19 -6
  54. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +38 -0
  55. data/spec/unit/rest/channel_spec.rb +10 -0
  56. data/spec/unit/rest/client_spec.rb +20 -0
  57. metadata +52 -32
  58. data/.travis.yml +0 -18
@@ -101,31 +101,15 @@ describe Ably::Realtime::Push::Admin, :event_machine do
101
101
  end
102
102
  end
103
103
 
104
- def request_body(request, protocol)
105
- if protocol == :msgpack
106
- MessagePack.unpack(request.body)
107
- else
108
- JSON.parse(request.body)
109
- end
110
- end
111
-
112
- def serialize(object, protocol)
113
- if protocol == :msgpack
114
- MessagePack.pack(object)
115
- else
116
- JSON.dump(object)
117
- end
118
- end
119
-
120
104
  let!(:publish_stub) do
121
105
  stub_request(:post, "#{client.rest_client.endpoint}/push/publish").
122
106
  with do |request|
123
- expect(request_body(request, protocol)['recipient']['camelCase']['secondLevelCamelCase']).to eql('val')
124
- 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')
125
109
  true
126
110
  end.to_return(
127
111
  :status => 201,
128
- :body => serialize({}, protocol),
112
+ :body => serialize_body({}, protocol),
129
113
  :headers => { 'Content-Type' => content_type }
130
114
  )
131
115
  end
@@ -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
@@ -87,8 +87,10 @@ describe Ably::Rest do
87
87
  let(:error_response) { '{ "error": { "statusCode": 500, "code": 50000, "message": "Internal error" } }' }
88
88
 
89
89
  before do
90
- stub_request(:get, "#{client.endpoint}/time").
91
- to_return(:status => 500, :body => error_response, :headers => { 'Content-Type' => 'application/json' })
90
+ (client.fallback_hosts.map { |host| "https://#{host}" } + [client.endpoint]).each do |host|
91
+ stub_request(:get, "#{host}/time")
92
+ .to_return(:status => 500, :body => error_response, :headers => { 'Content-Type' => 'application/json' })
93
+ end
92
94
  end
93
95
 
94
96
  it 'should raise a ServerError exception' do
@@ -98,8 +100,10 @@ describe Ably::Rest do
98
100
 
99
101
  describe '500 server error without a valid JSON response body', :webmock do
100
102
  before do
101
- stub_request(:get, "#{client.endpoint}/time").
102
- to_return(:status => 500, :headers => { 'Content-Type' => 'application/json' })
103
+ (client.fallback_hosts.map { |host| "https://#{host}" } + [client.endpoint]).each do |host|
104
+ stub_request(:get, "#{host}/time").
105
+ to_return(:status => 500, :headers => { 'Content-Type' => 'application/json' })
106
+ end
103
107
  end
104
108
 
105
109
  it 'should raise a ServerError exception' do
@@ -60,6 +60,8 @@ describe Ably::Rest::Channel do
60
60
  end
61
61
 
62
62
  it 'publishes an array of messages in one HTTP request' do
63
+ expect(messages.sum(&:size) < Ably::Rest::Channel::MAX_MESSAGE_SIZE).to eq(true)
64
+
63
65
  expect(client).to receive(:post).once.and_call_original
64
66
  expect(channel.publish(messages)).to eql(true)
65
67
  expect(channel.history.items.map(&:name)).to match_array(messages.map { |message| message[:name] })
@@ -75,6 +77,8 @@ describe Ably::Rest::Channel do
75
77
  end
76
78
 
77
79
  it 'publishes an array of messages in one HTTP request' do
80
+ expect(messages.sum(&:size) < Ably::Rest::Channel::MAX_MESSAGE_SIZE).to eq(true)
81
+
78
82
  expect(client).to receive(:post).once.and_call_original
79
83
  expect(channel.publish(messages)).to eql(true)
80
84
  expect(channel.history.items.map(&:name)).to match_array(messages.map(&:name))
@@ -350,6 +354,15 @@ describe Ably::Rest::Channel do
350
354
  end
351
355
  end
352
356
  end
357
+
358
+ context 'message size is exceeded (#TO3l8)' do
359
+ let(:data) { 101.times.map { { data: 'x' * 655 } } }
360
+
361
+ it 'should raise Ably::Exceptions::MaxMessageSizeExceeded exception' do
362
+ expect { channel.publish([ data: data ]) }.to \
363
+ raise_error(Ably::Exceptions::MaxMessageSizeExceeded)
364
+ end
365
+ end
353
366
  end
354
367
 
355
368
  describe '#history' do
@@ -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
@@ -308,30 +301,44 @@ describe Ably::Rest::Client do
308
301
  context 'configured' do
309
302
  let(:client_options) { default_options.merge(key: api_key, environment: 'production') }
310
303
 
311
- it 'should make connection attempts to A.ably-realtime.com, B.ably-realtime.com, C.ably-realtime.com, D.ably-realtime.com, E.ably-realtime.com (#RSC15a)' do
304
+ it 'should make connection attempts to a.ably-realtime.com, b.ably-realtime.com, c.ably-realtime.com, d.ably-realtime.com, e.ably-realtime.com (#RSC15a)' do
312
305
  hosts = []
313
306
  5.times do
314
307
  hosts << client.fallback_connection.host
315
308
  end
316
- expect(hosts).to match_array(%w(A.ably-realtime.com B.ably-realtime.com C.ably-realtime.com D.ably-realtime.com E.ably-realtime.com))
309
+ expect(hosts).to match_array(%w(a.ably-realtime.com b.ably-realtime.com c.ably-realtime.com d.ably-realtime.com e.ably-realtime.com))
317
310
  end
318
311
  end
319
312
 
320
313
  context 'when environment is NOT production (#RSC15b)' do
321
- let(:client_options) { default_options.merge(environment: 'sandbox', key: api_key) }
322
- let!(:default_host_request_stub) do
323
- stub_request(:post, "https://#{environment}-#{Ably::Rest::Client::DOMAIN}#{path}").to_return do
324
- raise Faraday::TimeoutError.new('timeout error message')
314
+ context 'and custom fallback hosts are empty' do
315
+ let(:client_options) { default_options.merge(environment: 'sandbox', key: api_key, fallback_hosts: []) }
316
+ let!(:default_host_request_stub) do
317
+ stub_request(:post, "https://#{environment}-#{Ably::Rest::Client::DOMAIN}#{path}").to_return do
318
+ raise Faraday::TimeoutError.new('timeout error message')
319
+ end
320
+ end
321
+
322
+ it 'does not retry failed requests with fallback hosts when there is a connection error' do
323
+ expect { publish_block.call }.to raise_error Ably::Exceptions::ConnectionTimeout
325
324
  end
326
325
  end
327
326
 
328
- it 'does not retry failed requests with fallback hosts when there is a connection error' do
329
- expect { publish_block.call }.to raise_error Ably::Exceptions::ConnectionTimeout
327
+ context 'and no custom fallback hosts are provided' do
328
+ let(:client_options) { default_options.merge(environment: 'sandbox', key: api_key) }
329
+
330
+ it 'should make connection attempts to sandbox-a-fallback.ably-realtime.com, sandbox-b-fallback.ably-realtime.com, sandbox-c-fallback.ably-realtime.com, sandbox-d-fallback.ably-realtime.com, sandbox-e-fallback.ably-realtime.com (#RSC15a)' do
331
+ hosts = []
332
+ 5.times do
333
+ hosts << client.fallback_connection.host
334
+ end
335
+ expect(hosts).to match_array(%w(a b c d e).map { |id| "sandbox-#{id}-fallback.ably-realtime.com" })
336
+ end
330
337
  end
331
338
  end
332
339
 
333
340
  context 'when environment is production' do
334
- let(:custom_hosts) { %w(A.ably-realtime.com B.ably-realtime.com) }
341
+ let(:custom_hosts) { %w(a.ably-realtime.com b.ably-realtime.com) }
335
342
  let(:max_retry_count) { 2 }
336
343
  let(:max_retry_duration) { 0.5 }
337
344
  let(:fallback_block) { proc { raise Faraday::SSLError.new('ssl error message') } }
@@ -830,11 +837,12 @@ describe Ably::Rest::Client do
830
837
  end
831
838
 
832
839
  context 'when environment is not production and server returns a 50x error' do
840
+ let(:env) { 'custom-env' }
841
+ let(:default_fallbacks) { %w(a b c d e).map { |id| "#{env}-#{id}-fallback.ably-realtime.com" } }
833
842
  let(:custom_hosts) { %w(A.foo.com B.foo.com) }
834
843
  let(:max_retry_count) { 2 }
835
844
  let(:max_retry_duration) { 0.5 }
836
845
  let(:fallback_block) { proc { raise Faraday::SSLError.new('ssl error message') } }
837
- let(:env) { 'custom-env' }
838
846
  let(:production_options) do
839
847
  default_options.merge(
840
848
  environment: env,
@@ -858,6 +866,26 @@ describe Ably::Rest::Client do
858
866
  stub_request(:post, "https://#{env}-#{Ably::Rest::Client::DOMAIN}#{path}").to_return(&fallback_block)
859
867
  end
860
868
 
869
+ context 'with no fallback hosts provided (#TBC, see https://github.com/ably/wiki/issues/361)' do
870
+ let(:client_options) {
871
+ production_options.merge(log_level: :fatal)
872
+ }
873
+
874
+ it 'uses the default fallback hosts for that environment as this is not an authentication failure' do
875
+ fallbacks_called_count = 0
876
+ default_fallbacks.each do |host|
877
+ counting_fallback_proc = proc do
878
+ fallbacks_called_count += 1
879
+ fallback_block.call
880
+ end
881
+ stub_request(:post, "https://#{host}#{path}").to_return(&counting_fallback_proc)
882
+ end
883
+ expect { publish_block.call }.to raise_error(Ably::Exceptions::ServerError)
884
+ expect(default_host_request_stub).to have_been_requested
885
+ expect(fallbacks_called_count).to be >= 2
886
+ end
887
+ end
888
+
861
889
  context 'with custom fallback hosts provided (#RSC15b, #TO3k6)' do
862
890
  let!(:first_fallback_request_stub) do
863
891
  stub_request(:post, "https://#{custom_hosts[0]}#{path}").to_return(&fallback_block)
@@ -1053,29 +1081,15 @@ describe Ably::Rest::Client do
1053
1081
  end
1054
1082
 
1055
1083
  context 'version headers', :webmock do
1056
- [nil, 'foo'].each do |variant|
1057
- context "with variant #{variant ? variant : 'none'}" do
1058
- if variant
1059
- before do
1060
- Ably.lib_variant = variant
1061
- end
1084
+ [nil, 'ably-ruby/1.1.1 ruby/1.9.3'].each do |agent|
1085
+ context "with #{agent ? "custom #{agent}" : 'default'} agent" do
1086
+ let(:client_options) { default_options.merge(key: api_key, agent: agent) }
1062
1087
 
1063
- after do
1064
- Ably.lib_variant = nil
1065
- end
1066
- end
1067
-
1068
- let(:client_options) { default_options.merge(key: api_key) }
1069
1088
  let!(:publish_message_stub) do
1070
- lib = ['ruby']
1071
- lib << variant if variant
1072
- lib << Ably::VERSION
1073
-
1074
-
1075
1089
  stub_request(:post, "#{client.endpoint}/channels/foo/publish").
1076
1090
  with(headers: {
1077
1091
  'X-Ably-Version' => Ably::PROTOCOL_VERSION,
1078
- 'X-Ably-Lib' => lib.join('-')
1092
+ 'Ably-Agent' => agent || Ably::AGENT
1079
1093
  }).
1080
1094
  to_return(status: 201, body: '{}', headers: { 'Content-Type' => 'application/json' })
1081
1095
  end
@@ -1089,8 +1103,10 @@ describe Ably::Rest::Client do
1089
1103
  end
1090
1104
  end
1091
1105
 
1092
- context '#request (#RSC19*)' do
1106
+ context '#request (#RSC19*, #TO3l9)' do
1093
1107
  let(:client_options) { default_options.merge(key: api_key) }
1108
+ let(:device_id) { random_str }
1109
+ let(:endpoint) { client.endpoint }
1094
1110
 
1095
1111
  context 'get' do
1096
1112
  it 'returns an HttpPaginatedResponse object' do
@@ -1130,13 +1146,96 @@ describe Ably::Rest::Client do
1130
1146
  end
1131
1147
  end
1132
1148
  end
1149
+
1150
+ context 'post', :webmock do
1151
+ before do
1152
+ stub_request(:delete, "#{endpoint}/push/deviceRegistrations/#{device_id}/resetUpdateToken").
1153
+ to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
1154
+ end
1155
+
1156
+ it 'supports post' do
1157
+ response = client.request(:delete, "push/deviceRegistrations/#{device_id}/resetUpdateToken")
1158
+
1159
+ expect(response).to be_success
1160
+ end
1161
+
1162
+ it 'raises an exception once body size in bytes exceeded' do
1163
+ expect {
1164
+ client.request(:post, endpoint, {}, { content: 'x' * Ably::Rest::Client::MAX_FRAME_SIZE })
1165
+ }.to raise_error(Ably::Exceptions::MaxFrameSizeExceeded)
1166
+ end
1167
+ end
1168
+
1169
+ context 'delete', :webmock do
1170
+ before do
1171
+ stub_request(:delete, "#{endpoint}/push/channelSubscriptions?deviceId=#{device_id}").
1172
+ to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
1173
+ end
1174
+
1175
+ it 'supports delete' do
1176
+ response = client.request(:delete, "/push/channelSubscriptions", { deviceId: device_id})
1177
+
1178
+ expect(response).to be_success
1179
+ end
1180
+ end
1181
+
1182
+ context 'patch', :webmock do
1183
+ let(:body_params) { { 'metadata' => { 'key' => 'value' } } }
1184
+
1185
+ before do
1186
+ stub_request(:patch, "#{endpoint}/push/deviceRegistrations/#{device_id}")
1187
+ .with(body: serialize_body(body_params, protocol))
1188
+ .to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
1189
+ end
1190
+
1191
+ it 'supports patch' do
1192
+ response = client.request(:patch, "/push/deviceRegistrations/#{device_id}", {}, body_params)
1193
+
1194
+ expect(response).to be_success
1195
+ end
1196
+
1197
+ it 'raises an exception once body size in bytes exceeded' do
1198
+ expect {
1199
+ client.request(:patch, endpoint, {}, { content: 'x' * Ably::Rest::Client::MAX_FRAME_SIZE })
1200
+ }.to raise_error(Ably::Exceptions::MaxFrameSizeExceeded)
1201
+ end
1202
+ end
1203
+
1204
+ context 'put', :webmock do
1205
+ let(:body_params) do
1206
+ {
1207
+ 'id' => random_str,
1208
+ 'platform' => 'ios',
1209
+ 'formFactor' => 'phone',
1210
+ 'metadata' => { 'key' => 'value' }
1211
+ }
1212
+ end
1213
+
1214
+ before do
1215
+ stub_request(:put, "#{endpoint}/push/deviceRegistrations/#{device_id}")
1216
+ .with(body: serialize_body(body_params, protocol))
1217
+ .to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
1218
+ end
1219
+
1220
+ it 'supports put' do
1221
+ response = client.request(:put, "/push/deviceRegistrations/#{device_id}", {}, body_params)
1222
+
1223
+ expect(response).to be_success
1224
+ end
1225
+
1226
+ it 'raises an exception once body size in bytes exceeded' do
1227
+ expect {
1228
+ client.request(:put, endpoint, {}, { content: 'x' * Ably::Rest::Client::MAX_FRAME_SIZE })
1229
+ }.to raise_error(Ably::Exceptions::MaxFrameSizeExceeded)
1230
+ end
1231
+ end
1133
1232
  end
1134
1233
 
1135
1234
  context 'request_id generation' do
1136
1235
  context 'Timeout error' do
1137
- context 'with option add_request_ids: true', :webmock, :prevent_log_stubbing do
1236
+ context 'with option add_request_ids: true and no fallback hosts', :webmock, :prevent_log_stubbing do
1138
1237
  let(:custom_logger_object) { TestLogger.new }
1139
- let(:client_options) { default_options.merge(key: api_key, logger: custom_logger_object, add_request_ids: true) }
1238
+ let(:client_options) { default_options.merge(key: api_key, logger: custom_logger_object, add_request_ids: true, fallback_hosts: []) }
1140
1239
 
1141
1240
  before do
1142
1241
  @request_id = nil
@@ -1226,8 +1325,8 @@ describe Ably::Rest::Client do
1226
1325
  end
1227
1326
  end
1228
1327
 
1229
- context 'without request_id' do
1230
- let(:client_options) { default_options.merge(key: api_key, http_request_timeout: 0) }
1328
+ context 'without request_id and no fallback hosts' do
1329
+ let(:client_options) { default_options.merge(key: api_key, http_request_timeout: 0, fallback_hosts: []) }
1231
1330
 
1232
1331
  it 'does not include request_id in ConnectionTimeout error' do
1233
1332
  begin