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