ably 1.1.2 → 1.1.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check.yml +27 -0
  3. data/CHANGELOG.md +67 -0
  4. data/COPYRIGHT +1 -0
  5. data/LICENSE +172 -11
  6. data/MAINTAINERS.md +1 -0
  7. data/README.md +11 -21
  8. data/SPEC.md +1020 -922
  9. data/ably.gemspec +4 -4
  10. data/lib/ably/auth.rb +12 -2
  11. data/lib/ably/exceptions.rb +2 -2
  12. data/lib/ably/modules/ably.rb +11 -1
  13. data/lib/ably/realtime/channel.rb +7 -11
  14. data/lib/ably/realtime/channel/channel_manager.rb +2 -2
  15. data/lib/ably/realtime/channel/channel_properties.rb +24 -0
  16. data/lib/ably/realtime/client.rb +9 -0
  17. data/lib/ably/realtime/connection.rb +5 -4
  18. data/lib/ably/realtime/connection/websocket_transport.rb +67 -1
  19. data/lib/ably/realtime/presence.rb +0 -14
  20. data/lib/ably/rest/channel.rb +27 -19
  21. data/lib/ably/rest/client.rb +31 -15
  22. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +4 -1
  23. data/lib/ably/version.rb +1 -1
  24. data/spec/acceptance/realtime/auth_spec.rb +3 -3
  25. data/spec/acceptance/realtime/channel_spec.rb +10 -0
  26. data/spec/acceptance/realtime/client_spec.rb +72 -16
  27. data/spec/acceptance/realtime/connection_failures_spec.rb +26 -11
  28. data/spec/acceptance/realtime/connection_spec.rb +36 -17
  29. data/spec/acceptance/realtime/presence_history_spec.rb +0 -58
  30. data/spec/acceptance/realtime/presence_spec.rb +54 -0
  31. data/spec/acceptance/realtime/push_admin_spec.rb +3 -19
  32. data/spec/acceptance/rest/auth_spec.rb +6 -75
  33. data/spec/acceptance/rest/base_spec.rb +8 -4
  34. data/spec/acceptance/rest/channel_spec.rb +42 -4
  35. data/spec/acceptance/rest/client_spec.rb +121 -26
  36. data/spec/acceptance/rest/push_admin_spec.rb +3 -19
  37. data/spec/shared/client_initializer_behaviour.rb +131 -8
  38. data/spec/spec_helper.rb +1 -0
  39. data/spec/support/serialization_helper.rb +21 -0
  40. data/spec/support/test_app.rb +2 -2
  41. data/spec/unit/realtime/client_spec.rb +19 -6
  42. metadata +20 -15
  43. data/.travis.yml +0 -19
@@ -52,63 +52,5 @@ describe Ably::Realtime::Presence, 'history', :event_machine do
52
52
 
53
53
  presence_client_one.enter(data)
54
54
  end
55
-
56
- context 'with option until_attach: true' do
57
- let(:event) { random_str }
58
- let(:presence_data_before_attach) { random_str }
59
- let(:presence_data_after_attach) { random_str }
60
-
61
- it 'retrieves all presence messages before channel was attached' do
62
- presence_client_two.enter(presence_data_before_attach) do
63
- presence_client_one.enter(presence_data_after_attach) do
64
- presence_client_one.history(until_attach: true) do |presence_page|
65
- expect(presence_page.items.count).to eql(1)
66
- expect(presence_page.items.first.data).to eql(presence_data_before_attach)
67
- stop_reactor
68
- end
69
- end
70
- end
71
- end
72
-
73
- context 'and two pages of messages' do
74
- let(:wildcard_token) { lambda { |token_params| Ably::Rest::Client.new(default_options).auth.request_token(client_id: '*') } }
75
- let(:client_one) { auto_close Ably::Realtime::Client.new(default_options.merge(auth_callback: wildcard_token)) }
76
- let(:client_two) { auto_close Ably::Realtime::Client.new(default_options.merge(auth_callback: wildcard_token)) }
77
-
78
- # TODO: Remove retry logic when presence history regression fixed
79
- # https://github.com/ably/realtime/issues/1707
80
- #
81
- it 'retrieves two pages of messages before channel was attached', retry: 10, :retry_wait => 5 do
82
- when_all(*10.times.map { |i| presence_client_two.enter_client("client:#{i}", presence_data_before_attach) }) do
83
- when_all(*10.times.map { |i| presence_client_one.enter_client("client:#{i}", presence_data_after_attach) }) do
84
- presence_client_one.history(until_attach: true, limit: 5) do |presence_page|
85
- expect(presence_page.items.count).to eql(5)
86
- expect(presence_page.items.map(&:data).uniq.first).to eql(presence_data_before_attach)
87
-
88
- presence_page.next do |presence_next_page|
89
- expect(presence_next_page.items.count).to eql(5)
90
- expect(presence_next_page.items.map(&:data).uniq.first).to eql(presence_data_before_attach)
91
- if presence_next_page.has_next?
92
- presence_next_page.next do |last|
93
- expect(last.items.count).to eql(0)
94
- end
95
- else
96
- expect(presence_next_page).to be_last
97
- end
98
- stop_reactor
99
- end
100
- end
101
- end
102
- end
103
- end
104
- end
105
-
106
- it 'fails with an exception unless state is attached' do
107
- presence_client_one.history(until_attach: true).errback do |error|
108
- expect(error.message).to match(/not attached/)
109
- stop_reactor
110
- end
111
- end
112
- end
113
55
  end
114
56
  end
@@ -2449,6 +2449,60 @@ describe Ably::Realtime::Presence, :event_machine do
2449
2449
  end
2450
2450
  end
2451
2451
 
2452
+ describe '#RTP17b' do
2453
+ let(:leave_action) { Ably::Models::PresenceMessage::ACTION.Leave }
2454
+
2455
+ it 'updates presence members on leave' do
2456
+ presence_client_two.subscribe(:enter) do
2457
+ channel_anonymous_client.attach do
2458
+ channel_anonymous_client.presence.get do |members|
2459
+ presence_client_two.subscribe(:leave) do
2460
+ expect(presence_client_two.members.local_members).to be_empty
2461
+ stop_reactor
2462
+ end
2463
+
2464
+ leave_message = Ably::Models::PresenceMessage.new(
2465
+ 'id' => "#{client_two.connection.id}:#{presence_client_two.client_id}:1",
2466
+ 'clientId' => presence_client_two.client_id,
2467
+ 'connectionId' => client_two.connection.id,
2468
+ 'timestamp' => as_since_epoch(Time.now),
2469
+ 'action' => leave_action
2470
+ )
2471
+
2472
+ presence_client_two.__incoming_msgbus__.publish :presence, leave_message
2473
+ end
2474
+ end
2475
+ end
2476
+
2477
+ presence_client_two.enter
2478
+ end
2479
+
2480
+ it 'does no update presence members on fabricated leave' do
2481
+ presence_client_two.subscribe(:enter) do
2482
+ channel_anonymous_client.attach do
2483
+ channel_anonymous_client.presence.get do |members|
2484
+ presence_client_two.subscribe(:leave) do
2485
+ expect(presence_client_two.members.local_members).to_not be_empty
2486
+ stop_reactor
2487
+ end
2488
+
2489
+ fabricated_leave_message = Ably::Models::PresenceMessage.new(
2490
+ 'id' => "#{client_two.connection.id}:#{presence_client_two.client_id}:1",
2491
+ 'clientId' => presence_client_two.client_id,
2492
+ 'connectionId' => "fabricated:#{presence_client_two.client_id}:0",
2493
+ 'timestamp' => as_since_epoch(Time.now),
2494
+ 'action' => leave_action
2495
+ )
2496
+
2497
+ presence_client_two.__incoming_msgbus__.publish :presence, fabricated_leave_message
2498
+ end
2499
+ end
2500
+ end
2501
+
2502
+ presence_client_two.enter
2503
+ end
2504
+ end
2505
+
2452
2506
  context 'when a channel becomes attached again' do
2453
2507
  let(:attached_action) { Ably::Models::ProtocolMessage::ACTION.Attached.to_i }
2454
2508
  let(:sync_action) { Ably::Models::ProtocolMessage::ACTION.Sync.to_i }
@@ -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
@@ -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