diff --git a/Rakefile b/Rakefile index 76f4c08..7d3dbd7 100755 --- a/Rakefile +++ b/Rakefile @@ -1,16 +1,12 @@ #!/usr/bin/env rake require 'bundler/gem_tasks' -require 'rake/testtask' +require 'rspec/core/rake_task' require 'yard' -Rake::TestTask.new do |test| - test.libs << 'lib' << 'test' - test.pattern = 'test/**/*_test.rb' - test.verbose = true -end +RSpec::Core::RakeTask.new(:spec) -task :default => :test +task :default => :spec namespace :doc do YARD::Rake::YardocTask.new do |task| diff --git a/lib/simple_oauth/header.rb b/lib/simple_oauth/header.rb index 5420702..60182a3 100644 --- a/lib/simple_oauth/header.rb +++ b/lib/simple_oauth/header.rb @@ -101,7 +101,7 @@ module SimpleOAuth end def url_params - CGI.parse(@uri.query || '').inject([]){|p,(k,vs)| p + vs.map{|v| [k, v] } } + CGI.parse(@uri.query || '').inject([]){|p,(k,vs)| p + vs.sort.map{|v| [k, v] } } end def rsa_sha1_signature diff --git a/simple_oauth.gemspec b/simple_oauth.gemspec index 718eabe..cf363c3 100644 --- a/simple_oauth.gemspec +++ b/simple_oauth.gemspec @@ -10,9 +10,8 @@ Gem::Specification.new do |gem| gem.summary = gem.description gem.homepage = 'https://github.com/laserlemon/simple_oauth' - gem.add_development_dependency 'minitest' - gem.add_development_dependency 'mocha', '~> 0.10.0' gem.add_development_dependency 'rake' + gem.add_development_dependency 'rspec', '~> 2.0' gem.add_development_dependency 'simplecov' gem.add_development_dependency 'yard' diff --git a/spec/simple_oauth/header_spec.rb b/spec/simple_oauth/header_spec.rb new file mode 100644 index 0000000..e962ad8 --- /dev/null +++ b/spec/simple_oauth/header_spec.rb @@ -0,0 +1,354 @@ +# encoding: utf-8 + +require 'spec_helper' + +describe SimpleOAuth::Header do + describe '.default_options' do + let(:default_options){ SimpleOAuth::Header.default_options } + + it 'is different every time' do + SimpleOAuth::Header.default_options.should_not == default_options + end + + it 'is used for new headers' do + SimpleOAuth::Header.stub(:default_options => default_options) + header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json', {}) + header.options.should == default_options + end + + it 'includes a signature method and an OAuth version' do + default_options[:signature_method].should_not be_nil + default_options[:version].should_not be_nil + end + end + + describe '.encode' do + it 'encodes (most) non-word characters' do + [' ', '!', '@', '#', '$', '%', '^', '&'].each do |character| + encoded = SimpleOAuth::Header.encode(character) + encoded.should_not == character + encoded.should == URI.encode(character, /.*/) + end + end + + it 'does not encode - . or ~' do + ['-', '.', '~'].each do |character| + encoded = SimpleOAuth::Header.encode(character) + encoded.should == character + end + end + + def self.test_special_characters + it 'encodes non-ASCII characters' do + SimpleOAuth::Header.encode('é').should == '%C3%A9' + end + + it 'encodes multibyte characters' do + SimpleOAuth::Header.encode('あ').should == '%E3%81%82' + end + end + + if RUBY_VERSION >= '1.9' + test_special_characters + else + %w(n N e E s S u U).each do |kcode| + describe %(when $KCODE = "#{kcode}") do + original_kcode = $KCODE + begin + $KCODE = kcode + test_special_characters + ensure + $KCODE = original_kcode + end + end + end + end + end + + describe '.decode' do + pending + end + + describe '.parse' do + let(:header){ SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}) } + let(:parsed_options){ SimpleOAuth::Header.parse(header) } + + it 'returns a hash' do + parsed_options.should be_a(Hash) + end + + it 'includes the options used to build the header' do + parsed_options.reject{|k,_| k == :signature }.should == header.options + end + + it 'includes a signature' do + header.options.should_not have_key(:signature) + parsed_options.should have_key(:signature) + parsed_options[:signature].should_not be_nil + end + end + + describe '#initialize' do + let(:header){ SimpleOAuth::Header.new(:get, 'HTTPS://api.TWITTER.com:443/1/statuses/friendships.json?foo=bar#anchor', {}) } + + it 'stringifies and uppercases the request method' do + header.method.should == 'GET' + end + + it 'downcases the scheme and authority' do + header.url.should =~ %r(^https://api\.twitter\.com/) + end + + it 'ignores the query and fragment' do + header.url.should =~ %r(/1/statuses/friendships\.json$) + end + end + + describe '#valid?' do + context 'using the HMAC-SHA1 signature method' do + it 'requires consumer and token secrets' do + secrets = {:consumer_secret => 'CONSUMER_SECRET', :token_secret => 'TOKEN_SECRET'} + header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, secrets) + parsed_header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, header) + parsed_header.should_not be_valid + parsed_header.should be_valid(secrets) + end + end + + context 'using the RSA-SHA1 signature method' do + it 'requires an identical private key' do + secrets = {:consumer_secret => rsa_private_key} + header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, secrets.merge(:signature_method => 'RSA-SHA1')) + parsed_header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, header) + expect{ parsed_header.valid? }.to raise_error(TypeError) + parsed_header.should be_valid(secrets) + end + end + + context 'using the RSA-SHA1 signature method' do + it 'requires consumer and token secrets' do + secrets = {:consumer_secret => 'CONSUMER_SECRET', :token_secret => 'TOKEN_SECRET'} + header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, secrets.merge(:signature_method => 'PLAINTEXT')) + parsed_header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, header) + parsed_header.should_not be_valid + parsed_header.should be_valid(secrets) + end + end + end + + describe '#normalized_attributes' do + let(:header){ SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}) } + let(:normalized_attributes){ header.send(:normalized_attributes) } + + it 'returns a sorted-key, quoted-value and comma-separated list' do + header.stub(:signed_attributes => {:d => 1, :c => 2, :b => 3, :a => 4}) + normalized_attributes.should == 'a="4", b="3", c="2", d="1"' + end + + it 'url-encodes its values' do + header.stub(:signed_attributes => {1 => '!', 2 => '@', 3 => '#', 4 => '$'}) + normalized_attributes.should == '1="%21", 2="%40", 3="%23", 4="%24"' + end + end + + describe '#signed_attributes' do + it 'includes the OAuth signature' do + header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}) + header.send(:signed_attributes).should have_key(:oauth_signature) + end + end + + describe '#attributes' do + let(:header) do + options = {} + SimpleOAuth::Header::ATTRIBUTE_KEYS.each{|k| options[k] = k.to_s.upcase } + options[:other] = 'OTHER' + SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json', {}, options) + end + let(:attributes){ header.send(:attributes) } + + it 'prepends keys with "oauth_"' do + attributes.keys.should be_all{|k| k.to_s =~ /^oauth_/ } + end + + it 'excludes keys not included in the list of valid attributes' do + attributes.keys.should be_all{|k| k.is_a?(Symbol) } + attributes.should_not have_key(:oauth_other) + end + + it 'preserves values for valid keys' do + attributes.size.should == SimpleOAuth::Header::ATTRIBUTE_KEYS.size + attributes.should be_all{|k,v| k.to_s == "oauth_#{v.downcase}" } + end + end + + describe '#signature' do + context 'calls the appropriate signature method' do + specify 'when using HMAC-SHA1' do + header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, :signature_method => 'HMAC-SHA1') + header.should_receive(:hmac_sha1_signature).once.and_return('HMAC_SHA1_SIGNATURE') + header.send(:signature).should == 'HMAC_SHA1_SIGNATURE' + end + + specify 'when using RSA-SHA1' do + header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, :signature_method => 'RSA-SHA1') + header.should_receive(:rsa_sha1_signature).once.and_return('RSA_SHA1_SIGNATURE') + header.send(:signature).should == 'RSA_SHA1_SIGNATURE' + end + + specify 'when using PLAINTEXT' do + header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, :signature_method => 'PLAINTEXT') + header.should_receive(:plaintext_signature).once.and_return('PLAINTEXT_SIGNATURE') + header.send(:signature).should == 'PLAINTEXT_SIGNATURE' + end + end + end + + describe '#hmac_sha1_signature' do + it 'reproduces a successful Twitter GET' do + options = { + :consumer_key => '8karQBlMg6gFOwcf8kcoYw', + :consumer_secret => '3d0vcHyUiiqADpWxolW8nlDIpSWMlyK7YNgc5Qna2M', + :nonce => '547fed103e122eecf84c080843eedfe6', + :signature_method => 'HMAC-SHA1', + :timestamp => '1286830180', + :token => '201425800-Sv4sTcgoffmHGkTCue0JnURT8vrm4DiFAkeFNDkh', + :token_secret => 'T5qa1tF57tfDzKmpM89DHsNuhgOY4NT6DlNLsTFcuQ' + } + header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, options) + header.to_s.should == 'OAuth oauth_consumer_key="8karQBlMg6gFOwcf8kcoYw", oauth_nonce="547fed103e122eecf84c080843eedfe6", oauth_signature="i9CT6ahDRAlfGX3hKYf78QzXsaw%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1286830180", oauth_token="201425800-Sv4sTcgoffmHGkTCue0JnURT8vrm4DiFAkeFNDkh", oauth_version="1.0"' + end + + it 'reproduces a successful Twitter POST' do + options = { + :consumer_key => '8karQBlMg6gFOwcf8kcoYw', + :consumer_secret => '3d0vcHyUiiqADpWxolW8nlDIpSWMlyK7YNgc5Qna2M', + :nonce => 'b40a3e0f18590ecdcc0e273f7d7c82f8', + :signature_method => 'HMAC-SHA1', + :timestamp => '1286830181', + :token => '201425800-Sv4sTcgoffmHGkTCue0JnURT8vrm4DiFAkeFNDkh', + :token_secret => 'T5qa1tF57tfDzKmpM89DHsNuhgOY4NT6DlNLsTFcuQ' + } + header = SimpleOAuth::Header.new(:post, 'https://api.twitter.com/1/statuses/update.json', {:status => 'hi, again'}, options) + header.to_s.should == 'OAuth oauth_consumer_key="8karQBlMg6gFOwcf8kcoYw", oauth_nonce="b40a3e0f18590ecdcc0e273f7d7c82f8", oauth_signature="mPqSFKejrWWk3ZT9bTQjhO5b2xI%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1286830181", oauth_token="201425800-Sv4sTcgoffmHGkTCue0JnURT8vrm4DiFAkeFNDkh", oauth_version="1.0"' + end + end + + describe '#secret' do + let(:header){ SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json', {}) } + let(:secret){ header.send(:secret) } + + it 'combines the consumer and token secrets with an ampersand' do + header.stub(:options => {:consumer_secret => 'CONSUMER_SECRET', :token_secret => 'TOKEN_SECRET'}) + secret.should == 'CONSUMER_SECRET&TOKEN_SECRET' + end + + it 'URL encodes each secret value before combination' do + header.stub(:options => {:consumer_secret => 'CONSUM#R_SECRET', :token_secret => 'TOKEN_S#CRET'}) + secret.should == 'CONSUM%23R_SECRET&TOKEN_S%23CRET' + end + end + + describe '#signature_base' do + let(:header){ SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json', {}) } + let(:signature_base){ header.send(:signature_base) } + + it 'combines the request method, URL and normalized parameters using ampersands' do + header.stub(:method => 'METHOD', :url => 'URL', :normalized_params => 'NORMALIZED_PARAMS') + signature_base.should == 'METHOD&URL&NORMALIZED_PARAMS' + end + + it 'URL encodes each value before combination' do + header.stub(:method => 'ME#HOD', :url => 'U#L', :normalized_params => 'NORMAL#ZED_PARAMS') + signature_base.should == 'ME%23HOD&U%23L&NORMAL%23ZED_PARAMS' + end + end + + describe '#normalized_params' do + let(:header) do + header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json', {}) + header.stub(:signature_params => [['A', '4'], ['B', '3'], ['B', '2'], ['C', '1'], ['D[]', '0 ']]) + header + end + let(:signature_params){ header.send(:signature_params) } + let(:normalized_params){ header.send(:normalized_params) } + + it 'joins key/value pairs with equal signs and ampersands' do + normalized_params.should be_a(String) + parts = normalized_params.split('&') + parts.size.should == signature_params.size + pairs = parts.map{|p| p.split('=') } + pairs.should be_all{|p| p.size == 2 } + end + end + + describe '#signature_params' do + let(:header){ SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json', {}) } + let(:signature_params){ header.send(:signature_params) } + + it 'combines OAuth header attributes, body parameters and URL parameters into an flattened array of key/value pairs' do + header.stub( + :attributes => {:attribute => 'ATTRIBUTE'}, + :params => {'param' => 'PARAM'}, + :url_params => [['url_param', '1'], ['url_param', '2']] + ) + signature_params.should == [ + [:attribute, 'ATTRIBUTE'], + ['param', 'PARAM'], + ['url_param', '1'], + ['url_param', '2'] + ] + end + end + + describe '#url_params' do + it 'returns an empty array when the URL has no query parameters' do + header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json', {}) + header.send(:url_params).should == [] + end + + it 'returns an array of key/value pairs for each query parameter' do + header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json?test=TEST', {}) + header.send(:url_params).should == [['test', 'TEST']] + end + + it 'sorts values for repeated keys' do + header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json?test=3&test=1&test=2', {}) + header.send(:url_params).should == [['test', '1'], ['test', '2'], ['test', '3']] + end + end + + describe '#rsa_sha1_signature' do + it 'reproduces a successful OAuth example GET' do + options = { + :consumer_key => 'dpf43f3p2l4k3l03', + :consumer_secret => rsa_private_key, + :nonce => '13917289812797014437', + :signature_method => 'RSA-SHA1', + :timestamp => '1196666512' + } + header = SimpleOAuth::Header.new(:get, 'http://photos.example.net/photos', {:file => 'vacaction.jpg', :size => 'original'}, options) + header.to_s.should == 'OAuth oauth_consumer_key="dpf43f3p2l4k3l03", oauth_nonce="13917289812797014437", oauth_signature="jvTp%2FwX1TYtByB1m%2BPbyo0lnCOLIsyGCH7wke8AUs3BpnwZJtAuEJkvQL2%2F9n4s5wUmUl4aCI4BwpraNx4RtEXMe5qg5T1LVTGliMRpKasKsW%2F%2Fe%2BRinhejgCuzoH26dyF8iY2ZZ%2F5D1ilgeijhV%2FvBka5twt399mXwaYdCwFYE%3D", oauth_signature_method="RSA-SHA1", oauth_timestamp="1196666512", oauth_version="1.0"' + end + end + + describe '#private_key' do + pending + end + + describe '#plaintext_signature' do + it 'reproduces a successful OAuth example GET' do + options = { + :consumer_key => 'abcd', + :consumer_secret => 'efgh', + :nonce => 'oLKtec51GQy', + :signature_method => 'PLAINTEXT', + :timestamp => '1286977095', + :token => 'ijkl', + :token_secret => 'mnop' + } + header = SimpleOAuth::Header.new(:get, 'http://host.net/resource?name=value', {:name => 'value'}, options) + header.to_s.should == 'OAuth oauth_consumer_key="abcd", oauth_nonce="oLKtec51GQy", oauth_signature="efgh%26mnop", oauth_signature_method="PLAINTEXT", oauth_timestamp="1286977095", oauth_token="ijkl", oauth_version="1.0"' + end + end +end diff --git a/test/helper.rb b/spec/spec_helper.rb similarity index 52% rename from test/helper.rb rename to spec/spec_helper.rb index b05409a..f725354 100644 --- a/test/helper.rb +++ b/spec/spec_helper.rb @@ -3,8 +3,6 @@ unless ENV['CI'] SimpleCov.start end -require 'bundler' -Bundler.setup -require 'test/unit' -require 'mocha' require 'simple_oauth' + +Dir[File.expand_path('../support/**/*.rb', __FILE__)].each{|f| require f } diff --git a/test/rsa_private_key b/spec/support/fixtures/rsa-private-key similarity index 100% rename from test/rsa_private_key rename to spec/support/fixtures/rsa-private-key diff --git a/spec/support/rsa.rb b/spec/support/rsa.rb new file mode 100644 index 0000000..0d3f116 --- /dev/null +++ b/spec/support/rsa.rb @@ -0,0 +1,11 @@ +module RSAHelpers + PRIVATE_KEY_PATH = File.expand_path('../fixtures/rsa-private-key', __FILE__) + + def rsa_private_key + @rsa_private_key ||= File.read(PRIVATE_KEY_PATH) + end +end + +RSpec.configure do |config| + config.include RSAHelpers +end diff --git a/test/simple_oauth_test.rb b/test/simple_oauth_test.rb deleted file mode 100644 index ccb8370..0000000 --- a/test/simple_oauth_test.rb +++ /dev/null @@ -1,309 +0,0 @@ -# -*- encoding: utf-8 -*- -require 'helper' - -class SimpleOAuthTest < Test::Unit::TestCase - def test_default_options - # Default header options should change with each call due to generation of - # a unique "timestamp" and "nonce" value combination. - default_options = SimpleOAuth::Header.default_options - assert_not_equal default_options, SimpleOAuth::Header.default_options - - SimpleOAuth::Header.stubs(:default_options).returns(default_options) - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json', {}) - - # Given no options argument, header options defer to the default options. - assert_equal default_options, header.options - - # Default options should include a signature method and the OAuth version. - assert_equal 'HMAC-SHA1', default_options[:signature_method] - assert_equal '1.0', default_options[:version] - end - - def test_encode - # Non-word characters should be URL encoded... - [' ', '!', '@', '#', '$', '%', '^', '&'].each do |character| - encoded = SimpleOAuth::Header.encode(character) - assert_not_equal character, encoded - assert_equal URI.encode(character, /.*/), encoded - end - - # ...except for the "-", "." and "~" characters. - ['-', '.', '~'].each do |character| - assert_equal character, SimpleOAuth::Header.encode(character) - end - - major, minor, patch = RUBY_VERSION.split('.') - new_ruby = major.to_i >= 2 || major.to_i == 1 && minor.to_i >= 9 - old_kcode = $KCODE if !new_ruby - begin - %w(n N e E s S u U).each do |kcode| - $KCODE = kcode if !new_ruby - assert_equal '%E3%81%82', SimpleOAuth::Header.encode('あ'), "Failed to correctly escape Japanese under $KCODE = #{kcode}" - assert_equal '%C3%A9', SimpleOAuth::Header.encode('é'), "Failed to correctly escape e+acute under $KCODE = #{kcode}" - end - ensure - $KCODE = old_kcode if !new_ruby - end - end - - def test_decode - # Pending - end - - def test_parse - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}) - parsed_options = SimpleOAuth::Header.parse(header) - - # Parsed options should be a Hash. - assert_kind_of Hash, parsed_options - - # Parsed options should equal the options used to build the header, along - # with the additional signature. - assert_equal header.options, parsed_options.reject{|k,v| k == :signature } - end - - def test_initialize - header = SimpleOAuth::Header.new(:get, 'HTTPS://api.TWITTER.com:443/1/statuses/friendships.json#anchor', {}) - - # HTTP method should be an uppercase string. - # - # See: http://oauth.net/core/1.0/#rfc.section.9.1.3 - assert_equal 'GET', header.method - - # Request URL should downcase the scheme and authority parts as well as - # remove the query and fragment parts. - # - # See: http://oauth.net/core/1.0/#rfc.section.9.1.2 - assert_equal 'https://api.twitter.com/1/statuses/friendships.json', header.url - end - - def test_url - # Pending - end - - def test_to_s - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}) - assert_equal "OAuth #{header.send(:normalized_attributes)}", header.to_s - end - - def test_valid? - # When given consumer and token secrets, those secrets must be passed into - # the parsed header validation in order for the validity check to pass. - secrets = {:consumer_secret => 'CONSUMER_SECRET', :token_secret => 'TOKEN_SECRET'} - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, secrets) - parsed_header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, header) - assert !parsed_header.valid? - assert parsed_header.valid?(secrets) - - # Using the RSA-SHA1 signature method, the consumer secret must be a valid - # RSA private key. When parsing the header on the server side, the same - # consumer secret must be included in order for the header to validate. - secrets = {:consumer_secret => File.read('test/rsa_private_key')} - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, secrets.merge(:signature_method => 'RSA-SHA1')) - parsed_header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, header) - assert_raise(TypeError){ parsed_header.valid? } - assert parsed_header.valid?(secrets) - - # Like the default HMAC-RSA1 signature method, the PLAINTEXT method - # requires use of both a consumer secret and a token secret. A parsed - # header will not validate without these secret values. - secrets = {:consumer_secret => 'CONSUMER_SECRET', :token_secret => 'TOKEN_SECRET'} - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, secrets.merge(:signature_method => 'PLAINTEXT')) - parsed_header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, header) - assert !parsed_header.valid? - assert parsed_header.valid?(secrets) - end - - def test_normalized_attributes - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}) - header.stubs(:signed_attributes).returns(:d => 1, :c => 2, :b => 3, :a => 4) - - # Should return the OAuth header attributes, sorted by name, with quoted - # values and comma-separated. - assert_equal 'a="4", b="3", c="2", d="1"', header.send(:normalized_attributes) - - # Values should also be URL encoded. - header.stubs(:signed_attributes).returns(1 => '!', 2 => '@', 3 => '#', 4 => '$') - assert_equal '1="%21", 2="%40", 3="%23", 4="%24"', header.send(:normalized_attributes) - end - - def test_signed_attributes - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}) - assert header.send(:signed_attributes).keys.include?(:oauth_signature) - end - - def test_attributes - attribute_options = SimpleOAuth::Header::ATTRIBUTE_KEYS.inject({}){|o,a| o.merge(a => a.to_s.upcase) } - options = attribute_options.merge(:other => 'OTHER') - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json', {}, options) - attributes = header.send(:attributes) - - # OAuth header attributes are all to begin with the "oauth_" prefix. - assert attributes.all?{|k,v| k.to_s =~ /^oauth_/ } - - # Custom options not included in the list of valid attribute keys should - # not be included in the header attributes. - assert !attributes.key?(:oauth_other) - - # Valid attribute option values should be preserved. - assert_equal attribute_options.size, attributes.size - assert attributes.all?{|k,v| k.to_s == "oauth_#{v.downcase}" } - end - - def test_signature - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, :signature_method => 'HMAC-SHA1') - header.expects(:hmac_sha1_signature).once.returns('HMAC_SHA1_SIGNATURE') - assert_equal 'HMAC_SHA1_SIGNATURE', header.send(:signature) - - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, :signature_method => 'RSA-SHA1') - header.expects(:rsa_sha1_signature).once.returns('RSA_SHA1_SIGNATURE') - assert_equal 'RSA_SHA1_SIGNATURE', header.send(:signature) - - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, :signature_method => 'PLAINTEXT') - header.expects(:plaintext_signature).once.returns('PLAINTEXT_SIGNATURE') - assert_equal 'PLAINTEXT_SIGNATURE', header.send(:signature) - end - - def test_hmac_sha1_signature - # Reproduce an actual successful call to the Twitter API using the - # HMAC-SHA1 signature method, GETting a list of friends. - options = { - :consumer_key => '8karQBlMg6gFOwcf8kcoYw', - :consumer_secret => '3d0vcHyUiiqADpWxolW8nlDIpSWMlyK7YNgc5Qna2M', - :nonce => '547fed103e122eecf84c080843eedfe6', - #:signature_method => 'HMAC-SHA1', - :timestamp => '1286830180', - :token => '201425800-Sv4sTcgoffmHGkTCue0JnURT8vrm4DiFAkeFNDkh', - :token_secret => 'T5qa1tF57tfDzKmpM89DHsNuhgOY4NT6DlNLsTFcuQ' - } - successful = 'OAuth oauth_consumer_key="8karQBlMg6gFOwcf8kcoYw", oauth_nonce="547fed103e122eecf84c080843eedfe6", oauth_signature="i9CT6ahDRAlfGX3hKYf78QzXsaw%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1286830180", oauth_token="201425800-Sv4sTcgoffmHGkTCue0JnURT8vrm4DiFAkeFNDkh", oauth_version="1.0"' - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friends.json', {}, options) - assert_equal successful, header.to_s - - # Reproduce a successful Twitter call, POSTing a new status. - options.merge!( - :nonce => 'b40a3e0f18590ecdcc0e273f7d7c82f8', - :timestamp => '1286830181' - ) - successful = 'OAuth oauth_consumer_key="8karQBlMg6gFOwcf8kcoYw", oauth_nonce="b40a3e0f18590ecdcc0e273f7d7c82f8", oauth_signature="mPqSFKejrWWk3ZT9bTQjhO5b2xI%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1286830181", oauth_token="201425800-Sv4sTcgoffmHGkTCue0JnURT8vrm4DiFAkeFNDkh", oauth_version="1.0"' - header = SimpleOAuth::Header.new(:post, 'https://api.twitter.com/1/statuses/update.json', {:status => 'hi, again'}, options) - assert_equal successful, header.to_s - end - - def test_secret - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json', {}) - header.stubs(:options).returns(:consumer_secret => 'CONSUMER_SECRET', :token_secret => 'TOKEN_SECRET') - - # Should combine the consumer and token secrets with an ampersand. - assert_equal 'CONSUMER_SECRET&TOKEN_SECRET', header.send(:secret) - - header.stubs(:options).returns(:consumer_secret => 'CONSUM#R_SECRET', :token_secret => 'TOKEN_S#CRET') - - # Should URL encode each secret value before combination. - assert_equal 'CONSUM%23R_SECRET&TOKEN_S%23CRET', header.send(:secret) - end - - def test_signature_base - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json', {}) - header.stubs(:method).returns('METHOD') - header.stubs(:url).returns('URL') - header.stubs(:normalized_params).returns('NORMALIZED_PARAMS') - - # Should combine HTTP method, URL and normalized parameters string using - # ampersands. - assert_equal 'METHOD&URL&NORMALIZED_PARAMS', header.send(:signature_base) - - header.stubs(:method).returns('ME#HOD') - header.stubs(:url).returns('U#L') - header.stubs(:normalized_params).returns('NORMAL#ZED_PARAMS') - - # Each of the three combined values should be URL encoded. - assert_equal 'ME%23HOD&U%23L&NORMAL%23ZED_PARAMS', header.send(:signature_base) - end - - def test_normalized_params - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json', {}) - header.stubs(:signature_params).returns([['A', '4'], ['B', '3'], ['B', '2'], ['C', '1'], ['D[]', '0 ']]) - - # The +normalized_params+ string should join key=value pairs with - # ampersands. - signature_params = header.send(:signature_params) - normalized_params = header.send(:normalized_params) - parts = normalized_params.split('&') - pairs = parts.map{|p| p.split('=') } - assert_kind_of String, normalized_params - assert_equal signature_params.size, parts.size - assert pairs.all?{|p| p.size == 2 } - - # The signature parameters should be sorted and the keys/values URL encoded - # first. - assert_equal signature_params.sort_by{|p| p.to_s}, pairs.map{|k, v| [URI.decode(k), URI.decode(v)]} - end - - def test_signature_params - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json', {}) - header.stubs(:attributes).returns(:attribute => 'ATTRIBUTE') - header.stubs(:params).returns('param' => 'PARAM') - header.stubs(:url_params).returns([['url_param', '1'], ['url_param', '2']]) - - # Should combine OAuth header attributes, body parameters and URL - # parameters into an array of key value pairs. - signature_params = header.send(:signature_params) - assert_kind_of Array, signature_params - assert_equal [:attribute, 'param', 'url_param', 'url_param'], signature_params.map{|p| p.first} - assert_equal ['ATTRIBUTE', 'PARAM', '1', '2'], signature_params.map{|p| p.last} - end - - def test_url_params - # A URL with no query parameters should produce empty +url_params+ - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json', {}) - assert_equal [], header.send(:url_params) - - # A URL with query parameters should return a hash having array values - # containing the given query parameters. - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json?test=TEST', {}) - url_params = header.send(:url_params) - assert_kind_of Array, url_params - assert_equal [['test', 'TEST']], url_params - - # If a query parameter is repeated, the values should be sorted. - header = SimpleOAuth::Header.new(:get, 'https://api.twitter.com/1/statuses/friendships.json?test=1&test=2', {}) - assert_equal [['test', '1'], ['test', '2']], header.send(:url_params) - end - - def test_rsa_sha1_signature - # Sample request taken from: - # http://wiki.oauth.net/TestCases - options = { - :consumer_key => 'dpf43f3p2l4k3l03', - :consumer_secret => File.read('test/rsa_private_key'), - :nonce => '13917289812797014437', - :signature_method => 'RSA-SHA1', - :timestamp => '1196666512' - } - successful = 'OAuth oauth_consumer_key="dpf43f3p2l4k3l03", oauth_nonce="13917289812797014437", oauth_signature="jvTp%2FwX1TYtByB1m%2BPbyo0lnCOLIsyGCH7wke8AUs3BpnwZJtAuEJkvQL2%2F9n4s5wUmUl4aCI4BwpraNx4RtEXMe5qg5T1LVTGliMRpKasKsW%2F%2Fe%2BRinhejgCuzoH26dyF8iY2ZZ%2F5D1ilgeijhV%2FvBka5twt399mXwaYdCwFYE%3D", oauth_signature_method="RSA-SHA1", oauth_timestamp="1196666512", oauth_version="1.0"' - header = SimpleOAuth::Header.new(:get, 'http://photos.example.net/photos', {:file => 'vacaction.jpg', :size => 'original'}, options) - assert_equal successful, header.to_s - end - - def test_private_key - # Pending - end - - def plaintext_signature - # Sample request taken from: - # http://oauth.googlecode.com/svn/code/javascript/example/signature.html - options = { - :consumer_key => 'abcd', - :consumer_secret => 'efgh', - :nonce => 'oLKtec51GQy', - :signature_method => 'PLAINTEXT', - :timestamp => '1286977095', - :token => 'ijkl', - :token_secret => 'mnop' - } - successful = 'OAuth oauth_consumer_key="abcd", oauth_nonce="oLKtec51GQy", oauth_signature="efgh%26mnop", oauth_signature_method="PLAINTEXT", oauth_timestamp="1286977095", oauth_token="ijkl", oauth_version="1.0"' - header = SimpleOAuth::Header.new(:get, 'http://host.net/resource?name=value', {:name => 'value'}, options) - assert_equal successful, header.to_s - end -end