lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb in ably-rest-0.8.2 vs lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb in ably-rest-0.8.3

- old
+ new

@@ -8,36 +8,134 @@ let(:client) { Ably::Rest::Client.new(client_options) } connection_retry = Ably::Rest::Client::CONNECTION_RETRY + def encode64(text) + Base64.encode64(text).gsub("\n", '') + end + context '#initialize' do let(:client_id) { random_str } let(:token_request) { client.auth.create_token_request(key_name: key_name, key_secret: key_secret, client_id: client_id) } - context 'with a :auth_callback Proc' do + context 'with only an API key' do + let(:client) { Ably::Rest::Client.new(client_options.merge(key: api_key)) } + + it 'uses basic authentication' do + expect(client.auth).to be_using_basic_auth + end + end + + context 'with an explicit string :token' do + let(:client) { Ably::Rest::Client.new(client_options.merge(token: random_str)) } + + it 'uses token authentication' do + expect(client.auth).to be_using_token_auth + end + end + + context 'with :use_token_auth set to true' do + let(:client) { Ably::Rest::Client.new(client_options.merge(key: api_key, use_token_auth: true)) } + + it 'uses token authentication' do + expect(client.auth).to be_using_token_auth + end + end + + context 'with a :client_id configured' do + let(:client) { Ably::Rest::Client.new(client_options.merge(key: api_key, client_id: random_str)) } + + it 'uses token authentication' do + expect(client.auth).to be_using_token_auth + end + end + + context 'with an :auth_callback Proc' do let(:client) { Ably::Rest::Client.new(client_options.merge(auth_callback: Proc.new { token_request })) } it 'calls the auth Proc 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 + expect(client.auth).to be_using_token_auth + end end context 'with an auth URL' do - let(:client_options) { default_options.merge(auth_url: token_request_url, auth_method: :get) } + let(:client_options) { default_options.merge(key: api_key, auth_url: token_request_url, auth_method: :get) } let(:token_request_url) { 'http://get.token.request.com/' } - before do - allow(client.auth).to receive(:token_request_from_auth_url).with(token_request_url, :auth_method => :get).and_return(token_request) + it 'uses token authentication' do + expect(client.auth).to be_using_token_auth end - it 'sends an HTTP request to the provided URL 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) + context 'before any REST request' do + before do + expect(client.auth).to receive(:token_request_from_auth_url).with(token_request_url, hash_including(:auth_method => :get)).once do + client.auth.create_token_request(token_params: { client_id: client_id }) + end + end + + it 'sends an HTTP request to the provided auth URL 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 end end + + context 'auth headers', webmock: true do + let(:channel_name) { random_str } + let(:history_params) { { 'direction' => 'backwards', 'limit' => 100 } } + let(:history_querystring) { history_params.map { |k, v| "#{k}=#{v}" }.join("&") } + + context 'with basic auth', webmock: true do + let(:client_options) { default_options.merge(key: api_key) } + + let!(:get_message_history_stub) do + stub_request(:get, "https://#{api_key}@#{environment}-#{Ably::Rest::Client::DOMAIN}/channels/#{channel_name}/messages?#{history_querystring}"). + to_return(body: [], headers: { 'Content-Type' => 'application/json' }) + end + + it 'sends the API key in authentication part of the secure URL (the Authorization: Basic header is not used with the Faraday HTTP library by default)' do + client.channel(channel_name).history history_params + expect(get_message_history_stub).to have_been_requested + end + end + + context 'with token auth', webmock: true do + let(:token_string) { random_str } + let(:client_options) { default_options.merge(token: token_string) } + + let!(:get_message_history_stub) do + stub_request(:get, "#{http_protocol}://#{environment}-#{Ably::Rest::Client::DOMAIN}/channels/#{channel_name}/messages?#{history_querystring}"). + with(headers: { 'Authorization' => "Bearer #{encode64(token_string)}" }). + to_return(body: [], headers: { 'Content-Type' => 'application/json' }) + end + + context 'without specifying protocol' do + let(:http_protocol) { 'https' } + + it 'sends the token string over HTTPS in the Authorization Bearer header with Base64 encoding' do + client.channel(channel_name).history history_params + expect(get_message_history_stub).to have_been_requested + end + end + + context 'when setting constructor ClientOption :tls to false' do + let(:client_options) { default_options.merge(token: token_string, tls: false) } + let(:http_protocol) { 'http' } + + it 'sends the token string over HTTP in the Authorization Bearer header with Base64 encoding' do + client.channel(channel_name).history history_params + expect(get_message_history_stub).to have_been_requested + end + end + end + end end context 'using tokens' do let(:client) do Ably::Rest::Client.new(client_options.merge(auth_callback: Proc.new do @@ -51,12 +149,17 @@ # If token expires against whilst runnig tests in a slower CI environment then use this token let(:token_request_next) { client.auth.create_token_request(token_request_options.merge(client_id: random_str)) } context 'when expired' do - let(:token_request_options) { { key_name: key_name, key_secret: key_secret, ttl: Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER } } + before do + # Ensure tokens issued expire immediately after issue + stub_const 'Ably::Auth::TOKEN_DEFAULTS', Ably::Auth::TOKEN_DEFAULTS.merge(renew_token_buffer: 0) + end + let(:token_request_options) { { key_name: key_name, key_secret: key_secret, token_params: { ttl: Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER } } } + it 'creates a new token automatically when the old token expires' 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(token_request_1.client_id) sleep 1 @@ -128,19 +231,20 @@ raise Faraday::TimeoutError.new('timeout error message') end end it 'does not retry failed requests with fallback hosts when there is a connection error' do - expect { publish_block.call }.to raise_error Ably::Exceptions::ConnectionTimeoutError + expect { publish_block.call }.to raise_error Ably::Exceptions::ConnectionTimeout end end context 'when environment is production' do let(:custom_hosts) { %w(A.ably-realtime.com B.ably-realtime.com) } let(:max_attempts) { 2 } let(:cumulative_timeout) { 0.5 } let(:client_options) { default_options.merge(environment: nil, key: api_key) } + let(:fallback_block) { Proc.new { raise Faraday::SSLError.new('ssl error message') } } before do stub_const 'Ably::FALLBACK_HOSTS', custom_hosts stub_const 'Ably::Rest::Client::CONNECTION_RETRY', { single_request_open_timeout: 4, @@ -149,19 +253,15 @@ max_retry_attempts: max_attempts } end let!(:first_fallback_request_stub) do - stub_request(:post, "https://#{api_key}@#{custom_hosts[0]}#{path}").to_return do - raise Faraday::SSLError.new('ssl error message') - end + stub_request(:post, "https://#{api_key}@#{custom_hosts[0]}#{path}").to_return(&fallback_block) end let!(:second_fallback_request_stub) do - stub_request(:post, "https://#{api_key}@#{custom_hosts[1]}#{path}").to_return do - raise Faraday::SSLError.new('ssl error message') - end + stub_request(:post, "https://#{api_key}@#{custom_hosts[1]}#{path}").to_return(&fallback_block) end context 'and connection times out' do let!(:default_host_request_stub) do stub_request(:post, "https://#{api_key}@#{Ably::Rest::Client::DOMAIN}#{path}").to_return do @@ -183,11 +283,11 @@ raise Faraday::TimeoutError.new('timeout error message') end end it 'makes no further attempts to any fallback hosts' do - expect { publish_block.call }.to raise_error Ably::Exceptions::ConnectionTimeoutError + expect { publish_block.call }.to raise_error Ably::Exceptions::ConnectionTimeout expect(default_host_request_stub).to have_been_requested expect(first_fallback_request_stub).to_not have_been_requested expect(second_fallback_request_stub).to_not have_been_requested end end @@ -205,10 +305,56 @@ expect(default_host_request_stub).to have_been_requested expect(first_fallback_request_stub).to have_been_requested expect(second_fallback_request_stub).to have_been_requested end end + + context 'and basic authentication fails' do + let(:status) { 401 } + let!(:default_host_request_stub) do + stub_request(:post, "https://#{api_key}@#{Ably::Rest::Client::DOMAIN}#{path}").to_return( + headers: { 'Content-Type' => 'application/json' }, + status: status, + body: { + "error" => { + "statusCode" => 401, + "code" => 40101, + "message" => "Invalid credentials" + } + }.to_json + ) + end + + it 'does not attempt the fallback hosts as this is an authentication failure' do + expect { publish_block.call }.to raise_error(Ably::Exceptions::InvalidRequest) + expect(default_host_request_stub).to have_been_requested + expect(first_fallback_request_stub).to_not have_been_requested + expect(second_fallback_request_stub).to_not have_been_requested + end + end + + context 'and server returns a 50x error' do + let(:status) { 502 } + let(:fallback_block) do + Proc.new do + { + headers: { 'Content-Type' => 'text/html' }, + status: status + } + end + end + let!(:default_host_request_stub) do + stub_request(:post, "https://#{api_key}@#{Ably::Rest::Client::DOMAIN}#{path}").to_return(&fallback_block) + end + + it 'attempts the fallback hosts as this is an authentication failure' do + expect { publish_block.call }.to raise_error(Ably::Exceptions::ServerError) + expect(default_host_request_stub).to have_been_requested + expect(first_fallback_request_stub).to have_been_requested + expect(second_fallback_request_stub).to have_been_requested + end + end end end context 'with a custom host' do let(:custom_host) { 'host.does.not.exist' } @@ -251,11 +397,11 @@ raise Faraday::TimeoutError.new('timeout error message') end end it 'fails immediately and raises a Faraday Error' do - expect { client.auth.request_token }.to raise_error Ably::Exceptions::ConnectionTimeoutError + expect { client.auth.request_token }.to raise_error Ably::Exceptions::ConnectionTimeout end context 'fallback hosts' do before do Ably::FALLBACK_HOSTS.each do |host| @@ -264,13 +410,29 @@ end end end specify 'are never used' do - expect { client.auth.request_token }.to raise_error Ably::Exceptions::ConnectionTimeoutError + expect { client.auth.request_token }.to raise_error Ably::Exceptions::ConnectionTimeout expect(custom_host_request_stub).to have_been_requested end end + end + end + + context '#auth' do + let(:dummy_auth_url) { 'http://dummy.url' } + let(:unique_ttl) { 1234 } + let(:client_options) { default_options.merge(auth_url: dummy_auth_url, ttl: unique_ttl) } + + + it 'is provides access to the Auth object' do + expect(client.auth).to be_kind_of(Ably::Auth) + end + + it 'configures the Auth object with all ClientOptions passed to client in the initializer' do + expect(client.auth.options[:ttl]).to eql(unique_ttl) + expect(client.auth.options[:auth_url]).to eql(dummy_auth_url) end end end end