spec/acceptance/rest/auth_spec.rb in ably-0.8.2 vs spec/acceptance/rest/auth_spec.rb in ably-0.8.3

- old
+ new

@@ -3,11 +3,15 @@ describe Ably::Auth do include Ably::Modules::Conversions def hmac_for(token_request_attributes, secret) - token_request= Ably::Models::IdiomaticRubyWrapper.new(token_request_attributes) + token_request = if token_request_attributes.kind_of?(Ably::Models::IdiomaticRubyWrapper) + token_request_attributes + else + Ably::Models::IdiomaticRubyWrapper.new(token_request_attributes) + end text = [ :key_name, :ttl, :capability, @@ -20,12 +24,14 @@ OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, secret, text) ) end vary_by_protocol do + let(:default_options) { { environment: environment, protocol: protocol } } + let(:client_options) { default_options.merge(key: api_key) } let(:client) do - Ably::Rest::Client.new(key: api_key, environment: environment, protocol: protocol) + Ably::Rest::Client.new(client_options) end let(:auth) { client.auth } let(:content_type) do if protocol == :msgpack 'application/x-msgpack' @@ -43,13 +49,13 @@ body[convert_to_mixed_case(key)].to_s == val.to_s end def serialize(object, protocol) if protocol == :msgpack - MessagePack.pack(token_response) + MessagePack.pack(object) else - JSON.dump(token_response) + JSON.dump(object) end end it 'has immutable options' do expect { auth.options['key_name'] = 'new_name' }.to raise_error RuntimeError, /can't modify frozen.*Hash/ @@ -58,47 +64,57 @@ describe '#request_token' do let(:ttl) { 30 * 60 } let(:capability) { { :foo => ['publish'] } } let(:token_details) do - auth.request_token( + auth.request_token(token_params: { ttl: ttl, capability: capability - ) + }) end - it 'returns a valid requested token in the expected format with valid issued and expires attributes' do + it 'creates a TokenRequest automatically and sends it to Ably to obtain a token', webmock: true do + token_request_stub = stub_request(:post, "#{client.endpoint}/keys/#{key_name}/requestToken"). + to_return(status: 201, body: serialize({}, protocol), headers: { 'Content-Type' => content_type }) + expect(auth).to receive(:create_token_request).and_call_original + auth.request_token + + expect(token_request_stub).to have_been_requested + end + + it 'returns a valid TokenDetails object in the expected format with valid issued and expires attributes' do + expect(token_details).to be_a(Ably::Models::TokenDetails) expect(token_details.token).to match(/^#{app_id}\.[\w-]+$/) expect(token_details.key_name).to match(/^#{key_name}$/) expect(token_details.issued).to be_within(2).of(Time.now) expect(token_details.expires).to be_within(2).of(Time.now + ttl) end - %w(client_id capability nonce timestamp ttl).each do |option| - context "with option :#{option}", :webmock do - def coerce_if_time_value(field_name, value, options = {}) - multiply = options[:multiply] + %w(client_id capability nonce timestamp ttl).each do |token_param| + context "with token_param :#{token_param}", :webmock do + def coerce_if_time_value(field_name, value, params = {}) + multiply = params[:multiply] return value unless %w(timestamp ttl).include?(field_name) value.to_i * (multiply ? multiply : 1) end - let(:random) { coerce_if_time_value(option, random_int_str) } - let(:options) { { option.to_sym => random } } + let(:random) { coerce_if_time_value(token_param, random_int_str) } + let(:token_params) { { token_param.to_sym => random } } let(:token_response) { {} } let!(:request_token_stub) do stub_request(:post, "#{client.endpoint}/keys/#{key_name}/requestToken"). with do |request| - request_body_includes(request, protocol, option, coerce_if_time_value(option, random, multiply: 1000)) + request_body_includes(request, protocol, token_param, coerce_if_time_value(token_param, random, multiply: 1000)) end.to_return( :status => 201, :body => serialize(token_response, protocol), :headers => { 'Content-Type' => content_type } ) end - before { auth.request_token options } + before { auth.request_token token_params: token_params } it "overrides default and uses camelCase notation for attributes" do expect(request_token_stub).to have_been_requested end end @@ -106,12 +122,13 @@ context 'with :key option', :webmock do let(:key_name) { "app.#{random_str}" } let(:key_secret) { random_str } let(:nonce) { random_str } - let(:token_options) { { key: "#{key_name}:#{key_secret}", nonce: nonce, timestamp: Time.now } } - let(:token_request) { auth.create_token_request(token_options) } + let(:auth_options) { { key: "#{key_name}:#{key_secret}" } } + let(:token_params) { { nonce: nonce, timestamp: Time.now } } + let(:token_request) { auth.create_token_request(auth_options, token_params) } let(:mac) do hmac_for(token_request, key_secret) end let(:token_response) { {} } @@ -123,11 +140,11 @@ :status => 201, :body => serialize(token_response, protocol), :headers => { 'Content-Type' => content_type }) end - let!(:token) { puts token_options; auth.request_token(token_options) } + let!(:token) { auth.request_token(auth_options, token_params) } specify 'key_name is used in request and signing uses key_secret' do expect(request_token_stub).to have_been_requested end end @@ -135,12 +152,13 @@ context 'with :key_name & :key_secret options', :webmock do let(:key_name) { "app.#{random_str}" } let(:key_secret) { random_str } let(:nonce) { random_str } - let(:name_secret_token_options) { { key_name: key_name, key_secret: key_secret, nonce: nonce, timestamp: Time.now } } - let(:token_request) { auth.create_token_request(name_secret_token_options) } + let(:auth_options) { { key_name: key_name, key_secret: key_secret } } + let(:token_params) { { nonce: nonce, timestamp: Time.now } } + let(:token_request) { auth.create_token_request(auth_options, token_params) } let(:mac) do hmac_for(token_request, key_secret) end let(:token_response) { {} } @@ -152,11 +170,11 @@ :status => 201, :body => serialize(token_response, protocol), :headers => { 'Content-Type' => content_type }) end - let!(:token) { auth.request_token(name_secret_token_options); } + let!(:token) { auth.request_token(auth_options, token_params) } specify 'key_name is used in request and signing uses key_secret' do expect(request_token_stub).to have_been_requested end end @@ -184,16 +202,16 @@ let(:auth_url_response) { { keyName: key_name }.to_json } let(:token_response) { {} } let(:query_params) { nil } let(:headers) { nil } let(:auth_method) { :get } - let(:options) do + let(:auth_options) do { - auth_url: auth_url, - auth_params: query_params, + auth_url: auth_url, + auth_params: query_params, auth_headers: headers, - auth_method: auth_method + auth_method: auth_method } end let!(:auth_url_request_stub) do stub = stub_request(auth_method, auth_url) @@ -217,11 +235,11 @@ :headers => { 'Content-Type' => content_type } ) end context 'when response from :auth_url is a valid token request' do - let!(:token) { auth.request_token(options) } + let!(:token) { auth.request_token(auth_options) } it 'requests a token from :auth_url using an HTTP GET request' do expect(request_token_stub).to have_been_requested expect(auth_url_request_stub).to have_been_requested end @@ -270,13 +288,14 @@ 'expires' => expires.to_i * 1000, 'capability'=> capability_str }.to_json end - let!(:token_details) { auth.request_token(options) } + let!(:token_details) { auth.request_token(auth_options) } it 'returns TokenDetails created from the token JSON' do + expect(auth_url_request_stub).to have_been_requested expect(request_token_stub).to_not have_been_requested expect(token_details).to be_a(Ably::Models::TokenDetails) expect(token_details.token).to eql(token) expect(token_details.expires).to be_within(1).of(expires) expect(token_details.issued).to be_within(1).of(issued) @@ -287,13 +306,14 @@ context 'when response from :auth_url is text/plain content type and a token string' do let(:token) { 'J_0Tlg.D7AVZkdOZW-PqNNGvCSp38' } let(:auth_url_content_type) { 'text/plain' } let(:auth_url_response) { token } - let!(:token_details) { auth.request_token(options) } + let!(:token_details) { auth.request_token(auth_options) } it 'returns TokenDetails created from the token JSON' do + expect(auth_url_request_stub).to have_been_requested expect(request_token_stub).to_not have_been_requested expect(token_details).to be_a(Ably::Models::TokenDetails) expect(token_details.token).to eql(token) end end @@ -303,42 +323,45 @@ let!(:auth_url_request_stub) do stub_request(auth_method, auth_url).to_return(:status => 500) end it 'raises ServerError' do - expect { auth.request_token options }.to raise_error(Ably::Exceptions::ServerError) + expect { auth.request_token auth_options }.to raise_error(Ably::Exceptions::ServerError) end end context 'XML' do let!(:auth_url_request_stub) do stub_request(auth_method, auth_url). to_return(:status => 201, :body => '<xml></xml>', :headers => { 'Content-Type' => 'application/xml' }) end it 'raises InvalidResponseBody' do - expect { auth.request_token options }.to raise_error(Ably::Exceptions::InvalidResponseBody) + expect { auth.request_token auth_options }.to raise_error(Ably::Exceptions::InvalidResponseBody) end end end end context 'with a Proc for the :auth_callback option' do context 'that returns a TokenRequest' do let(:client_id) { random_str } - let(:options) { { client_id: client_id } } - let!(:request_token) do - auth.request_token(options.merge(auth_callback: Proc.new do |block_options| + let(:ttl) { 8888 } + let(:auth_callback) do + Proc.new do |token_params_arg| @block_called = true - @block_options = block_options - auth.create_token_request(client_id: client_id) - end)) + expect(token_params_arg).to eq(token_params) + auth.create_token_request(token_params: { client_id: client_id }) + end end + let(:token_params) { { ttl: ttl } } + let!(:request_token) do + auth.request_token(auth_callback: auth_callback, token_params: token_params) + end - it 'calls the Proc when authenticating to obtain the request token' do + it 'calls the Proc with token_params when authenticating to obtain the request token' do expect(@block_called).to eql(true) - expect(@block_options).to include(options) end it 'uses the token request returned from the callback when requesting a new token' do expect(request_token.client_id).to eql(client_id) end @@ -352,27 +375,27 @@ let(:expires) { Time.now + 60} let(:capability) { {'foo'=>['publish']} } let(:capability_str) { JSON.dump(capability) } let!(:token_details) do - auth.request_token(options.merge(auth_callback: Proc.new do |block_options| + auth.request_token(auth_callback: Proc.new do |token_params_arg| @block_called = true - @block_options = block_options + @block_params = token_params_arg { 'token' => token, 'keyName' => 'J_0Tlg.NxCRig', 'clientId' => client_id, 'issued' => issued.to_i * 1000, 'expires' => expires.to_i * 1000, 'capability'=> capability_str } - end)) + end, token_params: options) end it 'calls the Proc when authenticating to obtain the request token' do expect(@block_called).to eql(true) - expect(@block_options).to include(options) + expect(@block_params).to include(options) end it 'uses the token request returned from the callback when requesting a new token' do expect(token_details).to be_a(Ably::Models::TokenDetails) expect(token_details.token).to eql(token) @@ -386,11 +409,11 @@ context 'that returns a TokenDetails object' do let(:client_id) { random_str } let!(:token_details) do auth.request_token(auth_callback: Proc.new do |block_options| - auth.create_token_request({ + auth.create_token_request(token_params: { client_id: client_id }) end) end @@ -418,53 +441,64 @@ end context 'persisted option', api_private: true do context 'when set to true', api_private: true do let(:options) { { persisted: true } } - let(:token_details) { auth.request_token(options) } + let(:token_details) { auth.request_token(token_params: options) } it 'returns a token with a short token ID that is used to look up the token details' do expect(token_details.token.length).to be < 64 expect(token_details.token).to match(/^#{app_id}\.A/) end end context 'when omitted', api_private: true do - let(:options) { { } } - let(:token_details) { auth.request_token(options) } + let(:token_details) { auth.request_token } it 'returns a literal token' do expect(token_details.token.length).to be > 64 end end end - context 'with client_id' do + context 'with auth_option :client_id' do let(:client_id) { random_str } let(:token_details) { auth.request_token(client_id: client_id) } it 'returns a token with the client_id' do expect(token_details.client_id).to eql(client_id) end end + + context 'with token_param :client_id' do + let(:client_id) { random_str } + let(:token_details) { auth.request_token(token_params: { client_id: client_id }) } + + it 'returns a token with the client_id' do + expect(token_details.client_id).to eql(client_id) + end + end end context 'before #authorise has been called' do it 'has no current_token_details' do expect(auth.current_token_details).to be_nil end end describe '#authorise' do context 'when called for the first time since the client has been instantiated' do - let(:request_options) do + let(:auth_options) do { auth_url: 'http://somewhere.com/' } end + let(:token_params) do + { ttl: 55 } + end - it 'passes all options to #request_token' do - expect(auth).to receive(:request_token).with(request_options) - auth.authorise request_options + it 'passes all auth_options and token_params to #request_token' do + expect(auth).to receive(:request_token).with(auth_options, token_params) + auth.authorise auth_options, token_params end it 'returns a valid token' do expect(auth.authorise).to be_a(Ably::Models::TokenDetails) end @@ -538,23 +572,44 @@ end end end describe '#create_token_request' do - let(:ttl) { 60 * 60 } - let(:capability) { { :foo => ["publish"] } } - let(:token_request_options) { Hash.new } - subject { auth.create_token_request(token_request_options) } + let(:ttl) { 60 * 60 } + let(:capability) { { "foo" => ["publish"] } } + let(:token_params) { Hash.new } + subject { auth.create_token_request(token_params: token_params) } + + it 'returns a TokenRequest object' do + expect(subject).to be_a(Ably::Models::TokenRequest) + end + + it 'returns a TokenRequest that can be passed to a client that can use it for authentication without an API key' do + auth_callback = Proc.new { subject } + client_without_api_key = Ably::Rest::Client.new(default_options.merge(auth_callback: auth_callback)) + expect(client_without_api_key.auth).to be_using_token_auth + expect { client_without_api_key.auth.authorise }.to_not raise_error + end + it 'uses the key name from the client' do expect(subject['keyName']).to eql(key_name) end it 'uses the default TTL' do expect(subject['ttl']).to eql(Ably::Auth::TOKEN_DEFAULTS.fetch(:ttl) * 1000) end + context 'with a :ttl option below the Token expiry buffer that ensures tokens are renewed 15s before they expire as they are considered expired' do + let(:ttl) { 1 } + + it 'uses the Token expiry buffer default + 10s to allow for a token request in flight' do + expect(subject.ttl).to be > 1 + expect(subject.ttl).to be > Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER + end + end + it 'uses the default capability' do expect(subject['capability']).to eql(Ably::Auth::TOKEN_DEFAULTS.fetch(:capability).to_json) end context 'the nonce' do @@ -567,23 +622,40 @@ expect(subject['nonce'].length).to be >= 16 end end %w(ttl nonce client_id).each do |attribute| - context "with option :#{attribute}" do - let(:option_value) { random_int_str(1_000_000_000).to_i } + context "with token param :#{attribute}" do + let(:token_param) { random_int_str(1_000_000_000).to_i } before do - token_request_options[attribute.to_sym] = option_value + token_params[attribute.to_sym] = token_param end it "overrides default" do - expect(subject.public_send(attribute).to_s).to eql(option_value.to_s) + expect(subject.public_send(attribute).to_s).to eql(token_param.to_s) end end end + context 'when specifying capability' do + before do + token_params[:capability] = capability + end + + it 'overrides the default' do + expect(subject.capability).to eql(capability) + end + + it 'uses these capabilities when Ably issues an actual token' do + auth_callback = Proc.new { subject } + client_without_api_key = Ably::Rest::Client.new(default_options.merge(auth_callback: auth_callback)) + client_without_api_key.auth.authorise + expect(client_without_api_key.auth.current_token_details.capability).to eql(capability) + end + end + context 'with additional invalid attributes' do - let(:token_request_options) { { nonce: 'valid', is_not_used_by_token_request: 'invalid' } } + let(:token_params) { { nonce: 'valid', is_not_used_by_token_request: 'invalid' } } specify 'are ignored' do expect(subject.hash.keys).to_not include(:is_not_used_by_token_request) expect(subject.hash.keys).to_not include(convert_to_mixed_case(:is_not_used_by_token_request)) expect(subject.hash.keys).to include(:nonce) expect(subject.nonce).to eql('valid') @@ -592,52 +664,62 @@ context 'when required fields are missing' do let(:client) { Ably::Rest::Client.new(auth_url: 'http://example.com', protocol: protocol) } it 'should raise an exception if key secret is missing' do - expect { auth.create_token_request(key_name: 'name') }.to raise_error Ably::Exceptions::TokenRequestError + expect { auth.create_token_request(key_name: 'name') }.to raise_error Ably::Exceptions::TokenRequestFailed end it 'should raise an exception if key name is missing' do - expect { auth.create_token_request(key_secret: 'secret') }.to raise_error Ably::Exceptions::TokenRequestError + expect { auth.create_token_request(key_secret: 'secret') }.to raise_error Ably::Exceptions::TokenRequestFailed end end - context 'with :query_time option' do - let(:time) { Time.now - 30 } - let(:token_request_options) { { query_time: true } } + context 'timestamp attribute' do + context 'with :query_time auth_option' do + let(:time) { Time.now - 30 } + let(:auth_options) { { query_time: true } } - it 'queries the server for the timestamp' do - expect(client).to receive(:time).and_return(time) - expect(subject['timestamp']).to be_within(1).of(time.to_f * 1000) + subject { auth.create_token_request(auth_options) } + + it 'queries the server for the timestamp' do + expect(client).to receive(:time).and_return(time) + expect(subject['timestamp']).to be_within(1).of(time.to_f * 1000) + end end - end - context 'with :timestamp option' do - let(:token_request_time) { Time.now + 5 } - let(:token_request_options) { { timestamp: token_request_time } } + context 'with :timestamp option' do + let(:token_request_time) { Time.now + 5 } + let(:token_params) { { timestamp: token_request_time } } - it 'uses the provided timestamp in the token request' do - expect(subject['timestamp']).to be_within(1).of(token_request_time.to_f * 1000) + it 'uses the provided timestamp in the token request' do + expect(subject['timestamp']).to be_within(1).of(token_request_time.to_f * 1000) + end end + + it 'is a Time object in Ruby and is set to the local time' do + expect(subject.timestamp.to_f).to be_within(1).of(Time.now.to_f) + end end context 'signing' do - let(:token_request_options) do + let(:token_attributes) do { key_name: random_str, ttl: random_int_str.to_i, capability: random_str, client_id: random_str, timestamp: random_int_str.to_i, nonce: random_str } end + let(:client_options) { default_options.merge(key_name: token_attributes.fetch(:key_name), key_secret: key_secret) } + let(:token_params) { token_attributes } # TokenRequest expects times in milliseconds, whereas create_token_request assumes Ruby default of seconds let(:token_request_attributes) do - token_request_options.merge(timestamp: token_request_options[:timestamp] * 1000, ttl: token_request_options[:ttl] * 1000) + token_attributes.merge(timestamp: token_attributes[:timestamp] * 1000, ttl: token_attributes[:ttl] * 1000) end it 'generates a valid HMAC' do hmac = hmac_for(Ably::Models::TokenRequest(token_request_attributes).hash, key_secret) expect(subject['mac']).to eql(hmac) @@ -649,14 +731,14 @@ let(:capability) { { :foo => ["publish"] } } describe 'with :token option' do let(:ttl) { 60 * 60 } let(:token_details) do - auth.request_token( + auth.request_token(token_params: { ttl: ttl, capability: capability - ) + }) end let(:token) { token_details.token } let(:token_auth_client) do Ably::Rest::Client.new(token: token, environment: environment, protocol: protocol) end @@ -672,10 +754,10 @@ expect(error.code).to eql(40160) end end it 'fails if timestamp is invalid' do - expect { auth.request_token(timestamp: Time.now - 180) }.to raise_error do |error| + expect { auth.request_token(token_params: { timestamp: Time.now - 180 }) }.to raise_error do |error| expect(error).to be_a(Ably::Exceptions::InvalidRequest) expect(error.status).to eql(401) expect(error.code).to eql(40101) end end