lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb in ably-rest-1.0.5 vs lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb in ably-rest-1.0.6

- old
+ new

@@ -63,14 +63,14 @@ it 'raises an exception' do expect { Ably::Rest::Client.new(client_options.merge(key: api_key, client_id: '*')) }.to raise_error ArgumentError end end - context 'with an :auth_callback Proc' do - let(:client) { Ably::Rest::Client.new(client_options.merge(auth_callback: Proc.new { token_request })) } + context 'with an :auth_callback lambda' do + let(:client) { Ably::Rest::Client.new(client_options.merge(auth_callback: lambda { |token_params| token_request })) } - it 'calls the auth Proc to get a new token' do + it 'calls the auth lambda to get a new token' do expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token_details } expect(client.auth.current_token_details.client_id).to eql(client_id) end it 'uses token authentication' do @@ -91,12 +91,12 @@ client.auth.authorize expect(client.auth.client_id).to eql('bob') end end - context 'with an :auth_callback Proc (clientId provided in library options instead of as a token_request param)' do - let(:client) { Ably::Rest::Client.new(client_options.merge(client_id: client_id, auth_callback: Proc.new { token_request })) } + context 'with an :auth_callback lambda (clientId provided in library options instead of as a token_request param)' do + let(:client) { Ably::Rest::Client.new(client_options.merge(client_id: client_id, auth_callback: lambda { |token_params| token_request })) } let(:token_request) { client.auth.create_token_request({}, key_name: key_name, key_secret: key_secret) } it 'correctly sets the clientId on the token' do expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token_details } expect(client.auth.current_token_details.client_id).to eql(client_id) @@ -176,11 +176,11 @@ end end context 'using tokens' do let(:client) do - Ably::Rest::Client.new(client_options.merge(auth_callback: Proc.new do + Ably::Rest::Client.new(client_options.merge(auth_callback: lambda do |token_params| @request_index ||= 0 @request_index += 1 send("token_request_#{@request_index > 2 ? 'next' : @request_index}") end)) end @@ -288,11 +288,11 @@ end end context 'fallback hosts', :webmock do let(:path) { '/channels/test/publish' } - let(:publish_block) { proc { client.channel('test').publish('event', 'data') } } + let(:publish_block) { lambda { client.channel('test').publish('event', 'data') } } context 'configured' do let(:client_options) { default_options.merge(key: api_key, environment: 'production') } it 'should make connection attempts to A.ably-realtime.com, B.ably-realtime.com, C.ably-realtime.com, D.ably-realtime.com, E.ably-realtime.com (#RSC15a)' do @@ -319,11 +319,11 @@ context 'when environment is production' do let(:custom_hosts) { %w(A.ably-realtime.com B.ably-realtime.com) } let(:max_retry_count) { 2 } let(:max_retry_duration) { 0.5 } - let(:fallback_block) { Proc.new { raise Faraday::SSLError.new('ssl error message') } } + let(:fallback_block) { proc { raise Faraday::SSLError.new('ssl error message') } } let(:client_options) do default_options.merge( environment: nil, key: api_key, http_max_retry_duration: max_retry_duration, @@ -452,11 +452,11 @@ end context 'and server returns a 50x error' do let(:status) { 502 } let(:fallback_block) do - Proc.new do + proc do { headers: { 'Content-Type' => 'text/html' }, status: status } end @@ -476,11 +476,11 @@ context 'when environment is production and server returns a 50x error' do let(:custom_hosts) { %w(A.foo.com B.foo.com) } let(:max_retry_count) { 2 } let(:max_retry_duration) { 0.5 } - let(:fallback_block) { Proc.new { raise Faraday::SSLError.new('ssl error message') } } + let(:fallback_block) { proc { raise Faraday::SSLError.new('ssl error message') } } let(:production_options) do default_options.merge( environment: nil, key: api_key, http_max_retry_duration: max_retry_duration, @@ -488,11 +488,11 @@ ) end let(:status) { 502 } let(:fallback_block) do - Proc.new do + proc do { headers: { 'Content-Type' => 'text/html' }, status: status } end @@ -545,74 +545,129 @@ end context 'and timing out the primary host' do before do @web_server = WEBrick::HTTPServer.new(:Port => port, :SSLEnable => false, :AccessLog => [], Logger: WEBrick::Log.new("/dev/null")) - @web_server.mount_proc "/channels/#{channel_name}/publish" do |req, res| - if req.header["host"].first.include?(primary_host) - @primary_host_requested = true - sleep request_timeout + 0.5 - else - @fallback_request_count ||= 0 - @fallback_request_count += 1 - if @fallback_request_count <= fail_fallback_request_count + request_handler = lambda do |result_body| + lambda do |req, res| + host = req.header["host"].first + if host.include?(primary_host) + @primary_host_request_count ||= 0 + @primary_host_request_count += 1 sleep request_timeout + 0.5 else - res.status = 200 - res['Content-Type'] = 'application/json' - res.body = '{}' + @fallback_request_count ||= 0 + @fallback_request_count += 1 + @fallback_hosts_tried ||= [] + @fallback_hosts_tried.push(host) + if @fallback_request_count <= fail_fallback_request_count + sleep request_timeout + 0.5 + else + res.status = 200 + res['Content-Type'] = 'application/json' + res.body = result_body + end end end end + @web_server.mount_proc "/time", &request_handler.call('[1000000000000]') + @web_server.mount_proc "/channels/#{channel_name}/publish", &request_handler.call('{}') Thread.new do @web_server.start end end - context 'with request timeout less than max_retry_duration' do + context 'POST with request timeout less than max_retry_duration' do let(:client_options) do default_options.merge( rest_host: primary_host, fallback_hosts: fallbacks, token: 'fake.token', port: port, tls: false, http_request_timeout: request_timeout, - max_retry_duration: request_timeout * 3, + http_max_retry_duration: request_timeout * 2.5, log_level: :error ) end let(:fail_fallback_request_count) { 1 } - it 'tries one of the fallback hosts (#RSC15d)' do + it 'tries the primary host, then both fallback hosts (#RSC15d)' do client.channel(channel_name).publish('event', 'data') - expect(@primary_host_requested).to be_truthy + expect(@primary_host_request_count).to eql(1) expect(@fallback_request_count).to eql(2) + expect(@fallback_hosts_tried.uniq.length).to eql(2) end end - context 'with request timeout less than max_retry_duration' do + context 'GET with request timeout less than max_retry_duration' do let(:client_options) do default_options.merge( rest_host: primary_host, fallback_hosts: fallbacks, token: 'fake.token', port: port, tls: false, http_request_timeout: request_timeout, - max_retry_duration: request_timeout / 2, + http_max_retry_duration: request_timeout * 2.5, log_level: :error ) end + let(:fail_fallback_request_count) { 1 } + + it 'tries the primary host, then both fallback hosts (#RSC15d)' do + client.time + expect(@primary_host_request_count).to eql(1) + expect(@fallback_request_count).to eql(2) + expect(@fallback_hosts_tried.uniq.length).to eql(2) + end + end + + context 'POST with request timeout more than max_retry_duration' do + let(:client_options) do + default_options.merge( + rest_host: primary_host, + fallback_hosts: fallbacks, + token: 'fake.token', + port: port, + tls: false, + http_request_timeout: request_timeout, + http_max_retry_duration: request_timeout / 2, + log_level: :error + ) + end let(:fail_fallback_request_count) { 0 } - it 'tries one of the fallback hosts (#RSC15d)' do - client.channel(channel_name).publish('event', 'data') - expect(@primary_host_requested).to be_truthy - expect(@fallback_request_count).to eql(1) + it 'does not try any fallback hosts (#RSC15d)' do + expect { client.channel(channel_name).publish('event', 'data') }.to raise_error Ably::Exceptions::ConnectionTimeout + expect(@primary_host_request_count).to eql(1) + expect(@fallback_request_count).to be_nil end end + + context 'GET with request timeout more than max_retry_duration' do + let(:client_options) do + default_options.merge( + rest_host: primary_host, + fallback_hosts: fallbacks, + token: 'fake.token', + port: port, + tls: false, + http_request_timeout: request_timeout, + http_max_retry_duration: request_timeout / 2, + log_level: :error + ) + end + let(:fail_fallback_request_count) { 0 } + + it 'does not try any fallback hosts (#RSC15d)' do + expect { client.time }.to raise_error Ably::Exceptions::ConnectionTimeout + expect(@primary_host_request_count).to eql(1) + expect(@fallback_request_count).to be_nil + end + end + end context 'and failing the primary host' do before do @web_server = WEBrick::HTTPServer.new(:Port => port, :SSLEnable => false, :AccessLog => [], Logger: WEBrick::Log.new("/dev/null")) @@ -660,11 +715,11 @@ context 'when environment is not production and server returns a 50x error' do let(:custom_hosts) { %w(A.foo.com B.foo.com) } let(:max_retry_count) { 2 } let(:max_retry_duration) { 0.5 } - let(:fallback_block) { Proc.new { raise Faraday::SSLError.new('ssl error message') } } + let(:fallback_block) { proc { raise Faraday::SSLError.new('ssl error message') } } let(:env) { 'custom-env' } let(:production_options) do default_options.merge( environment: env, key: api_key, @@ -673,11 +728,11 @@ ) end let(:status) { 502 } let(:fallback_block) do - Proc.new do + proc do { headers: { 'Content-Type' => 'text/html' }, status: status } end @@ -960,57 +1015,80 @@ end end context 'request_id generation' do context 'Timeout error' do - context 'with request_id', :webmock do - let(:custom_logger) do - Class.new do - def initialize - @messages = [] - end - - [:fatal, :error, :warn, :info, :debug].each do |severity| - define_method severity do |message, &block| - message_val = [message] - message_val << block.call if block - - @messages << [severity, message_val.compact.join(' ')] - end - end - - def logs - @messages - end - - def level - 1 - end - - def level=(new_level) - end - end - end - let(:custom_logger_object) { custom_logger.new } + context 'with option add_request_ids: true', :webmock, :prevent_log_stubbing do + let(:custom_logger_object) { TestLogger.new } let(:client_options) { default_options.merge(key: api_key, logger: custom_logger_object, add_request_ids: true) } + before do @request_id = nil stub_request(:get, Addressable::Template.new("#{client.endpoint}/time{?request_id}")).with do |request| @request_id = request.uri.query_values['request_id'] end.to_return do raise Faraday::TimeoutError.new('timeout error message') end end + it 'has an error with the same request_id of the request' do - expect{ client.time }.to raise_error(Ably::Exceptions::ConnectionTimeout, /#{@request_id}/) + expect { client.time }.to raise_error(Ably::Exceptions::ConnectionTimeout, /#{@request_id}/) + expect(@request_id).to be_a(String) + expect(@request_id).to_not be_empty expect(custom_logger_object.logs.find { |severity, message| message.match(/#{@request_id}/i)} ).to_not be_nil end end - context 'when specifying fallback hosts', :webmock do - let(:client_options) { { key: api_key, fallback_hosts_use_default: true, add_request_ids: true } } + context 'with option add_request_ids: true and REST operations with a message body' do + let(:client_options) { default_options.merge({ key: api_key, add_request_ids: true }) } + let(:channel_name) { random_str } + let(:channel) { client.channels.get(channel_name) } + + context 'with mocks to inspect the params', :webmock do + before do + stub_request(:post, Addressable::Template.new("#{client.endpoint}/channels/#{channel_name}/publish{?request_id}")). + with do |request| + @request_id = request.uri.query_values['request_id'] + end.to_return(:status => 200, :body => [], :headers => { 'Content-Type' => 'application/json' }) + end + + context 'with a single publish' do + it 'succeeds and sends the request_id as a param' do + channel.publish('name', { body: random_str }) + expect(@request_id.to_s).to_not be_empty + end + end + + context 'with an array publish' do + it 'succeeds and sends the request_id as a param' do + channel.publish([{ body: random_str }, { body: random_str }]) + expect(@request_id.to_s).to_not be_empty + end + end + end + + context 'without mocks to ensure the requests are accepted' do + context 'with a single publish' do + it 'succeeds and sends the request_id as a param' do + channel.publish('name', { body: random_str }) + expect(channel.history.items.length).to eql(1) + end + end + + context 'with an array publish' do + it 'succeeds and sends the request_id as a param' do + channel.publish([{ body: random_str }, { body: random_str }]) + expect(channel.history.items.length).to eql(2) + end + end + end + end + + context 'option add_request_ids: true and specified fallback hosts', :webmock do + let(:client_options) { { key: api_key, fallback_hosts_use_default: true, add_request_ids: true, log_level: :error } } let(:requests) { [] } + before do @request_id = nil hosts = Ably::FALLBACK_HOSTS + ['rest.ably.io'] hosts.each do |host| stub_request(:get, Addressable::Template.new("https://#{host.downcase}/time{?request_id}")).with do |request| @@ -1019,39 +1097,86 @@ end.to_return do raise Faraday::TimeoutError.new('timeout error message') end end end - it 'request_id is the same across retries' do + + specify 'request_id is the same across retries' do expect{ client.time }.to raise_error(Ably::Exceptions::ConnectionTimeout, /#{@request_id}/) + expect(@request_id).to be_a(String) + expect(@request_id).to_not be_empty expect(requests.uniq.count).to eql(1) expect(requests.uniq.first).to eql(@request_id) end end context 'without request_id' do let(:client_options) { default_options.merge(key: api_key, http_request_timeout: 0) } + it 'does not include request_id in ConnectionTimeout error' do begin client.stats rescue Ably::Exceptions::ConnectionTimeout => err expect(err.request_id).to eql(nil) end end end - end + end context 'UnauthorizedRequest nonce error' do let(:token_params) { { nonce: "samenonce_#{protocol}", timestamp: Time.now.to_i } } + it 'includes request_id in UnauthorizedRequest error due to replayed nonce' do client1 = Ably::Rest::Client.new(default_options.merge(key: api_key)) client2 = Ably::Rest::Client.new(default_options.merge(key: api_key, add_request_ids: true)) expect { client1.auth.request_token(token_params) }.not_to raise_error begin client2.auth.request_token(token_params) rescue Ably::Exceptions::UnauthorizedRequest => err expect(err.request_id).to_not eql(nil) end + end + end + end + + context 'failed request logging', :prevent_log_stubbing do + let(:custom_logger) { TestLogger.new } + let(:client_options) { default_options.merge(key: api_key, logger: custom_logger) } + + it 'is absent when requests do not fail' do + client.time + expect(custom_logger.logs(min_severity: :warn)).to be_empty + end + + context 'with the first request failing' do + let(:client_options) do + default_options.merge( + rest_host: 'non.existent.domain.local', + fallback_hosts: [[environment, Ably::Rest::Client::DOMAIN].join('-')], + key: api_key, + logger: custom_logger) + end + + it 'is present with success message when requests do not actually fail' do + client.time + expect(custom_logger.logs(min_severity: :warn).select { |severity, msg| msg.match(/Retry/) }.length).to eql(1) + expect(custom_logger.logs(min_severity: :warn).select { |severity, msg| msg.match(/SUCCEEDED/) }.length).to eql(1) + end + end + + context 'with all requests failing' do + let(:client_options) do + default_options.merge( + rest_host: 'non.existent.domain.local', + fallback_hosts: ['non2.existent.domain.local'], + key: api_key, + logger: custom_logger) + end + + it 'is present when all requests fail' do + expect { client.time }.to raise_error(Ably::Exceptions::ConnectionError) + expect(custom_logger.logs(min_severity: :warn).select { |severity, msg| msg.match(/Retry/) }.length).to be >= 2 + expect(custom_logger.logs(min_severity: :error).select { |severity, msg| msg.match(/FAILED/) }.length).to eql(1) end end end end end