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*
|
*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.
|
* 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.
|
By default, it extracts from the Location response header.
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ module ActiveResource
|
||||||
|
|
||||||
|
|
||||||
class Connection
|
class Connection
|
||||||
attr_accessor :site
|
attr_reader :site
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def requests
|
def requests
|
||||||
|
@ -39,23 +39,27 @@ module ActiveResource
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(site)
|
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
|
end
|
||||||
|
|
||||||
def get(path)
|
def get(path)
|
||||||
Hash.from_xml(request(:get, path).body)
|
Hash.from_xml(request(:get, path, build_request_headers).body)
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete(path)
|
def delete(path)
|
||||||
request(:delete, path, self.class.default_header)
|
request(:delete, path, build_request_headers)
|
||||||
end
|
end
|
||||||
|
|
||||||
def put(path, body = '')
|
def put(path, body = '')
|
||||||
request(:put, path, body, self.class.default_header)
|
request(:put, path, body, build_request_headers)
|
||||||
end
|
end
|
||||||
|
|
||||||
def post(path, body = '')
|
def post(path, body = '')
|
||||||
request(:post, path, body, self.class.default_header)
|
request(:post, path, body, build_request_headers)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -91,5 +95,13 @@ module ActiveResource
|
||||||
|
|
||||||
@http
|
@http
|
||||||
end
|
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
|
||||||
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
|
class BaseErrorsTest < Test::Unit::TestCase
|
||||||
def setup
|
def setup
|
||||||
ActiveResource::HttpMock.respond_to do |mock|
|
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
|
end
|
||||||
@exception = nil
|
@exception = nil
|
||||||
@person = Person.new(:name => '', :age => '')
|
@person = Person.new(:name => '', :age => '')
|
||||||
|
|
|
@ -7,25 +7,27 @@ class BaseTest < Test::Unit::TestCase
|
||||||
@matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
|
@matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
|
||||||
@david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')
|
@david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')
|
||||||
@addy = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address')
|
@addy = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address')
|
||||||
|
@default_request_headers = { 'Content-Type' => 'application/xml' }
|
||||||
|
|
||||||
ActiveResource::HttpMock.respond_to do |mock|
|
ActiveResource::HttpMock.respond_to do |mock|
|
||||||
mock.get "/people/1.xml", @matz
|
mock.get "/people/1.xml", {}, @matz
|
||||||
mock.get "/people/2.xml", @david
|
mock.get "/people/2.xml", {}, @david
|
||||||
mock.put "/people/1.xml", nil, 204
|
mock.put "/people/1.xml", {}, nil, 204
|
||||||
mock.delete "/people/1.xml", nil, 200
|
mock.delete "/people/1.xml", {}, nil, 200
|
||||||
mock.delete "/people/2.xml", nil, 400
|
mock.delete "/people/2.xml", {}, nil, 400
|
||||||
mock.post "/people.xml", nil, 201, 'Location' => '/people/5.xml'
|
mock.post "/people.xml", {}, nil, 201, 'Location' => '/people/5.xml'
|
||||||
mock.get "/people/99.xml", nil, 404
|
mock.get "/people/99.xml", {}, nil, 404
|
||||||
mock.get "/people.xml", "<people>#{@matz}#{@david}</people>"
|
mock.get "/people.xml", {}, "<people>#{@matz}#{@david}</people>"
|
||||||
mock.get "/people/1/addresses.xml", "<addresses>#{@addy}</addresses>"
|
mock.get "/people/1/addresses.xml", {}, "<addresses>#{@addy}</addresses>"
|
||||||
mock.get "/people/1/addresses/1.xml", @addy
|
mock.get "/people/1/addresses/1.xml", {}, @addy
|
||||||
mock.put "/people/1/addresses/1.xml", nil, 204
|
mock.put "/people/1/addresses/1.xml", {}, nil, 204
|
||||||
mock.delete "/people/1/addresses/1.xml", nil, 200
|
mock.delete "/people/1/addresses/1.xml", {}, nil, 200
|
||||||
mock.post "/people/1/addresses.xml", nil, 201, 'Location' => '/people/1/addresses/5'
|
mock.post "/people/1/addresses.xml", {}, nil, 201, 'Location' => '/people/1/addresses/5'
|
||||||
mock.get "/people//addresses.xml", nil, 404
|
mock.get "/people//addresses.xml", {}, nil, 404
|
||||||
mock.get "/people//addresses/1.xml", nil, 404
|
mock.get "/people//addresses/1.xml", {}, nil, 404
|
||||||
mock.put "/people//addresses/1.xml", nil, 404
|
mock.put "/people//addresses/1.xml", {}, nil, 404
|
||||||
mock.delete "/people//addresses/1.xml", nil, 404
|
mock.delete "/people//addresses/1.xml", {}, nil, 404
|
||||||
mock.post "/people//addresses.xml", nil, 404
|
mock.post "/people//addresses.xml", {}, nil, 404
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -144,8 +146,8 @@ class BaseTest < Test::Unit::TestCase
|
||||||
|
|
||||||
def test_update_conflict
|
def test_update_conflict
|
||||||
ActiveResource::HttpMock.respond_to do |mock|
|
ActiveResource::HttpMock.respond_to do |mock|
|
||||||
mock.get "/people/2.xml", @david
|
mock.get "/people/2.xml", {}, @david
|
||||||
mock.put "/people/2.xml", nil, 409
|
mock.put "/people/2.xml", @default_request_headers, nil, 409
|
||||||
end
|
end
|
||||||
assert_raises(ActiveResource::ResourceConflict) { Person.find(2).save }
|
assert_raises(ActiveResource::ResourceConflict) { Person.find(2).save }
|
||||||
end
|
end
|
||||||
|
@ -153,7 +155,7 @@ class BaseTest < Test::Unit::TestCase
|
||||||
def test_destroy
|
def test_destroy
|
||||||
assert Person.find(1).destroy
|
assert Person.find(1).destroy
|
||||||
ActiveResource::HttpMock.respond_to do |mock|
|
ActiveResource::HttpMock.respond_to do |mock|
|
||||||
mock.get "/people/1.xml", nil, 404
|
mock.get "/people/1.xml", {}, nil, 404
|
||||||
end
|
end
|
||||||
assert_raises(ActiveResource::ResourceNotFound) { Person.find(1).destroy }
|
assert_raises(ActiveResource::ResourceNotFound) { Person.find(1).destroy }
|
||||||
end
|
end
|
||||||
|
@ -161,7 +163,7 @@ class BaseTest < Test::Unit::TestCase
|
||||||
def test_destroy_with_custom_prefix
|
def test_destroy_with_custom_prefix
|
||||||
assert StreetAddress.find(1, :person_id => 1).destroy
|
assert StreetAddress.find(1, :person_id => 1).destroy
|
||||||
ActiveResource::HttpMock.respond_to do |mock|
|
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
|
end
|
||||||
assert_raises(ActiveResource::ResourceNotFound) { StreetAddress.find(1, :person_id => 1).destroy }
|
assert_raises(ActiveResource::ResourceNotFound) { StreetAddress.find(1, :person_id => 1).destroy }
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,10 +1,20 @@
|
||||||
require "#{File.dirname(__FILE__)}/abstract_unit"
|
require "#{File.dirname(__FILE__)}/abstract_unit"
|
||||||
|
require 'base64'
|
||||||
|
|
||||||
class ConnectionTest < Test::Unit::TestCase
|
class ConnectionTest < Test::Unit::TestCase
|
||||||
Response = Struct.new(:code)
|
Response = Struct.new(:code)
|
||||||
|
|
||||||
def setup
|
def setup
|
||||||
@conn = ActiveResource::Connection.new('http://localhost')
|
@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
|
end
|
||||||
|
|
||||||
def test_handle_response
|
def test_handle_response
|
||||||
|
@ -39,6 +49,36 @@ class ConnectionTest < Test::Unit::TestCase
|
||||||
end
|
end
|
||||||
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
|
protected
|
||||||
def assert_response_raises(klass, code)
|
def assert_response_raises(klass, code)
|
||||||
assert_raise(klass, "Expected response code #{code} to raise #{klass}") do
|
assert_raise(klass, "Expected response code #{code} to raise #{klass}") do
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
require 'active_resource/connection'
|
require 'active_resource/connection'
|
||||||
|
|
||||||
module ActiveResource
|
module ActiveResource
|
||||||
|
class InvalidRequestError < StandardError; end
|
||||||
|
|
||||||
class HttpMock
|
class HttpMock
|
||||||
class Responder
|
class Responder
|
||||||
def initialize(responses)
|
def initialize(responses)
|
||||||
|
@ -9,8 +11,8 @@ module ActiveResource
|
||||||
|
|
||||||
for method in [ :post, :put, :get, :delete ]
|
for method in [ :post, :put, :get, :delete ]
|
||||||
module_eval <<-EOE
|
module_eval <<-EOE
|
||||||
def #{method}(path, body = nil, status = 200, headers = {})
|
def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {})
|
||||||
@responses[Request.new(:#{method}, path, nil)] = Response.new(body || {}, status, headers)
|
@responses[Request.new(:#{method}, path, nil, request_headers)] = Response.new(body || {}, status, response_headers)
|
||||||
end
|
end
|
||||||
EOE
|
EOE
|
||||||
end
|
end
|
||||||
|
@ -39,12 +41,22 @@ module ActiveResource
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
for method in [ :post, :put, :get, :delete ]
|
for method in [ :post, :put ]
|
||||||
module_eval <<-EOE
|
module_eval <<-EOE
|
||||||
def #{method}(*arguments)
|
def #{method}(path, body, headers)
|
||||||
request = ActiveResource::Request.new(:#{method}, *arguments)
|
request = ActiveResource::Request.new(:#{method}, path, body, headers)
|
||||||
self.class.requests << request
|
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
|
end
|
||||||
EOE
|
EOE
|
||||||
end
|
end
|
||||||
|
@ -55,10 +67,11 @@ module ActiveResource
|
||||||
end
|
end
|
||||||
|
|
||||||
class Request
|
class Request
|
||||||
attr_accessor :path, :method, :body
|
attr_accessor :path, :method, :body, :headers
|
||||||
|
|
||||||
def initialize(method, path, body = nil, headers = nil)
|
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
|
end
|
||||||
|
|
||||||
def ==(other_request)
|
def ==(other_request)
|
||||||
|
@ -70,11 +83,11 @@ module ActiveResource
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
"<#{method.to_s.upcase}: #{path} (#{body})>"
|
"<#{method.to_s.upcase}: #{path} [#{headers}] (#{body})>"
|
||||||
end
|
end
|
||||||
|
|
||||||
def hash
|
def hash
|
||||||
"#{path}#{method}".hash
|
"#{path}#{method}#{headers}".hash
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue