Skip to content
This repository
Browse code

Add Basic HTTP Authentication to ActiveResource (closes #6305). [jona…

…than]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@5208 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
commit 7ac6ed893fbfe9b4d4ce0e0ef18c3fecfbd48ff4 1 parent d15d15b
risk danger olson authored September 29, 2006
2  activeresource/CHANGELOG
... ...
@@ -1,5 +1,7 @@
1 1
 *SVN*
2 2
 
  3
+* Add Basic HTTP Authentication to ActiveResource (closes #6305). [jonathan]
  4
+
3 5
 * Extracted #id_from_response as an entry point for customizing how a created resource gets its own ID.
4 6
   By default, it extracts from the Location response header.
5 7
 
24  activeresource/lib/active_resource/connection.rb
@@ -25,7 +25,7 @@ class ServerError < ConnectionError;  end  # 5xx Server Error
25 25
 
26 26
 
27 27
   class Connection
28  
-    attr_accessor :site
  28
+    attr_reader :site
29 29
 
30 30
     class << self
31 31
       def requests
@@ -39,23 +39,27 @@ class << self ; attr_reader :default_header end
39 39
     end
40 40
 
41 41
     def initialize(site)
42  
-      @site = site
  42
+      self.site = site.is_a?(URI) ? site : URI.parse(site)
  43
+    end
  44
+    
  45
+    def site=(site)
  46
+      @site = site.is_a?(URI) ? site : URI.parse(site)
43 47
     end
44 48
 
45 49
     def get(path)
46  
-      Hash.from_xml(request(:get, path).body)
  50
+      Hash.from_xml(request(:get, path, build_request_headers).body)
47 51
     end
48 52
 
49 53
     def delete(path)
50  
-      request(:delete, path, self.class.default_header)
  54
+      request(:delete, path, build_request_headers)
51 55
     end
52 56
 
53 57
     def put(path, body = '')
54  
-      request(:put, path, body, self.class.default_header)
  58
+      request(:put, path, body, build_request_headers)
55 59
     end
56 60
 
57 61
     def post(path, body = '')
58  
-      request(:post, path, body, self.class.default_header)
  62
+      request(:post, path, body, build_request_headers)
59 63
     end
60 64
 
61 65
     private
@@ -91,5 +95,13 @@ def http
91 95
 
92 96
         @http
93 97
       end
  98
+      
  99
+      def build_request_headers
  100
+        authorization_header.update(self.class.default_header)
  101
+      end
  102
+      
  103
+      def authorization_header
  104
+        (@site.user || @site.password ? { 'Authorization' => 'Basic ' + ["#{@site.user}:#{ @site.password}"].pack('m').delete("\r\n") } : {})
  105
+      end
94 106
   end
95 107
 end
82  activeresource/test/authorization_test.rb
... ...
@@ -0,0 +1,82 @@
  1
+require "#{File.dirname(__FILE__)}/abstract_unit"
  2
+require 'base64'
  3
+
  4
+class AuthorizationTest < Test::Unit::TestCase
  5
+  Response = Struct.new(:code)
  6
+
  7
+  def setup
  8
+    @conn = ActiveResource::Connection.new('http://localhost')
  9
+    @matz  = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
  10
+    @david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')
  11
+    @authenticated_conn = ActiveResource::Connection.new("http://david:test123@localhost")
  12
+    @authorization_request_header = { 'Authorization' => 'Basic ZGF2aWQ6dGVzdDEyMw==' }
  13
+    
  14
+    ActiveResource::HttpMock.respond_to do |mock|
  15
+      mock.get    "/people/2.xml",           @authorization_request_header, @david
  16
+      mock.put    "/people/2.xml",           @authorization_request_header, nil, 204
  17
+      mock.delete "/people/2.xml",           @authorization_request_header, nil, 200
  18
+      mock.post   "/people/2/addresses.xml", @authorization_request_header, nil, 201, 'Location' => '/people/1/addresses/5'
  19
+    end
  20
+  end
  21
+
  22
+  def test_authorization_header
  23
+    authorization_header = @authenticated_conn.send(:authorization_header)
  24
+    assert_equal @authorization_request_header['Authorization'], authorization_header['Authorization']
  25
+    authorization = authorization_header["Authorization"].to_s.split
  26
+    
  27
+    assert_equal "Basic", authorization[0]
  28
+    assert_equal ["david", "test123"], Base64.decode64(authorization[1]).split(":")[0..1]
  29
+  end
  30
+  
  31
+  def test_authorization_header_with_username_but_no_password
  32
+    @conn = ActiveResource::Connection.new("http://david:@localhost")
  33
+    authorization_header = @conn.send(:authorization_header)
  34
+    authorization = authorization_header["Authorization"].to_s.split
  35
+    
  36
+    assert_equal "Basic", authorization[0]
  37
+    assert_equal ["david"], Base64.decode64(authorization[1]).split(":")[0..1]
  38
+  end
  39
+  
  40
+  def test_authorization_header_with_password_but_no_username
  41
+    @conn = ActiveResource::Connection.new("http://:test123@localhost")
  42
+    authorization_header = @conn.send(:authorization_header)
  43
+    authorization = authorization_header["Authorization"].to_s.split
  44
+    
  45
+    assert_equal "Basic", authorization[0]
  46
+    assert_equal ["", "test123"], Base64.decode64(authorization[1]).split(":")[0..1]
  47
+  end
  48
+  
  49
+  def test_get
  50
+    david = @authenticated_conn.get("/people/2.xml")
  51
+    assert_equal "David", david["person"]["name"]
  52
+  end
  53
+  
  54
+  def test_post
  55
+    response = @authenticated_conn.post("/people/2/addresses.xml")
  56
+    assert_equal "/people/1/addresses/5", response["Location"]
  57
+  end
  58
+  
  59
+  def test_put
  60
+    response = @authenticated_conn.put("/people/2.xml")
  61
+    assert_equal 204, response.code
  62
+  end
  63
+  
  64
+  def test_delete
  65
+    response = @authenticated_conn.delete("/people/2.xml")
  66
+    assert_equal 200, response.code
  67
+  end
  68
+
  69
+  def test_raises_invalid_request_on_unauthorized_requests
  70
+    assert_raises(ActiveResource::InvalidRequestError) { @conn.post("/people/2.xml") }
  71
+    assert_raises(ActiveResource::InvalidRequestError) { @conn.post("/people/2/addresses.xml") }
  72
+    assert_raises(ActiveResource::InvalidRequestError) { @conn.put("/people/2.xml") }
  73
+    assert_raises(ActiveResource::InvalidRequestError) { @conn.delete("/people/2.xml") }
  74
+  end
  75
+
  76
+  protected
  77
+    def assert_response_raises(klass, code)
  78
+      assert_raise(klass, "Expected response code #{code} to raise #{klass}") do
  79
+        @conn.send(:handle_response, Response.new(code))
  80
+      end
  81
+    end
  82
+end
2  activeresource/test/base_errors_test.rb
@@ -4,7 +4,7 @@
4 4
 class BaseErrorsTest < Test::Unit::TestCase
5 5
   def setup
6 6
     ActiveResource::HttpMock.respond_to do |mock|
7  
-      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
  7
+      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
8 8
     end
9 9
     @exception = nil
10 10
     @person    = Person.new(:name => '', :age => '')
46  activeresource/test/base_test.rb
@@ -7,25 +7,27 @@ def setup
7 7
     @matz  = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
8 8
     @david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')
9 9
     @addy  = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address')
  10
+    @default_request_headers = { 'Content-Type' => 'application/xml' }
  11
+    
10 12
     ActiveResource::HttpMock.respond_to do |mock|
11  
-      mock.get    "/people/1.xml",             @matz
12  
-      mock.get    "/people/2.xml",             @david
13  
-      mock.put    "/people/1.xml",             nil, 204
14  
-      mock.delete "/people/1.xml",             nil, 200
15  
-      mock.delete "/people/2.xml",             nil, 400
16  
-      mock.post   "/people.xml",               nil, 201, 'Location' => '/people/5.xml'
17  
-      mock.get    "/people/99.xml",            nil, 404
18  
-      mock.get    "/people.xml",               "<people>#{@matz}#{@david}</people>"
19  
-      mock.get    "/people/1/addresses.xml",   "<addresses>#{@addy}</addresses>"
20  
-      mock.get    "/people/1/addresses/1.xml", @addy
21  
-      mock.put    "/people/1/addresses/1.xml", nil, 204
22  
-      mock.delete "/people/1/addresses/1.xml", nil, 200
23  
-      mock.post   "/people/1/addresses.xml",   nil, 201, 'Location' => '/people/1/addresses/5'
24  
-      mock.get    "/people//addresses.xml",    nil, 404
25  
-      mock.get    "/people//addresses/1.xml",  nil, 404
26  
-      mock.put    "/people//addresses/1.xml",  nil, 404
27  
-      mock.delete "/people//addresses/1.xml",  nil, 404
28  
-      mock.post   "/people//addresses.xml",    nil, 404
  13
+      mock.get    "/people/1.xml",             {}, @matz
  14
+      mock.get    "/people/2.xml",             {}, @david
  15
+      mock.put    "/people/1.xml",             {}, nil, 204
  16
+      mock.delete "/people/1.xml",             {}, nil, 200
  17
+      mock.delete "/people/2.xml",             {}, nil, 400
  18
+      mock.post   "/people.xml",               {}, nil, 201, 'Location' => '/people/5.xml'
  19
+      mock.get    "/people/99.xml",            {}, nil, 404
  20
+      mock.get    "/people.xml",               {}, "<people>#{@matz}#{@david}</people>"
  21
+      mock.get    "/people/1/addresses.xml",   {}, "<addresses>#{@addy}</addresses>"
  22
+      mock.get    "/people/1/addresses/1.xml", {}, @addy
  23
+      mock.put    "/people/1/addresses/1.xml", {}, nil, 204
  24
+      mock.delete "/people/1/addresses/1.xml", {}, nil, 200
  25
+      mock.post   "/people/1/addresses.xml",   {}, nil, 201, 'Location' => '/people/1/addresses/5'
  26
+      mock.get    "/people//addresses.xml",    {}, nil, 404
  27
+      mock.get    "/people//addresses/1.xml",  {}, nil, 404
  28
+      mock.put    "/people//addresses/1.xml",  {}, nil, 404
  29
+      mock.delete "/people//addresses/1.xml",  {}, nil, 404
  30
+      mock.post   "/people//addresses.xml",    {}, nil, 404
29 31
     end
30 32
   end
31 33
 
@@ -144,8 +146,8 @@ def test_update_with_custom_prefix
144 146
 
145 147
   def test_update_conflict
146 148
     ActiveResource::HttpMock.respond_to do |mock|
147  
-      mock.get "/people/2.xml", @david
148  
-      mock.put "/people/2.xml", nil, 409
  149
+      mock.get "/people/2.xml", {}, @david
  150
+      mock.put "/people/2.xml", @default_request_headers, nil, 409
149 151
     end
150 152
     assert_raises(ActiveResource::ResourceConflict) { Person.find(2).save }
151 153
   end
@@ -153,7 +155,7 @@ def test_update_conflict
153 155
   def test_destroy
154 156
     assert Person.find(1).destroy
155 157
     ActiveResource::HttpMock.respond_to do |mock|
156  
-      mock.get "/people/1.xml", nil, 404
  158
+      mock.get "/people/1.xml", {}, nil, 404
157 159
     end
158 160
     assert_raises(ActiveResource::ResourceNotFound) { Person.find(1).destroy }
159 161
   end
@@ -161,7 +163,7 @@ def test_destroy
161 163
   def test_destroy_with_custom_prefix
162 164
     assert StreetAddress.find(1, :person_id => 1).destroy
163 165
     ActiveResource::HttpMock.respond_to do |mock|
164  
-      mock.get "/people/1/addresses/1.xml", nil, 404
  166
+      mock.get "/people/1/addresses/1.xml", {}, nil, 404
165 167
     end
166 168
     assert_raises(ActiveResource::ResourceNotFound) { StreetAddress.find(1, :person_id => 1).destroy }
167 169
   end
40  activeresource/test/connection_test.rb
... ...
@@ -1,10 +1,20 @@
1 1
 require "#{File.dirname(__FILE__)}/abstract_unit"
  2
+require 'base64'
2 3
 
3 4
 class ConnectionTest < Test::Unit::TestCase
4 5
   Response = Struct.new(:code)
5 6
 
6 7
   def setup
7 8
     @conn = ActiveResource::Connection.new('http://localhost')
  9
+    @matz  = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
  10
+    @david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')
  11
+    @default_request_headers = { 'Content-Type' => 'application/xml' }
  12
+    ActiveResource::HttpMock.respond_to do |mock|
  13
+      mock.get    "/people/1.xml", {}, @matz
  14
+      mock.put    "/people/1.xml", {}, nil, 204
  15
+      mock.delete "/people/1.xml", {}, nil, 200
  16
+      mock.post   "/people.xml",   {}, nil, 201, 'Location' => '/people/5.xml'
  17
+    end
8 18
   end
9 19
 
10 20
   def test_handle_response
@@ -38,7 +48,37 @@ def test_handle_response
38 48
       assert_response_raises ActiveResource::ConnectionError, code
39 49
     end
40 50
   end
  51
+  
  52
+  def test_site_accessor_accepts_uri_or_string_argument
  53
+    site = URI.parse("http://localhost")
  54
+
  55
+    assert_nothing_raised { @conn.site = "http://localhost" }
  56
+    assert_equal site,  @conn.site
41 57
 
  58
+    assert_nothing_raised { @conn.site = site }
  59
+    assert_equal site, @conn.site
  60
+  end
  61
+  
  62
+  def test_get
  63
+    matz = @conn.get("/people/1.xml")
  64
+    assert_equal "Matz", matz["person"]["name"]
  65
+  end
  66
+  
  67
+  def test_post
  68
+    response = @conn.post("/people.xml")
  69
+    assert_equal "/people/5.xml", response["Location"]
  70
+  end
  71
+  
  72
+  def test_put
  73
+    response = @conn.put("/people/1.xml")
  74
+    assert_equal 204, response.code
  75
+  end
  76
+  
  77
+  def test_delete
  78
+    response = @conn.delete("/people/1.xml")
  79
+    assert_equal 200, response.code
  80
+  end
  81
+  
42 82
   protected
43 83
     def assert_response_raises(klass, code)
44 84
       assert_raise(klass, "Expected response code #{code} to raise #{klass}") do
33  activeresource/test/http_mock.rb
... ...
@@ -1,6 +1,8 @@
1 1
 require 'active_resource/connection'
2 2
 
3 3
 module ActiveResource
  4
+  class InvalidRequestError < StandardError; end
  5
+    
4 6
   class HttpMock
5 7
     class Responder
6 8
       def initialize(responses)
@@ -9,8 +11,8 @@ def initialize(responses)
9 11
       
10 12
       for method in [ :post, :put, :get, :delete ]
11 13
         module_eval <<-EOE
12  
-          def #{method}(path, body = nil, status = 200, headers = {})
13  
-            @responses[Request.new(:#{method}, path, nil)] = Response.new(body || {}, status, headers)
  14
+          def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {})
  15
+            @responses[Request.new(:#{method}, path, nil, request_headers)] = Response.new(body || {}, status, response_headers)
14 16
           end
15 17
         EOE
16 18
       end
@@ -39,12 +41,22 @@ def reset!
39 41
       end
40 42
     end
41 43
 
42  
-    for method in [ :post, :put, :get, :delete ]
  44
+    for method in [ :post, :put ]
  45
+      module_eval <<-EOE
  46
+        def #{method}(path, body, headers)
  47
+          request = ActiveResource::Request.new(:#{method}, path, body, headers)
  48
+          self.class.requests << request
  49
+          self.class.responses[request] || raise(InvalidRequestError.new("No response recorded for: \#{request}"))
  50
+        end
  51
+      EOE
  52
+    end
  53
+    
  54
+    for method in [ :get, :delete ]
43 55
       module_eval <<-EOE
44  
-        def #{method}(*arguments)
45  
-          request = ActiveResource::Request.new(:#{method}, *arguments)
  56
+        def #{method}(path, headers)
  57
+          request = ActiveResource::Request.new(:#{method}, path, nil, headers)
46 58
           self.class.requests << request
47  
-          self.class.responses[request] || raise("No response recorded for: \#{request}")
  59
+          self.class.responses[request] || raise(InvalidRequestError.new("No response recorded for: \#{request}"))
48 60
         end
49 61
       EOE
50 62
     end
@@ -55,10 +67,11 @@ def initialize(site)
55 67
   end
56 68
 
57 69
   class Request
58  
-    attr_accessor :path, :method, :body
  70
+    attr_accessor :path, :method, :body, :headers
59 71
     
60 72
     def initialize(method, path, body = nil, headers = nil)
61  
-      @method, @path, @body = method, path, body
  73
+      @method, @path, @body, @headers = method, path, body, headers
  74
+      @headers.update('Content-Type' => 'application/xml')
62 75
     end
63 76
 
64 77
     def ==(other_request)
@@ -70,11 +83,11 @@ def eql?(other_request)
70 83
     end
71 84
     
72 85
     def to_s
73  
-      "<#{method.to_s.upcase}: #{path} (#{body})>"
  86
+      "<#{method.to_s.upcase}: #{path} [#{headers}] (#{body})>"
74 87
     end
75 88
     
76 89
     def hash
77  
-      "#{path}#{method}".hash
  90
+      "#{path}#{method}#{headers}".hash
78 91
     end
79 92
   end
80 93
   

0 notes on commit 7ac6ed8

Please sign in to comment.
Something went wrong with that request. Please try again.