ably 0.6.2 → 0.7.0
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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.ruby-version.old +1 -0
- data/.travis.yml +0 -2
- data/Rakefile +22 -4
- data/SPEC.md +1676 -0
- data/ably.gemspec +1 -1
- data/lib/ably.rb +0 -8
- data/lib/ably/auth.rb +54 -46
- data/lib/ably/exceptions.rb +19 -5
- data/lib/ably/logger.rb +1 -1
- data/lib/ably/models/error_info.rb +1 -1
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +11 -9
- data/lib/ably/models/message.rb +15 -12
- data/lib/ably/models/message_encoders/base.rb +6 -5
- data/lib/ably/models/message_encoders/base64.rb +1 -0
- data/lib/ably/models/message_encoders/cipher.rb +6 -3
- data/lib/ably/models/message_encoders/json.rb +1 -0
- data/lib/ably/models/message_encoders/utf8.rb +2 -9
- data/lib/ably/models/nil_logger.rb +20 -0
- data/lib/ably/models/paginated_resource.rb +5 -2
- data/lib/ably/models/presence_message.rb +21 -12
- data/lib/ably/models/protocol_message.rb +22 -6
- data/lib/ably/modules/ably.rb +11 -0
- data/lib/ably/modules/async_wrapper.rb +2 -0
- data/lib/ably/modules/conversions.rb +23 -3
- data/lib/ably/modules/encodeable.rb +2 -1
- data/lib/ably/modules/enum.rb +2 -0
- data/lib/ably/modules/event_emitter.rb +7 -1
- data/lib/ably/modules/event_machine_helpers.rb +2 -0
- data/lib/ably/modules/http_helpers.rb +2 -0
- data/lib/ably/modules/model_common.rb +12 -2
- data/lib/ably/modules/state_emitter.rb +76 -0
- data/lib/ably/modules/state_machine.rb +53 -0
- data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
- data/lib/ably/modules/uses_state_machine.rb +74 -0
- data/lib/ably/realtime.rb +4 -2
- data/lib/ably/realtime/channel.rb +51 -58
- data/lib/ably/realtime/channel/channel_manager.rb +91 -0
- data/lib/ably/realtime/channel/channel_state_machine.rb +68 -0
- data/lib/ably/realtime/client.rb +70 -26
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +31 -13
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
- data/lib/ably/realtime/connection.rb +135 -92
- data/lib/ably/realtime/connection/connection_manager.rb +216 -33
- data/lib/ably/realtime/connection/connection_state_machine.rb +30 -73
- data/lib/ably/realtime/models/nil_channel.rb +10 -1
- data/lib/ably/realtime/presence.rb +336 -92
- data/lib/ably/rest.rb +2 -2
- data/lib/ably/rest/channel.rb +13 -4
- data/lib/ably/rest/client.rb +138 -38
- data/lib/ably/rest/middleware/logger.rb +24 -3
- data/lib/ably/rest/presence.rb +12 -7
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/channel_history_spec.rb +101 -85
- data/spec/acceptance/realtime/channel_spec.rb +461 -120
- data/spec/acceptance/realtime/client_spec.rb +119 -0
- data/spec/acceptance/realtime/connection_failures_spec.rb +499 -0
- data/spec/acceptance/realtime/connection_spec.rb +571 -97
- data/spec/acceptance/realtime/message_spec.rb +347 -333
- data/spec/acceptance/realtime/presence_history_spec.rb +35 -40
- data/spec/acceptance/realtime/presence_spec.rb +769 -239
- data/spec/acceptance/realtime/stats_spec.rb +14 -22
- data/spec/acceptance/realtime/time_spec.rb +16 -20
- data/spec/acceptance/rest/auth_spec.rb +425 -364
- data/spec/acceptance/rest/base_spec.rb +108 -176
- data/spec/acceptance/rest/channel_spec.rb +89 -89
- data/spec/acceptance/rest/channels_spec.rb +30 -32
- data/spec/acceptance/rest/client_spec.rb +273 -0
- data/spec/acceptance/rest/encoders_spec.rb +185 -0
- data/spec/acceptance/rest/message_spec.rb +186 -163
- data/spec/acceptance/rest/presence_spec.rb +150 -111
- data/spec/acceptance/rest/stats_spec.rb +45 -40
- data/spec/acceptance/rest/time_spec.rb +8 -10
- data/spec/rspec_config.rb +10 -1
- data/spec/shared/client_initializer_behaviour.rb +212 -0
- data/spec/{support/model_helper.rb → shared/model_behaviour.rb} +6 -6
- data/spec/{support/protocol_msgbus_helper.rb → shared/protocol_msgbus_behaviour.rb} +1 -1
- data/spec/spec_helper.rb +9 -0
- data/spec/support/api_helper.rb +11 -0
- data/spec/support/event_machine_helper.rb +101 -3
- data/spec/support/markdown_spec_formatter.rb +90 -0
- data/spec/support/private_api_formatter.rb +36 -0
- data/spec/support/protocol_helper.rb +32 -0
- data/spec/support/random_helper.rb +15 -0
- data/spec/support/test_app.rb +4 -0
- data/spec/unit/auth_spec.rb +68 -0
- data/spec/unit/logger_spec.rb +77 -66
- data/spec/unit/models/error_info_spec.rb +1 -1
- data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +2 -3
- data/spec/unit/models/message_encoders/base64_spec.rb +2 -2
- data/spec/unit/models/message_encoders/cipher_spec.rb +2 -2
- data/spec/unit/models/message_encoders/utf8_spec.rb +2 -46
- data/spec/unit/models/message_spec.rb +160 -15
- data/spec/unit/models/paginated_resource_spec.rb +29 -27
- data/spec/unit/models/presence_message_spec.rb +163 -20
- data/spec/unit/models/protocol_message_spec.rb +43 -8
- data/spec/unit/modules/async_wrapper_spec.rb +2 -3
- data/spec/unit/modules/conversions_spec.rb +1 -1
- data/spec/unit/modules/enum_spec.rb +2 -3
- data/spec/unit/modules/event_emitter_spec.rb +62 -5
- data/spec/unit/modules/state_emitter_spec.rb +283 -0
- data/spec/unit/realtime/channel_spec.rb +107 -2
- data/spec/unit/realtime/channels_spec.rb +1 -0
- data/spec/unit/realtime/client_spec.rb +8 -48
- data/spec/unit/realtime/connection_spec.rb +3 -3
- data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +2 -2
- data/spec/unit/realtime/presence_spec.rb +13 -4
- data/spec/unit/realtime/realtime_spec.rb +0 -11
- data/spec/unit/realtime/websocket_transport_spec.rb +2 -2
- data/spec/unit/rest/channel_spec.rb +109 -0
- data/spec/unit/rest/channels_spec.rb +4 -3
- data/spec/unit/rest/client_spec.rb +30 -125
- data/spec/unit/rest/rest_spec.rb +10 -0
- data/spec/unit/util/crypto_spec.rb +10 -5
- data/spec/unit/util/pub_sub_spec.rb +5 -5
- metadata +44 -12
- data/spec/integration/modules/state_emitter_spec.rb +0 -80
- data/spec/integration/rest/auth.rb +0 -9
@@ -0,0 +1,119 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Ably::Realtime::Client, :event_machine do
|
5
|
+
vary_by_protocol do
|
6
|
+
let(:default_options) do
|
7
|
+
{ api_key: api_key, environment: environment, protocol: protocol }
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:client_options) { default_options }
|
11
|
+
let(:connection) { subject.connection }
|
12
|
+
let(:auth_params) { subject.auth.auth_params }
|
13
|
+
|
14
|
+
subject { Ably::Realtime::Client.new(client_options) }
|
15
|
+
|
16
|
+
context 'initialization' do
|
17
|
+
context 'basic auth' do
|
18
|
+
it 'is enabled by default with a provided :api_key option' do
|
19
|
+
connection.on(:connected) do
|
20
|
+
expect(auth_params[:key_id]).to_not be_nil
|
21
|
+
expect(auth_params[:access_token]).to be_nil
|
22
|
+
expect(subject.auth.current_token).to be_nil
|
23
|
+
stop_reactor
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context ':tls option' do
|
28
|
+
context 'set to false to forec a plain-text connection' do
|
29
|
+
let(:client_options) { default_options.merge(tls: false, log_level: :none) }
|
30
|
+
|
31
|
+
it 'fails to connect because a private key cannot be sent over a non-secure connection' do
|
32
|
+
connection.on(:connected) { raise 'Should not have connected' }
|
33
|
+
|
34
|
+
connection.on(:failed) do |error|
|
35
|
+
expect(error).to be_a(Ably::Exceptions::InsecureRequestError)
|
36
|
+
stop_reactor
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'token auth' do
|
44
|
+
[true, false].each do |tls_enabled|
|
45
|
+
context "with TLS #{tls_enabled ? 'enabled' : 'disabled'}" do
|
46
|
+
let(:capability) { { :foo => ["publish"] } }
|
47
|
+
let(:token) { Ably::Realtime::Client.new(default_options).auth.request_token(capability: capability) }
|
48
|
+
let(:client_options) { default_options.merge(token_id: token.id) }
|
49
|
+
|
50
|
+
context 'and a pre-generated Token provided with the :token_id option' do
|
51
|
+
it 'connects using token auth' do
|
52
|
+
connection.on(:connected) do
|
53
|
+
expect(auth_params[:access_token]).to_not be_nil
|
54
|
+
expect(auth_params[:key_id]).to be_nil
|
55
|
+
expect(subject.auth.current_token).to be_nil
|
56
|
+
stop_reactor
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'with valid :api_key and :use_token_auth option set to true' do
|
62
|
+
let(:client_options) { default_options.merge(use_token_auth: true) }
|
63
|
+
|
64
|
+
it 'automatically authorises on connect and generates a token' do
|
65
|
+
connection.on(:connected) do
|
66
|
+
expect(subject.auth.current_token).to_not be_nil
|
67
|
+
expect(auth_params[:access_token]).to_not be_nil
|
68
|
+
stop_reactor
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'with client_id' do
|
74
|
+
let(:client_options) do
|
75
|
+
default_options.merge(client_id: random_str)
|
76
|
+
end
|
77
|
+
it 'connects using token auth' do
|
78
|
+
run_reactor do
|
79
|
+
connection.on(:connected) do
|
80
|
+
expect(connection.state).to eq(:connected)
|
81
|
+
expect(auth_params[:access_token]).to_not be_nil
|
82
|
+
expect(auth_params[:key_id]).to be_nil
|
83
|
+
stop_reactor
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'with token_request_block' do
|
92
|
+
let(:client_id) { random_str }
|
93
|
+
let(:auth) { subject.auth }
|
94
|
+
|
95
|
+
subject do
|
96
|
+
Ably::Realtime::Client.new(client_options) do
|
97
|
+
@block_called = true
|
98
|
+
auth.create_token_request(client_id: client_id)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'calls the block' do
|
103
|
+
connection.on(:connected) do
|
104
|
+
expect(@block_called).to eql(true)
|
105
|
+
stop_reactor
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'uses the token request when requesting a new token' do
|
110
|
+
connection.on(:connected) do
|
111
|
+
expect(auth.current_token.client_id).to eql(client_id)
|
112
|
+
stop_reactor
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,499 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Ably::Realtime::Connection, 'failures', :event_machine do
|
5
|
+
let(:connection) { client.connection }
|
6
|
+
|
7
|
+
vary_by_protocol do
|
8
|
+
let(:default_options) do
|
9
|
+
{ api_key: api_key, environment: environment, protocol: protocol }
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:client_options) { default_options }
|
13
|
+
let(:client) do
|
14
|
+
Ably::Realtime::Client.new(client_options)
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'authentication failure' do
|
18
|
+
let(:client_options) do
|
19
|
+
default_options.merge(api_key: invalid_key, log_level: :none)
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'when API key is invalid' do
|
23
|
+
context 'with invalid app part of the key' do
|
24
|
+
let(:invalid_key) { 'not_an_app.invalid_key_id:invalid_key_value' }
|
25
|
+
|
26
|
+
it 'enters the failed state and returns a not found error' do
|
27
|
+
connection.on(:failed) do |error|
|
28
|
+
expect(connection.state).to eq(:failed)
|
29
|
+
# TODO: Check error type is an InvalidToken exception
|
30
|
+
expect(error.status).to eq(404)
|
31
|
+
expect(error.code).to eq(40400) # not found
|
32
|
+
stop_reactor
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'with invalid key ID part of the key' do
|
38
|
+
let(:invalid_key) { "#{app_id}.invalid_key_id:invalid_key_value" }
|
39
|
+
|
40
|
+
it 'enters the failed state and returns an authorization error' do
|
41
|
+
connection.on(:failed) do |error|
|
42
|
+
expect(connection.state).to eq(:failed)
|
43
|
+
# TODO: Check error type is a TokenNotFOund exception
|
44
|
+
expect(error.status).to eq(401)
|
45
|
+
expect(error.code).to eq(40400) # not found
|
46
|
+
stop_reactor
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'automatic connection retry' do
|
54
|
+
let(:client_failure_options) { default_options.merge(log_level: :none) }
|
55
|
+
|
56
|
+
context 'with invalid WebSocket host' do
|
57
|
+
let(:retry_every_for_tests) { 0.2 }
|
58
|
+
let(:max_time_in_state_for_tests) { 0.6 }
|
59
|
+
|
60
|
+
before do
|
61
|
+
# Reconfigure client library retry periods and timeouts so that tests run quickly
|
62
|
+
stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
|
63
|
+
Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
|
64
|
+
disconnected: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
|
65
|
+
suspended: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
let(:expected_retry_attempts) { (max_time_in_state_for_tests / retry_every_for_tests).round }
|
70
|
+
let(:state_changes) { Hash.new { |hash, key| hash[key] = 0 } }
|
71
|
+
let(:timer) { Hash.new }
|
72
|
+
|
73
|
+
let(:client_options) do
|
74
|
+
client_failure_options.merge(realtime_host: 'non.existent.host')
|
75
|
+
end
|
76
|
+
|
77
|
+
def count_state_changes
|
78
|
+
EventMachine.next_tick do
|
79
|
+
%w(connecting disconnected failed suspended).each do |state|
|
80
|
+
connection.on(state.to_sym) { state_changes[state.to_sym] += 1 }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def start_timer
|
86
|
+
timer[:start] = Time.now
|
87
|
+
end
|
88
|
+
|
89
|
+
def time_passed
|
90
|
+
Time.now.to_f - timer[:start].to_f
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'when disconnected' do
|
94
|
+
it 'enters the suspended state after multiple attempts to connect' do
|
95
|
+
connection.on(:failed) { raise 'Connection should not have reached :failed state yet' }
|
96
|
+
|
97
|
+
count_state_changes && start_timer
|
98
|
+
|
99
|
+
connection.once(:suspended) do
|
100
|
+
expect(connection.state).to eq(:suspended)
|
101
|
+
|
102
|
+
expect(state_changes[:connecting]).to eql(expected_retry_attempts)
|
103
|
+
expect(state_changes[:disconnected]).to eql(expected_retry_attempts)
|
104
|
+
|
105
|
+
expect(time_passed).to be > max_time_in_state_for_tests
|
106
|
+
stop_reactor
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe '#close' do
|
111
|
+
it 'transitions connection state to :closed' do
|
112
|
+
connection.on(:connected) { raise 'Connection should not have reached :connected state' }
|
113
|
+
connection.on(:failed) { raise 'Connection should not have reached :failed state yet' }
|
114
|
+
|
115
|
+
connection.once(:disconnected) do
|
116
|
+
expect(connection.state).to eq(:disconnected)
|
117
|
+
|
118
|
+
connection.on(:closed) do
|
119
|
+
expect(connection.state).to eq(:closed)
|
120
|
+
stop_reactor
|
121
|
+
end
|
122
|
+
|
123
|
+
connection.close
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'when connection state is :suspended' do
|
130
|
+
it 'enters the failed state after multiple attempts' do
|
131
|
+
connection.on(:connected) { raise 'Connection should not have reached :connected state' }
|
132
|
+
|
133
|
+
connection.once(:suspended) do
|
134
|
+
count_state_changes && start_timer
|
135
|
+
|
136
|
+
connection.on(:failed) do
|
137
|
+
expect(connection.state).to eq(:failed)
|
138
|
+
|
139
|
+
expect(state_changes[:connecting]).to eql(expected_retry_attempts)
|
140
|
+
expect(state_changes[:suspended]).to eql(expected_retry_attempts)
|
141
|
+
expect(state_changes[:disconnected]).to eql(0)
|
142
|
+
|
143
|
+
expect(time_passed).to be > max_time_in_state_for_tests
|
144
|
+
stop_reactor
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe '#close' do
|
150
|
+
it 'transitions connection state to :closed' do
|
151
|
+
connection.on(:connected) { raise 'Connection should not have reached :connected state' }
|
152
|
+
|
153
|
+
connection.once(:suspended) do
|
154
|
+
expect(connection.state).to eq(:suspended)
|
155
|
+
|
156
|
+
connection.on(:closed) do
|
157
|
+
expect(connection.state).to eq(:closed)
|
158
|
+
stop_reactor
|
159
|
+
end
|
160
|
+
|
161
|
+
connection.close
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
context 'when connection state is :failed' do
|
168
|
+
describe '#close' do
|
169
|
+
it 'will not transition state to :close and raises a StateChangeError exception' do
|
170
|
+
connection.on(:connected) { raise 'Connection should not have reached :connected state' }
|
171
|
+
|
172
|
+
connection.once(:failed) do
|
173
|
+
expect(connection.state).to eq(:failed)
|
174
|
+
expect { connection.close }.to raise_error Ably::Exceptions::StateChangeError, /Unable to transition from failed => closing/
|
175
|
+
stop_reactor
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
context '#error_reason' do
|
182
|
+
[:disconnected, :suspended, :failed].each do |state|
|
183
|
+
it "contains the error when state is #{state}" do
|
184
|
+
connection.on(state) do |error|
|
185
|
+
expect(connection.error_reason).to eq(error)
|
186
|
+
expect(connection.error_reason.code).to eql(80000)
|
187
|
+
stop_reactor
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'is reset to nil when :connected' do
|
193
|
+
connection.once(:disconnected) do |error|
|
194
|
+
# stub the host so that the connection connects
|
195
|
+
allow(connection).to receive(:host).and_return(TestApp.instance.host)
|
196
|
+
connection.once(:connected) do
|
197
|
+
expect(connection.error_reason).to be_nil
|
198
|
+
stop_reactor
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'is reset to nil when :closed' do
|
204
|
+
connection.once(:disconnected) do |error|
|
205
|
+
connection.close do
|
206
|
+
expect(connection.error_reason).to be_nil
|
207
|
+
stop_reactor
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
describe '#connect' do
|
215
|
+
let(:timeouts) { Ably::Realtime::Connection::ConnectionManager::TIMEOUTS }
|
216
|
+
|
217
|
+
before do
|
218
|
+
stub_const 'Ably::Realtime::Connection::ConnectionManager::TIMEOUTS',
|
219
|
+
Ably::Realtime::Connection::ConnectionManager::TIMEOUTS.merge(open: 1.5)
|
220
|
+
|
221
|
+
connection.on(:connected) { raise "Connection should not open in this test as CONNECTED ProtocolMessage is never received" }
|
222
|
+
|
223
|
+
connection.once(:connecting) do
|
224
|
+
# don't process any incoming ProtocolMessages so the connection never opens
|
225
|
+
connection.__incoming_protocol_msgbus__.unsubscribe
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
context 'connection opening times out' do
|
230
|
+
it 'attempts to reconnect' do
|
231
|
+
started_at = Time.now
|
232
|
+
|
233
|
+
connection.once(:disconnected) do
|
234
|
+
expect(Time.now.to_f - started_at.to_f).to be > timeouts.fetch(:open)
|
235
|
+
connection.once(:connecting) do
|
236
|
+
stop_reactor
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
connection.connect
|
241
|
+
end
|
242
|
+
|
243
|
+
it 'calls the errback of the returned Deferrable object when first connection attempt fails' do
|
244
|
+
connection.connect.errback do |error|
|
245
|
+
expect(connection.state).to eq(:disconnected)
|
246
|
+
stop_reactor
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
context 'when retry intervals are stubbed to attempt reconnection quickly' do
|
251
|
+
let(:client_options) { client_failure_options }
|
252
|
+
|
253
|
+
before do
|
254
|
+
# Reconfigure client library retry periods and timeouts so that tests run quickly
|
255
|
+
stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
|
256
|
+
Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
|
257
|
+
disconnected: { retry_every: 0.1, max_time_in_state: 0.2 },
|
258
|
+
suspended: { retry_every: 0.1, max_time_in_state: 0.2 },
|
259
|
+
)
|
260
|
+
end
|
261
|
+
|
262
|
+
it 'never calls the provided success block', em_timeout: 10 do
|
263
|
+
connection.connect do
|
264
|
+
raise 'success block should not have been called'
|
265
|
+
end
|
266
|
+
|
267
|
+
connection.once(:failed) do
|
268
|
+
stop_reactor
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
context 'connection resume' do
|
277
|
+
let(:channel_name) { random_str }
|
278
|
+
let(:channel) { client.channel(channel_name) }
|
279
|
+
let(:publishing_client) do
|
280
|
+
Ably::Realtime::Client.new(client_options)
|
281
|
+
end
|
282
|
+
let(:publishing_client_channel) { publishing_client.channel(channel_name) }
|
283
|
+
let(:client_options) { default_options.merge(log_level: :none) }
|
284
|
+
|
285
|
+
def fail_if_suspended_or_failed
|
286
|
+
connection.on(:suspended) { raise 'Connection should not have reached :suspended state' }
|
287
|
+
connection.on(:failed) { raise 'Connection should not have reached :failed state' }
|
288
|
+
end
|
289
|
+
|
290
|
+
context 'when DISCONNECTED ProtocolMessage received from the server' do
|
291
|
+
it 'reconnects automatically' do
|
292
|
+
fail_if_suspended_or_failed
|
293
|
+
|
294
|
+
connection.once(:connected) do
|
295
|
+
connection.once(:disconnected) do
|
296
|
+
connection.once(:connected) do
|
297
|
+
state_history = connection.state_history.map { |transition| transition[:state].to_sym }
|
298
|
+
expect(state_history).to eql([:connecting, :connected, :disconnected, :connecting, :connected])
|
299
|
+
stop_reactor
|
300
|
+
end
|
301
|
+
end
|
302
|
+
protocol_message = Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Disconnected.to_i)
|
303
|
+
connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
context 'when websocket transport is closed' do
|
309
|
+
it 'reconnects automatically' do
|
310
|
+
fail_if_suspended_or_failed
|
311
|
+
|
312
|
+
connection.once(:connected) do
|
313
|
+
connection.once(:disconnected) do
|
314
|
+
connection.once(:connected) do
|
315
|
+
state_history = connection.state_history.map { |transition| transition[:state].to_sym }
|
316
|
+
expect(state_history).to eql([:connecting, :connected, :disconnected, :connecting, :connected])
|
317
|
+
stop_reactor
|
318
|
+
end
|
319
|
+
end
|
320
|
+
connection.transport.close_connection_after_writing
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
context 'after successfully reconnecting and resuming' do
|
326
|
+
it 'retains connection_id and connection_key' do
|
327
|
+
previous_connection_id = nil
|
328
|
+
previous_connection_key = nil
|
329
|
+
|
330
|
+
connection.once(:connected) do
|
331
|
+
previous_connection_id = connection.id
|
332
|
+
previous_connection_key = connection.key
|
333
|
+
connection.transport.close_connection_after_writing
|
334
|
+
|
335
|
+
connection.once(:connected) do
|
336
|
+
expect(connection.key).to eql(previous_connection_key)
|
337
|
+
expect(connection.id).to eql(previous_connection_id)
|
338
|
+
stop_reactor
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
it 'retains channel subscription state' do
|
344
|
+
messages_received = false
|
345
|
+
|
346
|
+
channel.subscribe('event') do |message|
|
347
|
+
expect(message.data).to eql('message')
|
348
|
+
stop_reactor
|
349
|
+
end
|
350
|
+
|
351
|
+
channel.attach do
|
352
|
+
publishing_client_channel.attach do
|
353
|
+
connection.transport.close_connection_after_writing
|
354
|
+
|
355
|
+
connection.once(:connected) do
|
356
|
+
publishing_client_channel.publish 'event', 'message'
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
context 'when messages were published whilst the client was disconnected' do
|
363
|
+
it 'receives the messages published whilst offline' do
|
364
|
+
messages_received = false
|
365
|
+
|
366
|
+
channel.subscribe('event') do |message|
|
367
|
+
expect(message.data).to eql('message')
|
368
|
+
messages_received = true
|
369
|
+
end
|
370
|
+
|
371
|
+
channel.attach do
|
372
|
+
publishing_client_channel.attach do
|
373
|
+
connection.transport.off # remove all event handlers that detect socket connection state has changed
|
374
|
+
connection.transport.close_connection_after_writing
|
375
|
+
|
376
|
+
publishing_client_channel.publish('event', 'message') do
|
377
|
+
EventMachine.add_timer(1) do
|
378
|
+
expect(messages_received).to eql(false)
|
379
|
+
# simulate connection dropped to re-establish web socket
|
380
|
+
connection.transition_state_machine :disconnected
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
# subsequent connection will receive message sent whilst disconnected
|
385
|
+
connection.once(:connected) do
|
386
|
+
EventMachine.add_timer(1) do
|
387
|
+
expect(messages_received).to eql(true)
|
388
|
+
stop_reactor
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
describe 'fallback host feature' do
|
399
|
+
let(:retry_every_for_tests) { 0.1 }
|
400
|
+
let(:max_time_in_state_for_tests) { 0.3 }
|
401
|
+
|
402
|
+
before do
|
403
|
+
# Reconfigure client library retry periods and timeouts so that tests run quickly
|
404
|
+
stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
|
405
|
+
Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
|
406
|
+
disconnected: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
|
407
|
+
suspended: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
|
408
|
+
)
|
409
|
+
end
|
410
|
+
|
411
|
+
let(:expected_retry_attempts) { (max_time_in_state_for_tests / retry_every_for_tests).round }
|
412
|
+
let(:retry_count_for_one_state) { 1 + expected_retry_attempts } # initial connect then disconnected
|
413
|
+
let(:retry_count_for_all_states) { 1 + expected_retry_attempts * 2 } # initial connection, disconnected & then suspended
|
414
|
+
|
415
|
+
context 'with custom realtime websocket host option' do
|
416
|
+
let(:expected_host) { 'this.host.does.not.exist' }
|
417
|
+
let(:client_options) { default_options.merge(realtime_host: expected_host, log_level: :none) }
|
418
|
+
|
419
|
+
it 'never uses a fallback host' do
|
420
|
+
expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
|
421
|
+
expect(host).to eql(expected_host)
|
422
|
+
raise EventMachine::ConnectionError
|
423
|
+
end
|
424
|
+
|
425
|
+
connection.on(:failed) do
|
426
|
+
stop_reactor
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
context 'with non-production environment' do
|
432
|
+
let(:environment) { 'sandbox' }
|
433
|
+
let(:expected_host) { "#{environment}-#{Ably::Realtime::Client::DOMAIN}" }
|
434
|
+
let(:client_options) { default_options.merge(environment: environment, log_level: :none) }
|
435
|
+
|
436
|
+
it 'never uses a fallback host' do
|
437
|
+
expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
|
438
|
+
expect(host).to eql(expected_host)
|
439
|
+
raise EventMachine::ConnectionError
|
440
|
+
end
|
441
|
+
|
442
|
+
connection.on(:failed) do
|
443
|
+
stop_reactor
|
444
|
+
end
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
context 'with production environment' do
|
449
|
+
let(:custom_hosts) { %w(A.ably-realtime.com B.ably-realtime.com) }
|
450
|
+
before do
|
451
|
+
stub_const 'Ably::FALLBACK_HOSTS', custom_hosts
|
452
|
+
end
|
453
|
+
|
454
|
+
let(:expected_host) { Ably::Realtime::Client::DOMAIN }
|
455
|
+
let(:client_options) { default_options.merge(environment: nil, log_level: :none) }
|
456
|
+
|
457
|
+
let(:fallback_hosts_used) { Array.new }
|
458
|
+
|
459
|
+
it 'uses a fallback host on every subsequent disconnected attempt until suspended' do
|
460
|
+
request = 0
|
461
|
+
expect(EventMachine).to receive(:connect).exactly(retry_count_for_one_state).times do |host|
|
462
|
+
if request == 0
|
463
|
+
expect(host).to eql(expected_host)
|
464
|
+
else
|
465
|
+
expect(custom_hosts).to include(host)
|
466
|
+
fallback_hosts_used << host
|
467
|
+
end
|
468
|
+
request += 1
|
469
|
+
raise EventMachine::ConnectionError
|
470
|
+
end
|
471
|
+
|
472
|
+
connection.on(:suspended) do
|
473
|
+
expect(fallback_hosts_used.uniq).to match_array(custom_hosts)
|
474
|
+
stop_reactor
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
it 'uses the primary host when suspended, and a fallback host on every subsequent suspended attempt' do
|
479
|
+
request = 0
|
480
|
+
expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
|
481
|
+
if request == 0 || request == expected_retry_attempts + 1
|
482
|
+
expect(host).to eql(expected_host)
|
483
|
+
else
|
484
|
+
expect(custom_hosts).to include(host)
|
485
|
+
fallback_hosts_used << host
|
486
|
+
end
|
487
|
+
request += 1
|
488
|
+
raise EventMachine::ConnectionError
|
489
|
+
end
|
490
|
+
|
491
|
+
connection.on(:failed) do
|
492
|
+
expect(fallback_hosts_used.uniq).to match_array(custom_hosts)
|
493
|
+
stop_reactor
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|