ably-rest 0.7.1 → 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (148) hide show
  1. checksums.yaml +13 -5
  2. data/.gitmodules +1 -1
  3. data/.rspec +1 -0
  4. data/.travis.yml +7 -3
  5. data/SPEC.md +495 -419
  6. data/ably-rest.gemspec +19 -5
  7. data/lib/ably-rest.rb +9 -1
  8. data/lib/submodules/ably-ruby/.gitignore +6 -0
  9. data/lib/submodules/ably-ruby/.rspec +1 -0
  10. data/lib/submodules/ably-ruby/.ruby-version.old +1 -0
  11. data/lib/submodules/ably-ruby/.travis.yml +10 -0
  12. data/lib/submodules/ably-ruby/Gemfile +4 -0
  13. data/lib/submodules/ably-ruby/LICENSE.txt +22 -0
  14. data/lib/submodules/ably-ruby/README.md +122 -0
  15. data/lib/submodules/ably-ruby/Rakefile +34 -0
  16. data/lib/submodules/ably-ruby/SPEC.md +1794 -0
  17. data/lib/submodules/ably-ruby/ably.gemspec +36 -0
  18. data/lib/submodules/ably-ruby/lib/ably.rb +12 -0
  19. data/lib/submodules/ably-ruby/lib/ably/auth.rb +438 -0
  20. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +69 -0
  21. data/lib/submodules/ably-ruby/lib/ably/logger.rb +102 -0
  22. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +37 -0
  23. data/lib/submodules/ably-ruby/lib/ably/models/idiomatic_ruby_wrapper.rb +223 -0
  24. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +132 -0
  25. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base.rb +108 -0
  26. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base64.rb +40 -0
  27. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/cipher.rb +83 -0
  28. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/json.rb +34 -0
  29. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/utf8.rb +26 -0
  30. data/lib/submodules/ably-ruby/lib/ably/models/nil_logger.rb +20 -0
  31. data/lib/submodules/ably-ruby/lib/ably/models/paginated_resource.rb +173 -0
  32. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +147 -0
  33. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +210 -0
  34. data/lib/submodules/ably-ruby/lib/ably/models/stat.rb +161 -0
  35. data/lib/submodules/ably-ruby/lib/ably/models/token.rb +74 -0
  36. data/lib/submodules/ably-ruby/lib/ably/modules/ably.rb +15 -0
  37. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +62 -0
  38. data/lib/submodules/ably-ruby/lib/ably/modules/channels_collection.rb +69 -0
  39. data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +100 -0
  40. data/lib/submodules/ably-ruby/lib/ably/modules/encodeable.rb +69 -0
  41. data/lib/submodules/ably-ruby/lib/ably/modules/enum.rb +202 -0
  42. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +128 -0
  43. data/lib/submodules/ably-ruby/lib/ably/modules/event_machine_helpers.rb +26 -0
  44. data/lib/submodules/ably-ruby/lib/ably/modules/http_helpers.rb +41 -0
  45. data/lib/submodules/ably-ruby/lib/ably/modules/message_pack.rb +14 -0
  46. data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +41 -0
  47. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +153 -0
  48. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +57 -0
  49. data/lib/submodules/ably-ruby/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  50. data/lib/submodules/ably-ruby/lib/ably/modules/uses_state_machine.rb +74 -0
  51. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +64 -0
  52. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +298 -0
  53. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +92 -0
  54. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +69 -0
  55. data/lib/submodules/ably-ruby/lib/ably/realtime/channels.rb +50 -0
  56. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +184 -0
  57. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +184 -0
  58. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +70 -0
  59. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +445 -0
  60. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +368 -0
  61. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +91 -0
  62. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +188 -0
  63. data/lib/submodules/ably-ruby/lib/ably/realtime/models/nil_channel.rb +30 -0
  64. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +564 -0
  65. data/lib/submodules/ably-ruby/lib/ably/rest.rb +43 -0
  66. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +104 -0
  67. data/lib/submodules/ably-ruby/lib/ably/rest/channels.rb +44 -0
  68. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +396 -0
  69. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/encoder.rb +49 -0
  70. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/exceptions.rb +41 -0
  71. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/external_exceptions.rb +24 -0
  72. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +17 -0
  73. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/logger.rb +58 -0
  74. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_json.rb +27 -0
  75. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_message_pack.rb +27 -0
  76. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +92 -0
  77. data/lib/submodules/ably-ruby/lib/ably/util/crypto.rb +105 -0
  78. data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +43 -0
  79. data/lib/submodules/ably-ruby/lib/ably/version.rb +3 -0
  80. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +154 -0
  81. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +558 -0
  82. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +119 -0
  83. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +575 -0
  84. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +785 -0
  85. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +457 -0
  86. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +55 -0
  87. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +1001 -0
  88. data/lib/submodules/ably-ruby/spec/acceptance/realtime/stats_spec.rb +23 -0
  89. data/lib/submodules/ably-ruby/spec/acceptance/realtime/time_spec.rb +27 -0
  90. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +564 -0
  91. data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +165 -0
  92. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +134 -0
  93. data/lib/submodules/ably-ruby/spec/acceptance/rest/channels_spec.rb +41 -0
  94. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +273 -0
  95. data/lib/submodules/ably-ruby/spec/acceptance/rest/encoders_spec.rb +185 -0
  96. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +247 -0
  97. data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +292 -0
  98. data/lib/submodules/ably-ruby/spec/acceptance/rest/stats_spec.rb +172 -0
  99. data/lib/submodules/ably-ruby/spec/acceptance/rest/time_spec.rb +15 -0
  100. data/lib/submodules/ably-ruby/spec/resources/crypto-data-128.json +56 -0
  101. data/lib/submodules/ably-ruby/spec/resources/crypto-data-256.json +56 -0
  102. data/lib/submodules/ably-ruby/spec/rspec_config.rb +57 -0
  103. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +212 -0
  104. data/lib/submodules/ably-ruby/spec/shared/model_behaviour.rb +86 -0
  105. data/lib/submodules/ably-ruby/spec/shared/protocol_msgbus_behaviour.rb +36 -0
  106. data/lib/submodules/ably-ruby/spec/spec_helper.rb +20 -0
  107. data/lib/submodules/ably-ruby/spec/support/api_helper.rb +60 -0
  108. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +104 -0
  109. data/lib/submodules/ably-ruby/spec/support/markdown_spec_formatter.rb +118 -0
  110. data/lib/submodules/ably-ruby/spec/support/private_api_formatter.rb +36 -0
  111. data/lib/submodules/ably-ruby/spec/support/protocol_helper.rb +32 -0
  112. data/lib/submodules/ably-ruby/spec/support/random_helper.rb +15 -0
  113. data/lib/submodules/ably-ruby/spec/support/rest_testapp_before_retry.rb +15 -0
  114. data/lib/submodules/ably-ruby/spec/support/test_app.rb +113 -0
  115. data/lib/submodules/ably-ruby/spec/unit/auth_spec.rb +68 -0
  116. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +146 -0
  117. data/lib/submodules/ably-ruby/spec/unit/models/error_info_spec.rb +18 -0
  118. data/lib/submodules/ably-ruby/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +349 -0
  119. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/base64_spec.rb +181 -0
  120. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/cipher_spec.rb +260 -0
  121. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/json_spec.rb +135 -0
  122. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/utf8_spec.rb +56 -0
  123. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +389 -0
  124. data/lib/submodules/ably-ruby/spec/unit/models/paginated_resource_spec.rb +288 -0
  125. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +386 -0
  126. data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +315 -0
  127. data/lib/submodules/ably-ruby/spec/unit/models/stat_spec.rb +113 -0
  128. data/lib/submodules/ably-ruby/spec/unit/models/token_spec.rb +86 -0
  129. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +124 -0
  130. data/lib/submodules/ably-ruby/spec/unit/modules/conversions_spec.rb +72 -0
  131. data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +272 -0
  132. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +184 -0
  133. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +283 -0
  134. data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +206 -0
  135. data/lib/submodules/ably-ruby/spec/unit/realtime/channels_spec.rb +81 -0
  136. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +30 -0
  137. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +33 -0
  138. data/lib/submodules/ably-ruby/spec/unit/realtime/incoming_message_dispatcher_spec.rb +36 -0
  139. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +111 -0
  140. data/lib/submodules/ably-ruby/spec/unit/realtime/realtime_spec.rb +9 -0
  141. data/lib/submodules/ably-ruby/spec/unit/realtime/websocket_transport_spec.rb +25 -0
  142. data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +109 -0
  143. data/lib/submodules/ably-ruby/spec/unit/rest/channels_spec.rb +79 -0
  144. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +53 -0
  145. data/lib/submodules/ably-ruby/spec/unit/rest/rest_spec.rb +10 -0
  146. data/lib/submodules/ably-ruby/spec/unit/util/crypto_spec.rb +87 -0
  147. data/lib/submodules/ably-ruby/spec/unit/util/pub_sub_spec.rb +86 -0
  148. metadata +182 -27
@@ -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,575 @@
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 if the max_time_in_state is set' 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(:determine_host).and_yield(TestApp.instance.realtime_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
+
397
+ context 'when failing to resume because the connection_key is not or no longer valid' do
398
+ def kill_connection_transport_and_prevent_valid_resume
399
+ connection.transport.close_connection_after_writing
400
+ connection.configure_new '0123456789abcdef', '0123456789abcdef', -1 # force the resume connection key to be invalid
401
+ end
402
+
403
+ it 'updates the connection_id and connection_key' do
404
+ connection.once(:connected) do
405
+ previous_connection_id = connection.id
406
+ previous_connection_key = connection.key
407
+
408
+ connection.once(:connected) do
409
+ expect(connection.key).to_not eql(previous_connection_key)
410
+ expect(connection.id).to_not eql(previous_connection_id)
411
+ stop_reactor
412
+ end
413
+
414
+ kill_connection_transport_and_prevent_valid_resume
415
+ end
416
+ end
417
+
418
+ it 'detaches all channels' do
419
+ channel_count = 10
420
+ channels = channel_count.times.map { |index| client.channel("channel-#{index}") }
421
+ when_all(*channels.map(&:attach)) do
422
+ detached_channels = []
423
+ channels.each do |channel|
424
+ channel.on(:detached) do
425
+ detached_channels << channel
426
+ next unless detached_channels.count == channel_count
427
+ expect(detached_channels.count).to eql(channel_count)
428
+ stop_reactor
429
+ end
430
+ end
431
+
432
+ kill_connection_transport_and_prevent_valid_resume
433
+ end
434
+ end
435
+
436
+ it 'emits an error on the channel and sets the error reason' do
437
+ client.channel(random_str).attach do |channel|
438
+ channel.on(:error) do |error|
439
+ expect(error.message).to match(/Invalid connection key/i)
440
+ expect(error.code).to eql(80008)
441
+ expect(channel.error_reason).to eql(error)
442
+ stop_reactor
443
+ end
444
+
445
+ kill_connection_transport_and_prevent_valid_resume
446
+ end
447
+ end
448
+ end
449
+ end
450
+
451
+ describe 'fallback host feature' do
452
+ let(:retry_every_for_tests) { 0.1 }
453
+ let(:max_time_in_state_for_tests) { 0.3 }
454
+
455
+ before do
456
+ # Reconfigure client library retry periods and timeouts so that tests run quickly
457
+ stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
458
+ Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
459
+ disconnected: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
460
+ suspended: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
461
+ )
462
+ end
463
+
464
+ let(:expected_retry_attempts) { (max_time_in_state_for_tests / retry_every_for_tests).round }
465
+ let(:retry_count_for_one_state) { 1 + expected_retry_attempts } # initial connect then disconnected
466
+ let(:retry_count_for_all_states) { 1 + expected_retry_attempts * 2 } # initial connection, disconnected & then suspended
467
+
468
+ context 'with custom realtime websocket host option' do
469
+ let(:expected_host) { 'this.host.does.not.exist' }
470
+ let(:client_options) { default_options.merge(realtime_host: expected_host, log_level: :none) }
471
+
472
+ it 'never uses a fallback host' do
473
+ expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
474
+ expect(host).to eql(expected_host)
475
+ raise EventMachine::ConnectionError
476
+ end
477
+
478
+ connection.on(:failed) do
479
+ stop_reactor
480
+ end
481
+ end
482
+ end
483
+
484
+ context 'with non-production environment' do
485
+ let(:environment) { 'sandbox' }
486
+ let(:expected_host) { "#{environment}-#{Ably::Realtime::Client::DOMAIN}" }
487
+ let(:client_options) { default_options.merge(environment: environment, log_level: :none) }
488
+
489
+ it 'never uses a fallback host' do
490
+ expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
491
+ expect(host).to eql(expected_host)
492
+ raise EventMachine::ConnectionError
493
+ end
494
+
495
+ connection.on(:failed) do
496
+ stop_reactor
497
+ end
498
+ end
499
+ end
500
+
501
+ context 'with production environment' do
502
+ let(:custom_hosts) { %w(A.ably-realtime.com B.ably-realtime.com) }
503
+ before do
504
+ stub_const 'Ably::FALLBACK_HOSTS', custom_hosts
505
+ end
506
+
507
+ let(:expected_host) { Ably::Realtime::Client::DOMAIN }
508
+ let(:client_options) { default_options.merge(environment: nil, log_level: :none) }
509
+
510
+ let(:fallback_hosts_used) { Array.new }
511
+
512
+ context 'when the Internet is down' do
513
+ before do
514
+ allow(connection).to receive(:internet_up?).and_yield(false)
515
+ end
516
+
517
+ it 'never uses a fallback host' do
518
+ expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
519
+ expect(host).to eql(expected_host)
520
+ raise EventMachine::ConnectionError
521
+ end
522
+
523
+ connection.on(:failed) do
524
+ stop_reactor
525
+ end
526
+ end
527
+ end
528
+
529
+ context 'when the Internet is up' do
530
+ before do
531
+ allow(connection).to receive(:internet_up?).and_yield(true)
532
+ end
533
+
534
+ it 'uses a fallback host on every subsequent disconnected attempt until suspended' do
535
+ request = 0
536
+ expect(EventMachine).to receive(:connect).exactly(retry_count_for_one_state).times do |host|
537
+ if request == 0
538
+ expect(host).to eql(expected_host)
539
+ else
540
+ expect(custom_hosts).to include(host)
541
+ fallback_hosts_used << host
542
+ end
543
+ request += 1
544
+ raise EventMachine::ConnectionError
545
+ end
546
+
547
+ connection.on(:suspended) do
548
+ expect(fallback_hosts_used.uniq).to match_array(custom_hosts)
549
+ stop_reactor
550
+ end
551
+ end
552
+
553
+ it 'uses the primary host when suspended, and a fallback host on every subsequent suspended attempt' do
554
+ request = 0
555
+ expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
556
+ if request == 0 || request == expected_retry_attempts + 1
557
+ expect(host).to eql(expected_host)
558
+ else
559
+ expect(custom_hosts).to include(host)
560
+ fallback_hosts_used << host
561
+ end
562
+ request += 1
563
+ raise EventMachine::ConnectionError
564
+ end
565
+
566
+ connection.on(:failed) do
567
+ expect(fallback_hosts_used.uniq).to match_array(custom_hosts)
568
+ stop_reactor
569
+ end
570
+ end
571
+ end
572
+ end
573
+ end
574
+ end
575
+ end