1
0
Fork 0
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:
Rick Olson 2006-09-29 16:25:49 +00:00
parent d15d15b2c2
commit 7ac6ed893f
7 changed files with 190 additions and 39 deletions

View file

@ -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.

View file

@ -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

View 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

View file

@ -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 => '')

View file

@ -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

View file

@ -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

View file

@ -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