mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add Basic HTTP Authentication to ActiveResource (closes #6305). [jonathan]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@5208 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
d15d15b2c2
commit
7ac6ed893f
7 changed files with 190 additions and 39 deletions
|
@ -1,5 +1,7 @@
|
|||
*SVN*
|
||||
|
||||
* Add Basic HTTP Authentication to ActiveResource (closes #6305). [jonathan]
|
||||
|
||||
* Extracted #id_from_response as an entry point for customizing how a created resource gets its own ID.
|
||||
By default, it extracts from the Location response header.
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ module ActiveResource
|
|||
|
||||
|
||||
class Connection
|
||||
attr_accessor :site
|
||||
attr_reader :site
|
||||
|
||||
class << self
|
||||
def requests
|
||||
|
@ -39,23 +39,27 @@ module ActiveResource
|
|||
end
|
||||
|
||||
def initialize(site)
|
||||
@site = site
|
||||
self.site = site.is_a?(URI) ? site : URI.parse(site)
|
||||
end
|
||||
|
||||
def site=(site)
|
||||
@site = site.is_a?(URI) ? site : URI.parse(site)
|
||||
end
|
||||
|
||||
def get(path)
|
||||
Hash.from_xml(request(:get, path).body)
|
||||
Hash.from_xml(request(:get, path, build_request_headers).body)
|
||||
end
|
||||
|
||||
def delete(path)
|
||||
request(:delete, path, self.class.default_header)
|
||||
request(:delete, path, build_request_headers)
|
||||
end
|
||||
|
||||
def put(path, body = '')
|
||||
request(:put, path, body, self.class.default_header)
|
||||
request(:put, path, body, build_request_headers)
|
||||
end
|
||||
|
||||
def post(path, body = '')
|
||||
request(:post, path, body, self.class.default_header)
|
||||
request(:post, path, body, build_request_headers)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -91,5 +95,13 @@ module ActiveResource
|
|||
|
||||
@http
|
||||
end
|
||||
|
||||
def build_request_headers
|
||||
authorization_header.update(self.class.default_header)
|
||||
end
|
||||
|
||||
def authorization_header
|
||||
(@site.user || @site.password ? { 'Authorization' => 'Basic ' + ["#{@site.user}:#{ @site.password}"].pack('m').delete("\r\n") } : {})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
82
activeresource/test/authorization_test.rb
Normal file
82
activeresource/test/authorization_test.rb
Normal file
|
@ -0,0 +1,82 @@
|
|||
require "#{File.dirname(__FILE__)}/abstract_unit"
|
||||
require 'base64'
|
||||
|
||||
class AuthorizationTest < Test::Unit::TestCase
|
||||
Response = Struct.new(:code)
|
||||
|
||||
def setup
|
||||
@conn = ActiveResource::Connection.new('http://localhost')
|
||||
@matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
|
||||
@david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')
|
||||
@authenticated_conn = ActiveResource::Connection.new("http://david:test123@localhost")
|
||||
@authorization_request_header = { 'Authorization' => 'Basic ZGF2aWQ6dGVzdDEyMw==' }
|
||||
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/2.xml", @authorization_request_header, @david
|
||||
mock.put "/people/2.xml", @authorization_request_header, nil, 204
|
||||
mock.delete "/people/2.xml", @authorization_request_header, nil, 200
|
||||
mock.post "/people/2/addresses.xml", @authorization_request_header, nil, 201, 'Location' => '/people/1/addresses/5'
|
||||
end
|
||||
end
|
||||
|
||||
def test_authorization_header
|
||||
authorization_header = @authenticated_conn.send(:authorization_header)
|
||||
assert_equal @authorization_request_header['Authorization'], authorization_header['Authorization']
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["david", "test123"], Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_authorization_header_with_username_but_no_password
|
||||
@conn = ActiveResource::Connection.new("http://david:@localhost")
|
||||
authorization_header = @conn.send(:authorization_header)
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["david"], Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_authorization_header_with_password_but_no_username
|
||||
@conn = ActiveResource::Connection.new("http://:test123@localhost")
|
||||
authorization_header = @conn.send(:authorization_header)
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["", "test123"], Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_get
|
||||
david = @authenticated_conn.get("/people/2.xml")
|
||||
assert_equal "David", david["person"]["name"]
|
||||
end
|
||||
|
||||
def test_post
|
||||
response = @authenticated_conn.post("/people/2/addresses.xml")
|
||||
assert_equal "/people/1/addresses/5", response["Location"]
|
||||
end
|
||||
|
||||
def test_put
|
||||
response = @authenticated_conn.put("/people/2.xml")
|
||||
assert_equal 204, response.code
|
||||
end
|
||||
|
||||
def test_delete
|
||||
response = @authenticated_conn.delete("/people/2.xml")
|
||||
assert_equal 200, response.code
|
||||
end
|
||||
|
||||
def test_raises_invalid_request_on_unauthorized_requests
|
||||
assert_raises(ActiveResource::InvalidRequestError) { @conn.post("/people/2.xml") }
|
||||
assert_raises(ActiveResource::InvalidRequestError) { @conn.post("/people/2/addresses.xml") }
|
||||
assert_raises(ActiveResource::InvalidRequestError) { @conn.put("/people/2.xml") }
|
||||
assert_raises(ActiveResource::InvalidRequestError) { @conn.delete("/people/2.xml") }
|
||||
end
|
||||
|
||||
protected
|
||||
def assert_response_raises(klass, code)
|
||||
assert_raise(klass, "Expected response code #{code} to raise #{klass}") do
|
||||
@conn.send(:handle_response, Response.new(code))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,7 +4,7 @@ require "fixtures/person"
|
|||
class BaseErrorsTest < Test::Unit::TestCase
|
||||
def setup
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.post "/people.xml", "<?xml version=\"1.0\" encoding=\"UTF-8\"?><errors><error>Age can't be blank</error><error>Name can't be blank</error><error>Name must start with a letter</error><error>Person quota full for today.</error></errors>", 400
|
||||
mock.post "/people.xml", {}, "<?xml version=\"1.0\" encoding=\"UTF-8\"?><errors><error>Age can't be blank</error><error>Name can't be blank</error><error>Name must start with a letter</error><error>Person quota full for today.</error></errors>", 400
|
||||
end
|
||||
@exception = nil
|
||||
@person = Person.new(:name => '', :age => '')
|
||||
|
|
|
@ -7,25 +7,27 @@ class BaseTest < Test::Unit::TestCase
|
|||
@matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
|
||||
@david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')
|
||||
@addy = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address')
|
||||
@default_request_headers = { 'Content-Type' => 'application/xml' }
|
||||
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/1.xml", @matz
|
||||
mock.get "/people/2.xml", @david
|
||||
mock.put "/people/1.xml", nil, 204
|
||||
mock.delete "/people/1.xml", nil, 200
|
||||
mock.delete "/people/2.xml", nil, 400
|
||||
mock.post "/people.xml", nil, 201, 'Location' => '/people/5.xml'
|
||||
mock.get "/people/99.xml", nil, 404
|
||||
mock.get "/people.xml", "<people>#{@matz}#{@david}</people>"
|
||||
mock.get "/people/1/addresses.xml", "<addresses>#{@addy}</addresses>"
|
||||
mock.get "/people/1/addresses/1.xml", @addy
|
||||
mock.put "/people/1/addresses/1.xml", nil, 204
|
||||
mock.delete "/people/1/addresses/1.xml", nil, 200
|
||||
mock.post "/people/1/addresses.xml", nil, 201, 'Location' => '/people/1/addresses/5'
|
||||
mock.get "/people//addresses.xml", nil, 404
|
||||
mock.get "/people//addresses/1.xml", nil, 404
|
||||
mock.put "/people//addresses/1.xml", nil, 404
|
||||
mock.delete "/people//addresses/1.xml", nil, 404
|
||||
mock.post "/people//addresses.xml", nil, 404
|
||||
mock.get "/people/1.xml", {}, @matz
|
||||
mock.get "/people/2.xml", {}, @david
|
||||
mock.put "/people/1.xml", {}, nil, 204
|
||||
mock.delete "/people/1.xml", {}, nil, 200
|
||||
mock.delete "/people/2.xml", {}, nil, 400
|
||||
mock.post "/people.xml", {}, nil, 201, 'Location' => '/people/5.xml'
|
||||
mock.get "/people/99.xml", {}, nil, 404
|
||||
mock.get "/people.xml", {}, "<people>#{@matz}#{@david}</people>"
|
||||
mock.get "/people/1/addresses.xml", {}, "<addresses>#{@addy}</addresses>"
|
||||
mock.get "/people/1/addresses/1.xml", {}, @addy
|
||||
mock.put "/people/1/addresses/1.xml", {}, nil, 204
|
||||
mock.delete "/people/1/addresses/1.xml", {}, nil, 200
|
||||
mock.post "/people/1/addresses.xml", {}, nil, 201, 'Location' => '/people/1/addresses/5'
|
||||
mock.get "/people//addresses.xml", {}, nil, 404
|
||||
mock.get "/people//addresses/1.xml", {}, nil, 404
|
||||
mock.put "/people//addresses/1.xml", {}, nil, 404
|
||||
mock.delete "/people//addresses/1.xml", {}, nil, 404
|
||||
mock.post "/people//addresses.xml", {}, nil, 404
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -144,8 +146,8 @@ class BaseTest < Test::Unit::TestCase
|
|||
|
||||
def test_update_conflict
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/2.xml", @david
|
||||
mock.put "/people/2.xml", nil, 409
|
||||
mock.get "/people/2.xml", {}, @david
|
||||
mock.put "/people/2.xml", @default_request_headers, nil, 409
|
||||
end
|
||||
assert_raises(ActiveResource::ResourceConflict) { Person.find(2).save }
|
||||
end
|
||||
|
@ -153,7 +155,7 @@ class BaseTest < Test::Unit::TestCase
|
|||
def test_destroy
|
||||
assert Person.find(1).destroy
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/1.xml", nil, 404
|
||||
mock.get "/people/1.xml", {}, nil, 404
|
||||
end
|
||||
assert_raises(ActiveResource::ResourceNotFound) { Person.find(1).destroy }
|
||||
end
|
||||
|
@ -161,7 +163,7 @@ class BaseTest < Test::Unit::TestCase
|
|||
def test_destroy_with_custom_prefix
|
||||
assert StreetAddress.find(1, :person_id => 1).destroy
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/1/addresses/1.xml", nil, 404
|
||||
mock.get "/people/1/addresses/1.xml", {}, nil, 404
|
||||
end
|
||||
assert_raises(ActiveResource::ResourceNotFound) { StreetAddress.find(1, :person_id => 1).destroy }
|
||||
end
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
require "#{File.dirname(__FILE__)}/abstract_unit"
|
||||
require 'base64'
|
||||
|
||||
class ConnectionTest < Test::Unit::TestCase
|
||||
Response = Struct.new(:code)
|
||||
|
||||
def setup
|
||||
@conn = ActiveResource::Connection.new('http://localhost')
|
||||
@matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
|
||||
@david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')
|
||||
@default_request_headers = { 'Content-Type' => 'application/xml' }
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/1.xml", {}, @matz
|
||||
mock.put "/people/1.xml", {}, nil, 204
|
||||
mock.delete "/people/1.xml", {}, nil, 200
|
||||
mock.post "/people.xml", {}, nil, 201, 'Location' => '/people/5.xml'
|
||||
end
|
||||
end
|
||||
|
||||
def test_handle_response
|
||||
|
@ -39,6 +49,36 @@ class ConnectionTest < Test::Unit::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_site_accessor_accepts_uri_or_string_argument
|
||||
site = URI.parse("http://localhost")
|
||||
|
||||
assert_nothing_raised { @conn.site = "http://localhost" }
|
||||
assert_equal site, @conn.site
|
||||
|
||||
assert_nothing_raised { @conn.site = site }
|
||||
assert_equal site, @conn.site
|
||||
end
|
||||
|
||||
def test_get
|
||||
matz = @conn.get("/people/1.xml")
|
||||
assert_equal "Matz", matz["person"]["name"]
|
||||
end
|
||||
|
||||
def test_post
|
||||
response = @conn.post("/people.xml")
|
||||
assert_equal "/people/5.xml", response["Location"]
|
||||
end
|
||||
|
||||
def test_put
|
||||
response = @conn.put("/people/1.xml")
|
||||
assert_equal 204, response.code
|
||||
end
|
||||
|
||||
def test_delete
|
||||
response = @conn.delete("/people/1.xml")
|
||||
assert_equal 200, response.code
|
||||
end
|
||||
|
||||
protected
|
||||
def assert_response_raises(klass, code)
|
||||
assert_raise(klass, "Expected response code #{code} to raise #{klass}") do
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'active_resource/connection'
|
||||
|
||||
module ActiveResource
|
||||
class InvalidRequestError < StandardError; end
|
||||
|
||||
class HttpMock
|
||||
class Responder
|
||||
def initialize(responses)
|
||||
|
@ -9,8 +11,8 @@ module ActiveResource
|
|||
|
||||
for method in [ :post, :put, :get, :delete ]
|
||||
module_eval <<-EOE
|
||||
def #{method}(path, body = nil, status = 200, headers = {})
|
||||
@responses[Request.new(:#{method}, path, nil)] = Response.new(body || {}, status, headers)
|
||||
def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {})
|
||||
@responses[Request.new(:#{method}, path, nil, request_headers)] = Response.new(body || {}, status, response_headers)
|
||||
end
|
||||
EOE
|
||||
end
|
||||
|
@ -39,12 +41,22 @@ module ActiveResource
|
|||
end
|
||||
end
|
||||
|
||||
for method in [ :post, :put, :get, :delete ]
|
||||
for method in [ :post, :put ]
|
||||
module_eval <<-EOE
|
||||
def #{method}(*arguments)
|
||||
request = ActiveResource::Request.new(:#{method}, *arguments)
|
||||
def #{method}(path, body, headers)
|
||||
request = ActiveResource::Request.new(:#{method}, path, body, headers)
|
||||
self.class.requests << request
|
||||
self.class.responses[request] || raise("No response recorded for: \#{request}")
|
||||
self.class.responses[request] || raise(InvalidRequestError.new("No response recorded for: \#{request}"))
|
||||
end
|
||||
EOE
|
||||
end
|
||||
|
||||
for method in [ :get, :delete ]
|
||||
module_eval <<-EOE
|
||||
def #{method}(path, headers)
|
||||
request = ActiveResource::Request.new(:#{method}, path, nil, headers)
|
||||
self.class.requests << request
|
||||
self.class.responses[request] || raise(InvalidRequestError.new("No response recorded for: \#{request}"))
|
||||
end
|
||||
EOE
|
||||
end
|
||||
|
@ -55,10 +67,11 @@ module ActiveResource
|
|||
end
|
||||
|
||||
class Request
|
||||
attr_accessor :path, :method, :body
|
||||
attr_accessor :path, :method, :body, :headers
|
||||
|
||||
def initialize(method, path, body = nil, headers = nil)
|
||||
@method, @path, @body = method, path, body
|
||||
@method, @path, @body, @headers = method, path, body, headers
|
||||
@headers.update('Content-Type' => 'application/xml')
|
||||
end
|
||||
|
||||
def ==(other_request)
|
||||
|
@ -70,11 +83,11 @@ module ActiveResource
|
|||
end
|
||||
|
||||
def to_s
|
||||
"<#{method.to_s.upcase}: #{path} (#{body})>"
|
||||
"<#{method.to_s.upcase}: #{path} [#{headers}] (#{body})>"
|
||||
end
|
||||
|
||||
def hash
|
||||
"#{path}#{method}".hash
|
||||
"#{path}#{method}#{headers}".hash
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue