ably 0.8.3 → 0.8.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -3
  3. data/lib/ably/auth.rb +1 -1
  4. data/lib/ably/exceptions.rb +3 -0
  5. data/lib/ably/models/channel_state_change.rb +41 -0
  6. data/lib/ably/models/connection_state_change.rb +43 -0
  7. data/lib/ably/models/message.rb +1 -1
  8. data/lib/ably/models/presence_message.rb +1 -1
  9. data/lib/ably/models/protocol_message.rb +2 -1
  10. data/lib/ably/modules/state_emitter.rb +4 -1
  11. data/lib/ably/modules/uses_state_machine.rb +28 -4
  12. data/lib/ably/realtime/channel.rb +11 -3
  13. data/lib/ably/realtime/channel/channel_manager.rb +24 -4
  14. data/lib/ably/realtime/channel/channel_state_machine.rb +20 -11
  15. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +4 -4
  16. data/lib/ably/realtime/connection.rb +1 -0
  17. data/lib/ably/realtime/connection/connection_manager.rb +33 -21
  18. data/lib/ably/realtime/connection/connection_state_machine.rb +24 -16
  19. data/lib/ably/rest/channel.rb +3 -2
  20. data/lib/ably/util/crypto.rb +15 -0
  21. data/lib/ably/version.rb +1 -1
  22. data/spec/acceptance/realtime/channel_spec.rb +155 -9
  23. data/spec/acceptance/realtime/client_spec.rb +2 -2
  24. data/spec/acceptance/realtime/connection_failures_spec.rb +8 -4
  25. data/spec/acceptance/realtime/connection_spec.rb +122 -11
  26. data/spec/acceptance/realtime/message_spec.rb +119 -3
  27. data/spec/acceptance/realtime/presence_spec.rb +34 -13
  28. data/spec/acceptance/rest/channel_spec.rb +9 -0
  29. data/spec/acceptance/rest/client_spec.rb +10 -0
  30. data/spec/unit/models/channel_state_change_spec.rb +44 -0
  31. data/spec/unit/models/connection_state_change_spec.rb +54 -0
  32. data/spec/unit/util/crypto_spec.rb +18 -0
  33. metadata +8 -2
@@ -31,8 +31,8 @@ describe Ably::Realtime::Client, :event_machine do
31
31
  it 'fails to connect because a private key cannot be sent over a non-secure connection' do
32
32
  connection.on(:connected) { raise 'Should not have connected' }
33
33
 
34
- connection.on(:failed) do |error|
35
- expect(error).to be_a(Ably::Exceptions::InsecureRequest)
34
+ connection.on(:failed) do |connection_state_change|
35
+ expect(connection_state_change.reason).to be_a(Ably::Exceptions::InsecureRequest)
36
36
  stop_reactor
37
37
  end
38
38
  end
@@ -24,7 +24,8 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
24
24
  let(:invalid_key) { 'not_an_app.invalid_key_name:invalid_key_value' }
25
25
 
26
26
  it 'enters the failed state and returns a not found error' do
27
- connection.on(:failed) do |error|
27
+ connection.on(:failed) do |connection_state_change|
28
+ error = connection_state_change.reason
28
29
  expect(connection.state).to eq(:failed)
29
30
  # TODO: Check error type is an InvalidToken exception
30
31
  expect(error.status).to eq(404)
@@ -38,7 +39,8 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
38
39
  let(:invalid_key) { "#{app_id}.invalid_key_name:invalid_key_value" }
39
40
 
40
41
  it 'enters the failed state and returns an authorization error' do
41
- connection.on(:failed) do |error|
42
+ connection.on(:failed) do |connection_state_change|
43
+ error = connection_state_change.reason
42
44
  expect(connection.state).to eq(:failed)
43
45
  # TODO: Check error type is a TokenNotFOund exception
44
46
  expect(error.status).to eq(401)
@@ -182,7 +184,8 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
182
184
  context '#error_reason' do
183
185
  [:disconnected, :suspended, :failed].each do |state|
184
186
  it "contains the error when state is #{state}" do
185
- connection.on(state) do |error|
187
+ connection.on(state) do |connection_state_change|
188
+ error = connection_state_change.reason
186
189
  expect(connection.error_reason).to eq(error)
187
190
  expect(connection.error_reason.code).to eql(80000)
188
191
  stop_reactor
@@ -488,7 +491,8 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
488
491
  when_all(*channels.map(&:attach)) do
489
492
  detached_channels = []
490
493
  channels.each do |channel|
491
- channel.on(:detached) do |error|
494
+ channel.on(:detached) do |channel_state_change|
495
+ error = channel_state_change.reason
492
496
  expect(error.message).to match(/Invalid connection key/i)
493
497
  detached_channels << channel
494
498
  next unless detached_channels.count == channel_count
@@ -127,8 +127,8 @@ describe Ably::Realtime::Connection, :event_machine do
127
127
  it 'renews the token on connect, and only makes one subsequent attempt to obtain a new token' do
128
128
  expect(client.rest_client.auth).to receive(:authorise).at_least(:twice).and_call_original
129
129
  connection.once(:disconnected) do
130
- connection.once(:failed) do |error|
131
- expect(error.code).to eql(40140) # token expired
130
+ connection.once(:failed) do |connection_state_change|
131
+ expect(connection_state_change.reason.code).to eql(40140) # token expired
132
132
  stop_reactor
133
133
  end
134
134
  end
@@ -167,8 +167,8 @@ describe Ably::Realtime::Connection, :event_machine do
167
167
 
168
168
  connection.once(:connected) do
169
169
  started_at = Time.now
170
- connection.once(:disconnected) do |error|
171
- expect(error.code).to eq(40140) # Token expired
170
+ connection.once(:disconnected) do |connection_state_change|
171
+ expect(connection_state_change.reason.code).to eq(40140) # Token expired
172
172
 
173
173
  # Token has expired, so now ensure it is not used again
174
174
  stub_const 'Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER', original_token_expiry_buffer
@@ -178,7 +178,6 @@ describe Ably::Realtime::Connection, :event_machine do
178
178
  expect(client.auth.current_token_details).to_not be_expired
179
179
  expect(Time.now - started_at >= ttl)
180
180
  expect(original_token).to be_expired
181
- expect(error.code).to eql(40140) # token expired
182
181
  stop_reactor
183
182
  end
184
183
  end
@@ -922,12 +921,12 @@ describe Ably::Realtime::Connection, :event_machine do
922
921
  expect(connection.__outgoing_message_queue__).to be_empty
923
922
  channel.publish 'test'
924
923
 
925
- EventMachine.add_timer(0.02) do
924
+ EventMachine.next_tick do
926
925
  expect(connection.__outgoing_message_queue__).to_not be_empty
927
926
  end
928
927
 
929
928
  connection.once(:connected) do
930
- EventMachine.add_timer(0.02) do
929
+ EventMachine.add_timer(0.1) do
931
930
  expect(connection.__outgoing_message_queue__).to be_empty
932
931
  stop_reactor
933
932
  end
@@ -940,6 +939,8 @@ describe Ably::Realtime::Connection, :event_machine do
940
939
  end
941
940
 
942
941
  context 'when connection enters the :suspended state' do
942
+ let(:client_options) { default_options.merge(:log_level => :fatal) }
943
+
943
944
  before do
944
945
  # Reconfigure client library retry periods so that client stays in suspended state
945
946
  stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
@@ -956,13 +957,21 @@ describe Ably::Realtime::Connection, :event_machine do
956
957
  stop_reactor
957
958
  end
958
959
 
960
+ close_connection_proc = Proc.new do
961
+ EventMachine.add_timer(0.001) do
962
+ if connection.transport.nil?
963
+ close_connection_proc.call
964
+ else
965
+ connection.transport.close_connection_after_writing
966
+ end
967
+ end
968
+ end
969
+
959
970
  # Keep disconnecting the websocket transport after it attempts reconnection
960
- connection.transport.close_connection_after_writing
961
971
  connection.on(:connecting) do
962
- EventMachine.add_timer(0.03) do
963
- connection.transport.close_connection_after_writing
964
- end
972
+ close_connection_proc.call
965
973
  end
974
+ close_connection_proc.call
966
975
  end
967
976
  end
968
977
  end
@@ -979,5 +988,107 @@ describe Ably::Realtime::Connection, :event_machine do
979
988
  end
980
989
  end
981
990
  end
991
+
992
+ context 'connection state change' do
993
+ it 'emits a ConnectionStateChange object' do
994
+ connection.on(:connected) do |connection_state_change|
995
+ expect(connection_state_change).to be_a(Ably::Models::ConnectionStateChange)
996
+ stop_reactor
997
+ end
998
+ end
999
+
1000
+ context 'ConnectionStateChange object' do
1001
+ it 'has current state' do
1002
+ connection.on(:connected) do |connection_state_change|
1003
+ expect(connection_state_change.current).to eq(:connected)
1004
+ stop_reactor
1005
+ end
1006
+ end
1007
+
1008
+ it 'has a previous state' do
1009
+ connection.on(:connected) do |connection_state_change|
1010
+ expect(connection_state_change.previous).to eq(:connecting)
1011
+ stop_reactor
1012
+ end
1013
+ end
1014
+
1015
+ it 'contains a private API protocol_message attribute that is used for special state change events', :api_private do
1016
+ connection.on(:connected) do |connection_state_change|
1017
+ expect(connection_state_change.protocol_message).to be_a(Ably::Models::ProtocolMessage)
1018
+ expect(connection_state_change.reason).to be_nil
1019
+ stop_reactor
1020
+ end
1021
+ end
1022
+
1023
+ it 'has an empty reason when there is no error' do
1024
+ connection.on(:closed) do |connection_state_change|
1025
+ expect(connection_state_change.reason).to be_nil
1026
+ stop_reactor
1027
+ end
1028
+ connection.connect do
1029
+ connection.close
1030
+ end
1031
+ end
1032
+
1033
+ context 'on failure' do
1034
+ let(:client_options) { default_options.merge(log_level: :none) }
1035
+
1036
+ it 'has a reason Error object when there is an error on the connection' do
1037
+ connection.on(:failed) do |connection_state_change|
1038
+ expect(connection_state_change.reason).to be_a(Ably::Exceptions::BaseAblyException)
1039
+ stop_reactor
1040
+ end
1041
+ connection.connect do
1042
+ error = Ably::Exceptions::ConnectionFailed.new('forced failure', 500, 50000)
1043
+ client.connection.manager.error_received_from_server error
1044
+ end
1045
+ end
1046
+ end
1047
+
1048
+ context 'retry_in' do
1049
+ let(:client_options) { default_options.merge(log_level: :debug) }
1050
+
1051
+ it 'is nil when a retry is not required' do
1052
+ connection.on(:connected) do |connection_state_change|
1053
+ expect(connection_state_change.retry_in).to be_nil
1054
+ stop_reactor
1055
+ end
1056
+ end
1057
+
1058
+ it 'is 0 when first attempt to connect fails' do
1059
+ connection.once(:connecting) do
1060
+ connection.once(:disconnected) do |connection_state_change|
1061
+ expect(connection_state_change.retry_in).to eql(0)
1062
+ stop_reactor
1063
+ end
1064
+ EventMachine.add_timer(0.001) { connection.transport.unbind }
1065
+ end
1066
+ end
1067
+
1068
+ it 'is 0 when an immediate reconnect will occur' do
1069
+ connection.once(:connected) do
1070
+ connection.once(:disconnected) do |connection_state_change|
1071
+ expect(connection_state_change.retry_in).to eql(0)
1072
+ stop_reactor
1073
+ end
1074
+ connection.transport.unbind
1075
+ end
1076
+ end
1077
+
1078
+ it 'contains the next retry period when an immediate reconnect will not occur' do
1079
+ connection.once(:connected) do
1080
+ connection.once(:connecting) do
1081
+ connection.once(:disconnected) do |connection_state_change|
1082
+ expect(connection_state_change.retry_in).to be > 0
1083
+ stop_reactor
1084
+ end
1085
+ EventMachine.add_timer(0.001) { connection.transport.unbind }
1086
+ end
1087
+ connection.transport.unbind
1088
+ end
1089
+ end
1090
+ end
1091
+ end
1092
+ end
982
1093
  end
983
1094
  end
@@ -181,10 +181,14 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
181
181
 
182
182
  context 'with :echo_messages option set to false' do
183
183
  let(:no_echo_client) do
184
- Ably::Realtime::Client.new(default_options.merge(echo_messages: false))
184
+ Ably::Realtime::Client.new(default_options.merge(echo_messages: false, log_level: :debug))
185
185
  end
186
186
  let(:no_echo_channel) { no_echo_client.channel(channel_name) }
187
187
 
188
+ let(:rest_client) do
189
+ Ably::Rest::Client.new(default_options.merge(log_level: :debug))
190
+ end
191
+
188
192
  it 'will not echo messages to the client but will still broadcast messages to other connected clients', em_timeout: 10 do
189
193
  channel.attach do |echo_channel|
190
194
  no_echo_channel.attach do
@@ -196,13 +200,27 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
196
200
 
197
201
  echo_channel.subscribe('test_event') do |message|
198
202
  expect(message.data).to eql(payload)
199
- EventMachine.add_timer(1) do
203
+ EventMachine.add_timer(1.5) do
200
204
  stop_reactor
201
205
  end
202
206
  end
203
207
  end
204
208
  end
205
209
  end
210
+
211
+ it 'will not echo messages to the client from other REST clients publishing using that connection_ID', em_timeout: 10 do
212
+ skip 'Waiting on realtime#285 to be resolved'
213
+ no_echo_channel.attach do
214
+ no_echo_channel.subscribe('test_event') do |message|
215
+ fail "Message should not have been echoed back"
216
+ end
217
+
218
+ rest_client.channel(channel_name).publish('test_event', nil, connection_id: no_echo_client.connection.id)
219
+ EventMachine.add_timer(1.5) do
220
+ stop_reactor
221
+ end
222
+ end
223
+ end
206
224
  end
207
225
  end
208
226
 
@@ -257,7 +275,7 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
257
275
 
258
276
  context 'without suitable publishing permissions' do
259
277
  let(:restricted_client) do
260
- Ably::Realtime::Client.new(options.merge(key: restricted_api_key, environment: environment, protocol: protocol))
278
+ Ably::Realtime::Client.new(options.merge(key: restricted_api_key, environment: environment, protocol: protocol, :log_level => :error))
261
279
  end
262
280
  let(:restricted_channel) { restricted_client.channel("cansubscribe:example") }
263
281
  let(:payload) { 'Test message without permission to publish' }
@@ -569,5 +587,103 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
569
587
  end
570
588
  end
571
589
  end
590
+
591
+ describe 'when message is published, the connection disconnects before the ACK is received, and the connection is resumed' do
592
+ let(:event_name) { random_str }
593
+ let(:message_state) { [] }
594
+ let(:connection) { client.connection }
595
+ let(:client_options) { default_options.merge(:log_level => :debug) }
596
+ let(:msgs_received) { [] }
597
+
598
+ it 'publishes the message again, later receives the ACK and only one message is ever received from Ably' do
599
+ on_reconnected = Proc.new do
600
+ expect(message_state).to be_empty
601
+ EventMachine.add_timer(2) do
602
+ expect(message_state).to contain_exactly(:delivered)
603
+ # TODO: Uncomment once issue realtime#42 is resolved
604
+ # expect(msgs_received.length).to eql(1)
605
+ stop_reactor
606
+ end
607
+ end
608
+
609
+ connection.once(:connected) do
610
+ connection.transport.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
611
+ if protocol_message.messages.find { |message| message.name == event_name }
612
+ EventMachine.add_timer(0.001) do
613
+ connection.transport.unbind # trigger failure
614
+ expect(message_state).to be_empty
615
+ connection.once :connected, &on_reconnected
616
+ end
617
+ end
618
+ end
619
+ end
620
+
621
+ channel.publish(event_name).tap do |deferrable|
622
+ deferrable.callback { message_state << :delivered }
623
+ deferrable.errback do
624
+ raise 'Message delivery should not fail'
625
+ end
626
+ end
627
+
628
+ channel.subscribe do |message|
629
+ msgs_received << message
630
+ end
631
+ end
632
+ end
633
+
634
+ describe 'when message is published, the connection disconnects before the ACK is received' do
635
+ let(:connection) { client.connection }
636
+ let(:event_name) { random_str }
637
+
638
+ describe 'the connection becomes suspended' do
639
+ let(:client_options) { default_options.merge(:log_level => :fatal) }
640
+
641
+ it 'calls the errback for all messages' do
642
+ connection.once(:connected) do
643
+ connection.transport.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
644
+ if protocol_message.messages.find { |message| message.name == event_name }
645
+ EventMachine.add_timer(0.001) do
646
+ connection.transition_state_machine :suspended
647
+ end
648
+ end
649
+ end
650
+ end
651
+
652
+ channel.publish(event_name).tap do |deferrable|
653
+ deferrable.callback do
654
+ raise 'Message delivery should not happen'
655
+ end
656
+ deferrable.errback do
657
+ stop_reactor
658
+ end
659
+ end
660
+ end
661
+ end
662
+
663
+ describe 'the connection becomes failed' do
664
+ let(:client_options) { default_options.merge(:log_level => :none) }
665
+
666
+ it 'calls the errback for all messages' do
667
+ connection.once(:connected) do
668
+ connection.transport.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
669
+ if protocol_message.messages.find { |message| message.name == event_name }
670
+ EventMachine.add_timer(0.001) do
671
+ connection.transition_state_machine :failed, reason: RuntimeError.new
672
+ end
673
+ end
674
+ end
675
+ end
676
+
677
+ channel.publish(event_name).tap do |deferrable|
678
+ deferrable.callback do
679
+ raise 'Message delivery should not happen'
680
+ end
681
+ deferrable.errback do
682
+ stop_reactor
683
+ end
684
+ end
685
+ end
686
+ end
687
+ end
572
688
  end
573
689
  end
@@ -40,18 +40,29 @@ describe Ably::Realtime::Presence, :event_machine do
40
40
  end
41
41
 
42
42
  unless expected_state == :left
43
- %w(detached failed).each do |state|
44
- it "raise an exception if the channel is #{state}" do
45
- setup_test(method_name, args, options) do
46
- channel_client_one.attach do
47
- channel_client_one.change_state state.to_sym
48
- expect { presence_client_one.public_send(method_name, args) }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.#{state}/i
43
+ it 'raise an exception if the channel is detached' do
44
+ setup_test(method_name, args, options) do
45
+ channel_client_one.attach do
46
+ channel_client_one.transition_state_machine :detaching
47
+ channel_client_one.once(:detached) do
48
+ expect { presence_client_one.public_send(method_name, args) }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.detached/i
49
49
  stop_reactor
50
50
  end
51
51
  end
52
52
  end
53
53
  end
54
54
 
55
+ it 'raise an exception if the channel is failed' do
56
+ setup_test(method_name, args, options) do
57
+ channel_client_one.attach do
58
+ channel_client_one.transition_state_machine :failed
59
+ expect(channel_client_one.state).to eq(:failed)
60
+ expect { presence_client_one.public_send(method_name, args) }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.failed/i
61
+ stop_reactor
62
+ end
63
+ end
64
+ end
65
+
55
66
  it 'implicitly attaches the channel' do
56
67
  expect(channel_client_one).to_not be_attached
57
68
  presence_client_one.public_send(method_name, args) do
@@ -1061,16 +1072,25 @@ describe Ably::Realtime::Presence, :event_machine do
1061
1072
  presence_client_one.get { raise 'Intentional exception' }
1062
1073
  end
1063
1074
 
1064
- %w(detached failed).each do |state|
1065
- it "raise an exception if the channel is #{state}" do
1066
- channel_client_one.attach do
1067
- channel_client_one.change_state state.to_sym
1068
- expect { presence_client_one.get }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.#{state}/i
1075
+ it 'raise an exception if the channel is detached' do
1076
+ channel_client_one.attach do
1077
+ channel_client_one.transition_state_machine :detaching
1078
+ channel_client_one.once(:detached) do
1079
+ expect { presence_client_one.get }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.detached/i
1069
1080
  stop_reactor
1070
1081
  end
1071
1082
  end
1072
1083
  end
1073
1084
 
1085
+ it 'raise an exception if the channel is failed' do
1086
+ channel_client_one.attach do
1087
+ channel_client_one.transition_state_machine :failed
1088
+ expect(channel_client_one.state).to eq(:failed)
1089
+ expect { presence_client_one.get }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.failed/i
1090
+ stop_reactor
1091
+ end
1092
+ end
1093
+
1074
1094
  context 'during a sync' do
1075
1095
  let(:pages) { 2 }
1076
1096
  let(:members_per_page) { 100 }
@@ -1120,8 +1140,8 @@ describe Ably::Realtime::Presence, :event_machine do
1120
1140
  if protocol_message.action == :sync
1121
1141
  # prevent any more SYNC messages coming through
1122
1142
  client_two.connection.transport.__incoming_protocol_msgbus__.unsubscribe
1123
- channel_client_two.change_state :detaching
1124
- channel_client_two.change_state :detached
1143
+ channel_client_two.transition_state_machine :detaching
1144
+ channel_client_two.transition_state_machine :detached
1125
1145
  end
1126
1146
  end
1127
1147
  end
@@ -1565,6 +1585,7 @@ describe Ably::Realtime::Presence, :event_machine do
1565
1585
  context 'connection failure mid-way through a large member sync' do
1566
1586
  let(:members_count) { 400 }
1567
1587
  let(:sync_pages_received) { [] }
1588
+ let(:client_options) { default_options.merge(log_level: :error) }
1568
1589
 
1569
1590
  it 'resumes the SYNC operation', em_timeout: 15 do
1570
1591
  when_all(*members_count.times.map do |index|