ably 1.1.1 → 1.1.5

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check.yml +27 -0
  3. data/CHANGELOG.md +60 -2
  4. data/COPYRIGHT +1 -0
  5. data/LICENSE +172 -11
  6. data/MAINTAINERS.md +1 -0
  7. data/README.md +2 -14
  8. data/SPEC.md +1020 -922
  9. data/ably.gemspec +5 -5
  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/ably.rb +11 -1
  14. data/lib/ably/modules/state_machine.rb +1 -1
  15. data/lib/ably/realtime/channel.rb +7 -11
  16. data/lib/ably/realtime/channel/channel_manager.rb +2 -2
  17. data/lib/ably/realtime/channel/channel_properties.rb +24 -0
  18. data/lib/ably/realtime/client.rb +9 -0
  19. data/lib/ably/realtime/connection.rb +7 -4
  20. data/lib/ably/realtime/connection/connection_manager.rb +19 -1
  21. data/lib/ably/realtime/connection/websocket_transport.rb +67 -1
  22. data/lib/ably/realtime/presence.rb +0 -14
  23. data/lib/ably/rest/channel.rb +25 -17
  24. data/lib/ably/rest/client.rb +35 -17
  25. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +4 -1
  26. data/lib/ably/version.rb +1 -1
  27. data/spec/acceptance/realtime/auth_spec.rb +5 -4
  28. data/spec/acceptance/realtime/channel_spec.rb +21 -8
  29. data/spec/acceptance/realtime/client_spec.rb +80 -20
  30. data/spec/acceptance/realtime/connection_failures_spec.rb +90 -9
  31. data/spec/acceptance/realtime/connection_spec.rb +47 -19
  32. data/spec/acceptance/realtime/message_spec.rb +2 -4
  33. data/spec/acceptance/realtime/presence_history_spec.rb +0 -58
  34. data/spec/acceptance/realtime/presence_spec.rb +54 -0
  35. data/spec/acceptance/realtime/push_admin_spec.rb +43 -21
  36. data/spec/acceptance/rest/auth_spec.rb +6 -75
  37. data/spec/acceptance/rest/base_spec.rb +8 -4
  38. data/spec/acceptance/rest/channel_spec.rb +42 -4
  39. data/spec/acceptance/rest/client_spec.rb +121 -26
  40. data/spec/acceptance/rest/message_spec.rb +1 -2
  41. data/spec/acceptance/rest/push_admin_spec.rb +67 -27
  42. data/spec/shared/client_initializer_behaviour.rb +131 -8
  43. data/spec/spec_helper.rb +1 -0
  44. data/spec/support/debug_failure_helper.rb +9 -5
  45. data/spec/support/serialization_helper.rb +21 -0
  46. data/spec/support/test_app.rb +2 -2
  47. data/spec/unit/modules/enum_spec.rb +1 -1
  48. data/spec/unit/realtime/client_spec.rb +20 -7
  49. data/spec/unit/realtime/connection_spec.rb +1 -1
  50. metadata +22 -17
  51. data/.travis.yml +0 -19
@@ -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
@@ -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
@@ -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)
@@ -1091,6 +1119,8 @@ describe Ably::Rest::Client do
1091
1119
 
1092
1120
  context '#request (#RSC19*)' do
1093
1121
  let(:client_options) { default_options.merge(key: api_key) }
1122
+ let(:device_id) { random_str }
1123
+ let(:endpoint) { client.endpoint }
1094
1124
 
1095
1125
  context 'get' do
1096
1126
  it 'returns an HttpPaginatedResponse object' do
@@ -1130,13 +1160,78 @@ describe Ably::Rest::Client do
1130
1160
  end
1131
1161
  end
1132
1162
  end
1163
+
1164
+ context 'post', :webmock do
1165
+ before do
1166
+ stub_request(:delete, "#{endpoint}/push/deviceRegistrations/#{device_id}/resetUpdateToken").
1167
+ to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
1168
+ end
1169
+
1170
+ it 'supports post' do
1171
+ response = client.request(:delete, "push/deviceRegistrations/#{device_id}/resetUpdateToken")
1172
+
1173
+ expect(response).to be_success
1174
+ end
1175
+ end
1176
+
1177
+ context 'delete', :webmock do
1178
+ before do
1179
+ stub_request(:delete, "#{endpoint}/push/channelSubscriptions?deviceId=#{device_id}").
1180
+ to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
1181
+ end
1182
+
1183
+ it 'supports delete' do
1184
+ response = client.request(:delete, "/push/channelSubscriptions", { deviceId: device_id})
1185
+
1186
+ expect(response).to be_success
1187
+ end
1188
+ end
1189
+
1190
+ context 'patch', :webmock do
1191
+ let(:body_params) { { 'metadata' => { 'key' => 'value' } } }
1192
+
1193
+ before do
1194
+ stub_request(:patch, "#{endpoint}/push/deviceRegistrations/#{device_id}")
1195
+ .with(body: serialize_body(body_params, protocol))
1196
+ .to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
1197
+ end
1198
+
1199
+ it 'supports patch' do
1200
+ response = client.request(:patch, "/push/deviceRegistrations/#{device_id}", {}, body_params)
1201
+
1202
+ expect(response).to be_success
1203
+ end
1204
+ end
1205
+
1206
+ context 'put', :webmock do
1207
+ let(:body_params) do
1208
+ {
1209
+ 'id' => random_str,
1210
+ 'platform' => 'ios',
1211
+ 'formFactor' => 'phone',
1212
+ 'metadata' => { 'key' => 'value' }
1213
+ }
1214
+ end
1215
+
1216
+ before do
1217
+ stub_request(:put, "#{endpoint}/push/deviceRegistrations/#{device_id}")
1218
+ .with(body: serialize_body(body_params, protocol))
1219
+ .to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
1220
+ end
1221
+
1222
+ it 'supports put' do
1223
+ response = client.request(:put, "/push/deviceRegistrations/#{device_id}", {}, body_params)
1224
+
1225
+ expect(response).to be_success
1226
+ end
1227
+ end
1133
1228
  end
1134
1229
 
1135
1230
  context 'request_id generation' do
1136
1231
  context 'Timeout error' do
1137
- context 'with option add_request_ids: true', :webmock, :prevent_log_stubbing do
1232
+ context 'with option add_request_ids: true and no fallback hosts', :webmock, :prevent_log_stubbing do
1138
1233
  let(:custom_logger_object) { TestLogger.new }
1139
- let(:client_options) { default_options.merge(key: api_key, logger: custom_logger_object, add_request_ids: true) }
1234
+ let(:client_options) { default_options.merge(key: api_key, logger: custom_logger_object, add_request_ids: true, fallback_hosts: []) }
1140
1235
 
1141
1236
  before do
1142
1237
  @request_id = nil
@@ -1226,8 +1321,8 @@ describe Ably::Rest::Client do
1226
1321
  end
1227
1322
  end
1228
1323
 
1229
- context 'without request_id' do
1230
- let(:client_options) { default_options.merge(key: api_key, http_request_timeout: 0) }
1324
+ context 'without request_id and no fallback hosts' do
1325
+ let(:client_options) { default_options.merge(key: api_key, http_request_timeout: 0, fallback_hosts: []) }
1231
1326
 
1232
1327
  it 'does not include request_id in ConnectionTimeout error' do
1233
1328
  begin