ably 1.0.5 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -1
  3. data/ably.gemspec +4 -3
  4. data/lib/ably/auth.rb +4 -4
  5. data/lib/ably/logger.rb +1 -1
  6. data/lib/ably/models/idiomatic_ruby_wrapper.rb +8 -8
  7. data/lib/ably/models/message.rb +6 -4
  8. data/lib/ably/models/presence_message.rb +6 -4
  9. data/lib/ably/modules/async_wrapper.rb +2 -2
  10. data/lib/ably/modules/conversions.rb +1 -1
  11. data/lib/ably/modules/encodeable.rb +1 -1
  12. data/lib/ably/modules/event_emitter.rb +2 -2
  13. data/lib/ably/modules/safe_deferrable.rb +1 -1
  14. data/lib/ably/modules/safe_yield.rb +1 -1
  15. data/lib/ably/modules/state_emitter.rb +5 -5
  16. data/lib/ably/realtime/auth.rb +1 -1
  17. data/lib/ably/realtime/channel.rb +3 -3
  18. data/lib/ably/realtime/channel/channel_manager.rb +2 -2
  19. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +3 -2
  20. data/lib/ably/realtime/connection.rb +11 -6
  21. data/lib/ably/realtime/connection/websocket_transport.rb +1 -1
  22. data/lib/ably/realtime/presence.rb +3 -3
  23. data/lib/ably/realtime/presence/members_map.rb +6 -6
  24. data/lib/ably/rest/channel.rb +2 -2
  25. data/lib/ably/rest/client.rb +20 -12
  26. data/lib/ably/version.rb +1 -1
  27. data/spec/acceptance/realtime/auth_spec.rb +13 -37
  28. data/spec/acceptance/realtime/channel_history_spec.rb +7 -1
  29. data/spec/acceptance/realtime/channel_spec.rb +3 -3
  30. data/spec/acceptance/realtime/client_spec.rb +2 -2
  31. data/spec/acceptance/realtime/connection_failures_spec.rb +221 -7
  32. data/spec/acceptance/realtime/connection_spec.rb +13 -21
  33. data/spec/acceptance/realtime/message_spec.rb +2 -2
  34. data/spec/acceptance/realtime/presence_history_spec.rb +12 -3
  35. data/spec/acceptance/realtime/presence_spec.rb +10 -10
  36. data/spec/acceptance/rest/auth_spec.rb +21 -48
  37. data/spec/acceptance/rest/client_spec.rb +193 -68
  38. data/spec/shared/client_initializer_behaviour.rb +1 -9
  39. data/spec/spec_helper.rb +2 -0
  40. data/spec/support/event_emitter_helper.rb +31 -0
  41. data/spec/support/event_machine_helper.rb +1 -1
  42. data/spec/support/test_logger_helper.rb +42 -0
  43. data/spec/unit/logger_spec.rb +1 -9
  44. data/spec/unit/modules/async_wrapper_spec.rb +2 -2
  45. data/spec/unit/modules/event_emitter_spec.rb +3 -3
  46. data/spec/unit/modules/state_emitter_spec.rb +10 -10
  47. data/spec/unit/realtime/channel_spec.rb +1 -1
  48. data/spec/unit/realtime/connection_spec.rb +1 -1
  49. data/spec/unit/realtime/presence_spec.rb +1 -1
  50. data/spec/unit/rest/channel_spec.rb +22 -0
  51. data/spec/unit/util/pub_sub_spec.rb +3 -3
  52. metadata +26 -8
@@ -65,10 +65,10 @@ describe Ably::Rest::Client do
65
65
  end
66
66
  end
67
67
 
68
- context 'with an :auth_callback Proc' do
69
- let(:client) { Ably::Rest::Client.new(client_options.merge(auth_callback: Proc.new { token_request })) }
68
+ context 'with an :auth_callback lambda' do
69
+ let(:client) { Ably::Rest::Client.new(client_options.merge(auth_callback: lambda { |token_params| token_request })) }
70
70
 
71
- it 'calls the auth Proc to get a new token' do
71
+ it 'calls the auth lambda to get a new token' do
72
72
  expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token_details }
73
73
  expect(client.auth.current_token_details.client_id).to eql(client_id)
74
74
  end
@@ -93,8 +93,8 @@ describe Ably::Rest::Client do
93
93
  end
94
94
  end
95
95
 
96
- context 'with an :auth_callback Proc (clientId provided in library options instead of as a token_request param)' do
97
- let(:client) { Ably::Rest::Client.new(client_options.merge(client_id: client_id, auth_callback: Proc.new { token_request })) }
96
+ context 'with an :auth_callback lambda (clientId provided in library options instead of as a token_request param)' do
97
+ let(:client) { Ably::Rest::Client.new(client_options.merge(client_id: client_id, auth_callback: lambda { |token_params| token_request })) }
98
98
  let(:token_request) { client.auth.create_token_request({}, key_name: key_name, key_secret: key_secret) }
99
99
 
100
100
  it 'correctly sets the clientId on the token' do
@@ -178,7 +178,7 @@ describe Ably::Rest::Client do
178
178
 
179
179
  context 'using tokens' do
180
180
  let(:client) do
181
- Ably::Rest::Client.new(client_options.merge(auth_callback: Proc.new do
181
+ Ably::Rest::Client.new(client_options.merge(auth_callback: lambda do |token_params|
182
182
  @request_index ||= 0
183
183
  @request_index += 1
184
184
  send("token_request_#{@request_index > 2 ? 'next' : @request_index}")
@@ -290,7 +290,7 @@ describe Ably::Rest::Client do
290
290
 
291
291
  context 'fallback hosts', :webmock do
292
292
  let(:path) { '/channels/test/publish' }
293
- let(:publish_block) { proc { client.channel('test').publish('event', 'data') } }
293
+ let(:publish_block) { lambda { client.channel('test').publish('event', 'data') } }
294
294
 
295
295
  context 'configured' do
296
296
  let(:client_options) { default_options.merge(key: api_key, environment: 'production') }
@@ -321,7 +321,7 @@ describe Ably::Rest::Client do
321
321
  let(:custom_hosts) { %w(A.ably-realtime.com B.ably-realtime.com) }
322
322
  let(:max_retry_count) { 2 }
323
323
  let(:max_retry_duration) { 0.5 }
324
- let(:fallback_block) { Proc.new { raise Faraday::SSLError.new('ssl error message') } }
324
+ let(:fallback_block) { proc { raise Faraday::SSLError.new('ssl error message') } }
325
325
  let(:client_options) do
326
326
  default_options.merge(
327
327
  environment: nil,
@@ -454,7 +454,7 @@ describe Ably::Rest::Client do
454
454
  context 'and server returns a 50x error' do
455
455
  let(:status) { 502 }
456
456
  let(:fallback_block) do
457
- Proc.new do
457
+ proc do
458
458
  {
459
459
  headers: { 'Content-Type' => 'text/html' },
460
460
  status: status
@@ -478,7 +478,7 @@ describe Ably::Rest::Client do
478
478
  let(:custom_hosts) { %w(A.foo.com B.foo.com) }
479
479
  let(:max_retry_count) { 2 }
480
480
  let(:max_retry_duration) { 0.5 }
481
- let(:fallback_block) { Proc.new { raise Faraday::SSLError.new('ssl error message') } }
481
+ let(:fallback_block) { proc { raise Faraday::SSLError.new('ssl error message') } }
482
482
  let(:production_options) do
483
483
  default_options.merge(
484
484
  environment: nil,
@@ -490,7 +490,7 @@ describe Ably::Rest::Client do
490
490
 
491
491
  let(:status) { 502 }
492
492
  let(:fallback_block) do
493
- Proc.new do
493
+ proc do
494
494
  {
495
495
  headers: { 'Content-Type' => 'text/html' },
496
496
  status: status
@@ -547,28 +547,36 @@ describe Ably::Rest::Client do
547
547
  context 'and timing out the primary host' do
548
548
  before do
549
549
  @web_server = WEBrick::HTTPServer.new(:Port => port, :SSLEnable => false, :AccessLog => [], Logger: WEBrick::Log.new("/dev/null"))
550
- @web_server.mount_proc "/channels/#{channel_name}/publish" do |req, res|
551
- if req.header["host"].first.include?(primary_host)
552
- @primary_host_requested = true
553
- sleep request_timeout + 0.5
554
- else
555
- @fallback_request_count ||= 0
556
- @fallback_request_count += 1
557
- if @fallback_request_count <= fail_fallback_request_count
550
+ request_handler = lambda do |result_body|
551
+ lambda do |req, res|
552
+ host = req.header["host"].first
553
+ if host.include?(primary_host)
554
+ @primary_host_request_count ||= 0
555
+ @primary_host_request_count += 1
558
556
  sleep request_timeout + 0.5
559
557
  else
560
- res.status = 200
561
- res['Content-Type'] = 'application/json'
562
- res.body = '{}'
558
+ @fallback_request_count ||= 0
559
+ @fallback_request_count += 1
560
+ @fallback_hosts_tried ||= []
561
+ @fallback_hosts_tried.push(host)
562
+ if @fallback_request_count <= fail_fallback_request_count
563
+ sleep request_timeout + 0.5
564
+ else
565
+ res.status = 200
566
+ res['Content-Type'] = 'application/json'
567
+ res.body = result_body
568
+ end
563
569
  end
564
570
  end
565
571
  end
572
+ @web_server.mount_proc "/time", &request_handler.call('[1000000000000]')
573
+ @web_server.mount_proc "/channels/#{channel_name}/publish", &request_handler.call('{}')
566
574
  Thread.new do
567
575
  @web_server.start
568
576
  end
569
577
  end
570
578
 
571
- context 'with request timeout less than max_retry_duration' do
579
+ context 'POST with request timeout less than max_retry_duration' do
572
580
  let(:client_options) do
573
581
  default_options.merge(
574
582
  rest_host: primary_host,
@@ -577,20 +585,44 @@ describe Ably::Rest::Client do
577
585
  port: port,
578
586
  tls: false,
579
587
  http_request_timeout: request_timeout,
580
- max_retry_duration: request_timeout * 3,
588
+ http_max_retry_duration: request_timeout * 2.5,
581
589
  log_level: :error
582
590
  )
583
591
  end
584
592
  let(:fail_fallback_request_count) { 1 }
585
593
 
586
- it 'tries one of the fallback hosts (#RSC15d)' do
594
+ it 'tries the primary host, then both fallback hosts (#RSC15d)' do
587
595
  client.channel(channel_name).publish('event', 'data')
588
- expect(@primary_host_requested).to be_truthy
596
+ expect(@primary_host_request_count).to eql(1)
597
+ expect(@fallback_request_count).to eql(2)
598
+ expect(@fallback_hosts_tried.uniq.length).to eql(2)
599
+ end
600
+ end
601
+
602
+ context 'GET with request timeout less than max_retry_duration' do
603
+ let(:client_options) do
604
+ default_options.merge(
605
+ rest_host: primary_host,
606
+ fallback_hosts: fallbacks,
607
+ token: 'fake.token',
608
+ port: port,
609
+ tls: false,
610
+ http_request_timeout: request_timeout,
611
+ http_max_retry_duration: request_timeout * 2.5,
612
+ log_level: :error
613
+ )
614
+ end
615
+ let(:fail_fallback_request_count) { 1 }
616
+
617
+ it 'tries the primary host, then both fallback hosts (#RSC15d)' do
618
+ client.time
619
+ expect(@primary_host_request_count).to eql(1)
589
620
  expect(@fallback_request_count).to eql(2)
621
+ expect(@fallback_hosts_tried.uniq.length).to eql(2)
590
622
  end
591
623
  end
592
624
 
593
- context 'with request timeout less than max_retry_duration' do
625
+ context 'POST with request timeout more than max_retry_duration' do
594
626
  let(:client_options) do
595
627
  default_options.merge(
596
628
  rest_host: primary_host,
@@ -599,18 +631,41 @@ describe Ably::Rest::Client do
599
631
  port: port,
600
632
  tls: false,
601
633
  http_request_timeout: request_timeout,
602
- max_retry_duration: request_timeout / 2,
634
+ http_max_retry_duration: request_timeout / 2,
603
635
  log_level: :error
604
636
  )
605
637
  end
606
638
  let(:fail_fallback_request_count) { 0 }
607
639
 
608
- it 'tries one of the fallback hosts (#RSC15d)' do
609
- client.channel(channel_name).publish('event', 'data')
610
- expect(@primary_host_requested).to be_truthy
611
- expect(@fallback_request_count).to eql(1)
640
+ it 'does not try any fallback hosts (#RSC15d)' do
641
+ expect { client.channel(channel_name).publish('event', 'data') }.to raise_error Ably::Exceptions::ConnectionTimeout
642
+ expect(@primary_host_request_count).to eql(1)
643
+ expect(@fallback_request_count).to be_nil
644
+ end
645
+ end
646
+
647
+ context 'GET with request timeout more than max_retry_duration' do
648
+ let(:client_options) do
649
+ default_options.merge(
650
+ rest_host: primary_host,
651
+ fallback_hosts: fallbacks,
652
+ token: 'fake.token',
653
+ port: port,
654
+ tls: false,
655
+ http_request_timeout: request_timeout,
656
+ http_max_retry_duration: request_timeout / 2,
657
+ log_level: :error
658
+ )
659
+ end
660
+ let(:fail_fallback_request_count) { 0 }
661
+
662
+ it 'does not try any fallback hosts (#RSC15d)' do
663
+ expect { client.time }.to raise_error Ably::Exceptions::ConnectionTimeout
664
+ expect(@primary_host_request_count).to eql(1)
665
+ expect(@fallback_request_count).to be_nil
612
666
  end
613
667
  end
668
+
614
669
  end
615
670
 
616
671
  context 'and failing the primary host' do
@@ -662,7 +717,7 @@ describe Ably::Rest::Client do
662
717
  let(:custom_hosts) { %w(A.foo.com B.foo.com) }
663
718
  let(:max_retry_count) { 2 }
664
719
  let(:max_retry_duration) { 0.5 }
665
- let(:fallback_block) { Proc.new { raise Faraday::SSLError.new('ssl error message') } }
720
+ let(:fallback_block) { proc { raise Faraday::SSLError.new('ssl error message') } }
666
721
  let(:env) { 'custom-env' }
667
722
  let(:production_options) do
668
723
  default_options.merge(
@@ -675,7 +730,7 @@ describe Ably::Rest::Client do
675
730
 
676
731
  let(:status) { 502 }
677
732
  let(:fallback_block) do
678
- Proc.new do
733
+ proc do
679
734
  {
680
735
  headers: { 'Content-Type' => 'text/html' },
681
736
  status: status
@@ -962,36 +1017,10 @@ describe Ably::Rest::Client do
962
1017
 
963
1018
  context 'request_id generation' do
964
1019
  context 'Timeout error' do
965
- context 'with request_id', :webmock do
966
- let(:custom_logger) do
967
- Class.new do
968
- def initialize
969
- @messages = []
970
- end
971
-
972
- [:fatal, :error, :warn, :info, :debug].each do |severity|
973
- define_method severity do |message, &block|
974
- message_val = [message]
975
- message_val << block.call if block
976
-
977
- @messages << [severity, message_val.compact.join(' ')]
978
- end
979
- end
980
-
981
- def logs
982
- @messages
983
- end
984
-
985
- def level
986
- 1
987
- end
988
-
989
- def level=(new_level)
990
- end
991
- end
992
- end
993
- let(:custom_logger_object) { custom_logger.new }
1020
+ context 'with option add_request_ids: true', :webmock, :prevent_log_stubbing do
1021
+ let(:custom_logger_object) { TestLogger.new }
994
1022
  let(:client_options) { default_options.merge(key: api_key, logger: custom_logger_object, add_request_ids: true) }
1023
+
995
1024
  before do
996
1025
  @request_id = nil
997
1026
  stub_request(:get, Addressable::Template.new("#{client.endpoint}/time{?request_id}")).with do |request|
@@ -1000,15 +1029,64 @@ describe Ably::Rest::Client do
1000
1029
  raise Faraday::TimeoutError.new('timeout error message')
1001
1030
  end
1002
1031
  end
1032
+
1003
1033
  it 'has an error with the same request_id of the request' do
1004
- expect{ client.time }.to raise_error(Ably::Exceptions::ConnectionTimeout, /#{@request_id}/)
1034
+ expect { client.time }.to raise_error(Ably::Exceptions::ConnectionTimeout, /#{@request_id}/)
1035
+ expect(@request_id).to be_a(String)
1036
+ expect(@request_id).to_not be_empty
1005
1037
  expect(custom_logger_object.logs.find { |severity, message| message.match(/#{@request_id}/i)} ).to_not be_nil
1006
1038
  end
1007
1039
  end
1008
1040
 
1009
- context 'when specifying fallback hosts', :webmock do
1010
- let(:client_options) { { key: api_key, fallback_hosts_use_default: true, add_request_ids: true } }
1041
+ context 'with option add_request_ids: true and REST operations with a message body' do
1042
+ let(:client_options) { default_options.merge({ key: api_key, add_request_ids: true }) }
1043
+ let(:channel_name) { random_str }
1044
+ let(:channel) { client.channels.get(channel_name) }
1045
+
1046
+ context 'with mocks to inspect the params', :webmock do
1047
+ before do
1048
+ stub_request(:post, Addressable::Template.new("#{client.endpoint}/channels/#{channel_name}/publish{?request_id}")).
1049
+ with do |request|
1050
+ @request_id = request.uri.query_values['request_id']
1051
+ end.to_return(:status => 200, :body => [], :headers => { 'Content-Type' => 'application/json' })
1052
+ end
1053
+
1054
+ context 'with a single publish' do
1055
+ it 'succeeds and sends the request_id as a param' do
1056
+ channel.publish('name', { body: random_str })
1057
+ expect(@request_id.to_s).to_not be_empty
1058
+ end
1059
+ end
1060
+
1061
+ context 'with an array publish' do
1062
+ it 'succeeds and sends the request_id as a param' do
1063
+ channel.publish([{ body: random_str }, { body: random_str }])
1064
+ expect(@request_id.to_s).to_not be_empty
1065
+ end
1066
+ end
1067
+ end
1068
+
1069
+ context 'without mocks to ensure the requests are accepted' do
1070
+ context 'with a single publish' do
1071
+ it 'succeeds and sends the request_id as a param' do
1072
+ channel.publish('name', { body: random_str })
1073
+ expect(channel.history.items.length).to eql(1)
1074
+ end
1075
+ end
1076
+
1077
+ context 'with an array publish' do
1078
+ it 'succeeds and sends the request_id as a param' do
1079
+ channel.publish([{ body: random_str }, { body: random_str }])
1080
+ expect(channel.history.items.length).to eql(2)
1081
+ end
1082
+ end
1083
+ end
1084
+ end
1085
+
1086
+ context 'option add_request_ids: true and specified fallback hosts', :webmock do
1087
+ let(:client_options) { { key: api_key, fallback_hosts_use_default: true, add_request_ids: true, log_level: :error } }
1011
1088
  let(:requests) { [] }
1089
+
1012
1090
  before do
1013
1091
  @request_id = nil
1014
1092
  hosts = Ably::FALLBACK_HOSTS + ['rest.ably.io']
@@ -1021,8 +1099,11 @@ describe Ably::Rest::Client do
1021
1099
  end
1022
1100
  end
1023
1101
  end
1024
- it 'request_id is the same across retries' do
1102
+
1103
+ specify 'request_id is the same across retries' do
1025
1104
  expect{ client.time }.to raise_error(Ably::Exceptions::ConnectionTimeout, /#{@request_id}/)
1105
+ expect(@request_id).to be_a(String)
1106
+ expect(@request_id).to_not be_empty
1026
1107
  expect(requests.uniq.count).to eql(1)
1027
1108
  expect(requests.uniq.first).to eql(@request_id)
1028
1109
  end
@@ -1030,6 +1111,7 @@ describe Ably::Rest::Client do
1030
1111
 
1031
1112
  context 'without request_id' do
1032
1113
  let(:client_options) { default_options.merge(key: api_key, http_request_timeout: 0) }
1114
+
1033
1115
  it 'does not include request_id in ConnectionTimeout error' do
1034
1116
  begin
1035
1117
  client.stats
@@ -1038,10 +1120,11 @@ describe Ably::Rest::Client do
1038
1120
  end
1039
1121
  end
1040
1122
  end
1041
- end
1123
+ end
1042
1124
 
1043
1125
  context 'UnauthorizedRequest nonce error' do
1044
1126
  let(:token_params) { { nonce: "samenonce_#{protocol}", timestamp: Time.now.to_i } }
1127
+
1045
1128
  it 'includes request_id in UnauthorizedRequest error due to replayed nonce' do
1046
1129
  client1 = Ably::Rest::Client.new(default_options.merge(key: api_key))
1047
1130
  client2 = Ably::Rest::Client.new(default_options.merge(key: api_key, add_request_ids: true))
@@ -1054,5 +1137,47 @@ describe Ably::Rest::Client do
1054
1137
  end
1055
1138
  end
1056
1139
  end
1140
+
1141
+ context 'failed request logging', :prevent_log_stubbing do
1142
+ let(:custom_logger) { TestLogger.new }
1143
+ let(:client_options) { default_options.merge(key: api_key, logger: custom_logger) }
1144
+
1145
+ it 'is absent when requests do not fail' do
1146
+ client.time
1147
+ expect(custom_logger.logs(min_severity: :warn)).to be_empty
1148
+ end
1149
+
1150
+ context 'with the first request failing' do
1151
+ let(:client_options) do
1152
+ default_options.merge(
1153
+ rest_host: 'non.existent.domain.local',
1154
+ fallback_hosts: [[environment, Ably::Rest::Client::DOMAIN].join('-')],
1155
+ key: api_key,
1156
+ logger: custom_logger)
1157
+ end
1158
+
1159
+ it 'is present with success message when requests do not actually fail' do
1160
+ client.time
1161
+ expect(custom_logger.logs(min_severity: :warn).select { |severity, msg| msg.match(/Retry/) }.length).to eql(1)
1162
+ expect(custom_logger.logs(min_severity: :warn).select { |severity, msg| msg.match(/SUCCEEDED/) }.length).to eql(1)
1163
+ end
1164
+ end
1165
+
1166
+ context 'with all requests failing' do
1167
+ let(:client_options) do
1168
+ default_options.merge(
1169
+ rest_host: 'non.existent.domain.local',
1170
+ fallback_hosts: ['non2.existent.domain.local'],
1171
+ key: api_key,
1172
+ logger: custom_logger)
1173
+ end
1174
+
1175
+ it 'is present when all requests fail' do
1176
+ expect { client.time }.to raise_error(Ably::Exceptions::ConnectionError)
1177
+ expect(custom_logger.logs(min_severity: :warn).select { |severity, msg| msg.match(/Retry/) }.length).to be >= 2
1178
+ expect(custom_logger.logs(min_severity: :error).select { |severity, msg| msg.match(/FAILED/) }.length).to eql(1)
1179
+ end
1180
+ end
1181
+ end
1057
1182
  end
1058
1183
  end
@@ -261,15 +261,7 @@ shared_examples 'a client initializer' do
261
261
  end
262
262
 
263
263
  context 'with custom logger and log_level' do
264
- let(:custom_logger) do
265
- Class.new do
266
- extend Forwardable
267
- def initialize
268
- @logger = Logger.new(STDOUT)
269
- end
270
- def_delegators :@logger, :fatal, :error, :warn, :info, :debug, :level, :level=
271
- end
272
- end
264
+ let(:custom_logger) { TestLogger }
273
265
  let(:client_options) { default_options.merge(logger: custom_logger.new, log_level: Logger::DEBUG, auto_connect: false) }
274
266
 
275
267
  it 'uses the custom logger' do
@@ -15,9 +15,11 @@ require 'ably'
15
15
 
16
16
  require 'support/api_helper'
17
17
  require 'support/debug_failure_helper'
18
+ require 'support/event_emitter_helper'
18
19
  require 'support/private_api_formatter'
19
20
  require 'support/protocol_helper'
20
21
  require 'support/random_helper'
22
+ require 'support/test_logger_helper'
21
23
 
22
24
  require 'rspec_config'
23
25