summaryrefslogtreecommitdiff
path: root/chef/spec/unit/rest_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'chef/spec/unit/rest_spec.rb')
-rw-r--r--chef/spec/unit/rest_spec.rb798
1 files changed, 468 insertions, 330 deletions
diff --git a/chef/spec/unit/rest_spec.rb b/chef/spec/unit/rest_spec.rb
index 254ae832b7..f36a020fca 100644
--- a/chef/spec/unit/rest_spec.rb
+++ b/chef/spec/unit/rest_spec.rb
@@ -1,15 +1,17 @@
#
# Author:: Adam Jacob (<adam@opscode.com>)
# Author:: Christopher Brown (<cb@opscode.com>)
+# Author:: Daniel DeLeo (<dan@opscode.com>)
# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
-#
+#
# http://www.apache.org/licenses/LICENSE-2.0
-#
+#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,403 +22,539 @@
require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper"))
require 'uri'
require 'net/https'
+require 'stringio'
+
+SIGNING_KEY_DOT_PEM=<<-END_RSA_KEY
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA49TA0y81ps0zxkOpmf5V4/c4IeR5yVyQFpX3JpxO4TquwnRh
+8VSUhrw8kkTLmB3cS39Db+3HadvhoqCEbqPE6915kXSuk/cWIcNozujLK7tkuPEy
+YVsyTioQAddSdfe+8EhQVf3oHxaKmUd6waXrWqYCnhxgOjxocenREYNhZ/OETIei
+PbOku47vB4nJK/0GhKBytL2XnsRgfKgDxf42BqAi1jglIdeq8lAWZNF9TbNBU21A
+O1iuT7Pm6LyQujhggPznR5FJhXKRUARXBJZawxpGV4dGtdcahwXNE4601aXPra+x
+PcRd2puCNoEDBzgVuTSsLYeKBDMSfs173W1QYwIDAQABAoIBAGF05q7vqOGbMaSD
+2Q7YbuE/JTHKTBZIlBI1QC2x+0P5GDxyEFttNMOVzcs7xmNhkpRw8eX1LrInrpMk
+WsIBKAFFEfWYlf0RWtRChJjNl+szE9jQxB5FJnWtJH/FHa78tR6PsF24aQyzVcJP
+g0FGujBihwgfV0JSCNOBkz8MliQihjQA2i8PGGmo4R4RVzGfxYKTIq9vvRq/+QEa
+Q4lpVLoBqnENpnY/9PTl6JMMjW2b0spbLjOPVwDaIzXJ0dChjNXo15K5SHI5mALJ
+I5gN7ODGb8PKUf4619ez194FXq+eob5YJdilTFKensIUvt3YhP1ilGMM+Chi5Vi/
+/RCTw3ECgYEA9jTw4wv9pCswZ9wbzTaBj9yZS3YXspGg26y6Ohq3ZmvHz4jlT6uR
+xK+DDcUiK4072gci8S4Np0fIVS7q6ivqcOdzXPrTF5/j+MufS32UrBbUTPiM1yoO
+ECcy+1szl/KoLEV09bghPbvC58PFSXV71evkaTETYnA/F6RK12lEepcCgYEA7OSy
+bsMrGDVU/MKJtwqyGP9ubA53BorM4Pp9VVVSCrGGVhb9G/XNsjO5wJC8J30QAo4A
+s59ZzCpyNRy046AB8jwRQuSwEQbejSdeNgQGXhZ7aIVUtuDeFFdaIz/zjVgxsfj4
+DPOuzieMmJ2MLR4F71ocboxNoDI7xruPSE8dDhUCgYA3vx732cQxgtHwAkeNPJUz
+dLiE/JU7CnxIoSB9fYUfPLI+THnXgzp7NV5QJN2qzMzLfigsQcg3oyo6F2h7Yzwv
+GkjlualIRRzCPaCw4Btkp7qkPvbs1QngIHALt8fD1N69P3DPHkTwjG4COjKWgnJq
+qoHKS6Fe/ZlbigikI6KsuwKBgQCTlSLoyGRHr6oj0hqz01EDK9ciMJzMkZp0Kvn8
+OKxlBxYW+jlzut4MQBdgNYtS2qInxUoAnaz2+hauqhSzntK3k955GznpUatCqx0R
+b857vWviwPX2/P6+E3GPdl8IVsKXCvGWOBZWTuNTjQtwbDzsUepWoMgXnlQJSn5I
+YSlLxQKBgQD16Gw9kajpKlzsPa6XoQeGmZALT6aKWJQlrKtUQIrsIWM0Z6eFtX12
+2jjHZ0awuCQ4ldqwl8IfRogWMBkHOXjTPVK0YKWWlxMpD/5+bGPARa5fir8O1Zpo
+Y6S6MeZ69Rp89ma4ttMZ+kwi1+XyHqC/dlcVRW42Zl5Dc7BALRlJjQ==
+-----END RSA PRIVATE KEY-----
+ END_RSA_KEY
+
describe Chef::REST do
before(:each) do
+ @log_stringio = StringIO.new
+ @logger = Logger.new(@log_stringio)
+ @original_chef_logger = Chef::Log.logger
+ Chef::Log.logger = @logger
+
Chef::REST::CookieJar.stub!(:instance).and_return({})
- @rest = Chef::REST.new("url", nil, nil)
- end
+ @base_url = "http://chef.example.com:4000"
+ @monkey_uri = URI.parse("http://chef.example.com:4000/monkey")
+ @rest = Chef::REST.new(@base_url, nil, nil)
- describe "load_signing_key" do
- before(:each) do
- @private_key = <<EOH
------BEGIN RSA PRIVATE KEY-----
-MIIEowIBAAKCAQEAx8xAfO2BO8kUughpjWwHPN2rgDcES15PbMEGe6OdJgjFARkt
-FMdEusbGxmXKpk51Ggxi2P6ZYEoZfniZWt4qSt4i1vanDayRlJ1qoRCOaYj5cQS7
-gpHspHWqkY3HfGvx4svdutQ06o/gypx2QYfi68YrIUQexPiTUhsnP9FlgNt40Rl1
-YgBiIlJUk7d3q+1b/+POTNKPeyjGK9hoTloplbSx+cYdZgc4/YpU0eLBoHPuPv5l
-QD+Y8VNS39bvY2NWbCqhV508gExAK26FxXTDNpi2mTZmbRZ8U0PKrCgF6gBSeod5
-EdQnNgoZHmA2fzfPHWfJd2OEuMcNM7DWpPDizQIDAQABAoIBAAGVDYGvw9E8Y2yh
-umxDSb9ipgQK637JTWm4EZwTDKCLezvp/iBm/5VXE6XoknVEs8q0BGhhg8dubstA
-mz5L+hvDrJT1ORdzoWeC46BI6EfPrOIHPpDnJO+cevBSJh1HIZBBOw1KtuyQnSAd
-oxYbxGFHnXnS90dqDIie7G2l897UWoiQWNMLY+A+l5H4GLC+4Phq02pLd4OQwXA3
-Nd+3Nq69aOeccyfSDeeG7u35TKrjQPIxU210aR18d/0trR20BKsKbT30GPE1tQQd
-jm4uReSPttTQ+NjwBQKKYmO2F9b9MPzmQ7c+KycBRmf+IOgZeZ54JN0GzUXsDTjJ
-+ZSgdgUCgYEA41aetBJwsKkF973gL54QCB5vFhRw3TdUgYhQgz04B5JGouGTSALy
-u1XtO6il65Zf6FwFSzXiggYYxTKyP/zwL88CQAVA7rleyhoZrw2bD6R2RZLivRba
-50rstltUbjevd96TagFY7i9gVHL9E6DKJH4unZfIM0Bl2IZQraqCR8MCgYEA4PzC
-FfUwiLa5drN6OVWZZfwxOeMbQUsYVeq7pHyeuvIe0euhcCLabBqfVt0pxqf1hem+
-l2+PnSKtvbI9siwt6WvJCtB3e/3aHOA3d6Y9TYxoyJAK007mRlQbbgqLzG83tZH2
-twO2tjo+h1+nv5yjE7aF9ItszegwTWsupvR+Ei8CgYAy0nt6MCEnLTIbV0RWANT+
-q6cT3Y/5tFPc/Vdab4YmEypdYWZmk9olzSjSzHoDN8PLEz9PuAUiIjDJbPLyYR5k
-4bdUDpicha5OKhWRz83Zal/SX+r2cLSRPmu6vKIcXbCJcKWt7g0uekLjvi0bhTeL
-fvX23yavZnceN7Czkkm7twKBgEFTgrNHdykrDRzXLhT5ssm2+UAani5ONKm1t3gi
-KyCS7rn7FevuYsdiz4M0Qk4JNLQGU6262dNBX3smBt32D/qnrj8ymo7o/WzG+bQH
-E+OxcjdSA6KpVRl0kGZaL49Td7SDxkQLkwDEVqWN87IiNAOkSq7f0N7UnTnNdkVJ
-1lVHAoGBANYgMoEj7gIJdch7hMdQcFfq9+4ntAAbsl3JFW+T9ChATn0XHAylP9ha
-ZaGlRrC7vxcF06vMe0HXyH1XVK3J9186zliTa4oDjkQ0D5X7Ga7KktLXAmQTysUH
-V3jwIQbAF6LqLUnGOq6rJzQxrWKvFt0mVDyuJzIJGSbnN/Sl5J6P
------END RSA PRIVATE KEY-----
-EOH
- IO.stub!(:read).and_return(@private_key)
- end
+ Chef::REST::CookieJar.instance.clear
+ end
+
+ after do
+ Chef::Log.logger = @original_chef_logger
+ end
- it "should return the contents of the key file" do
- File.stub!(:exists?).and_return(true)
- File.stub!(:readable?).and_return(true)
- @rest.load_signing_key("/tmp/keyfile.pem").should be(@private_key)
+ describe "calling an HTTP verb on a path or absolute URL" do
+ it "adds a relative URL to the base url it was initialized with" do
+ @rest.create_url("foo/bar/baz").should == URI.parse(@base_url + "/foo/bar/baz")
end
- it "should raise a Chef::Exceptions::PrivateKeyMissing exception if the key cannot be found" do
- IO.stub!(:read).and_raise(IOError)
- lambda {
- @rest.load_signing_key("/tmp/keyfile.pem")
- }.should raise_error(Chef::Exceptions::PrivateKeyMissing)
+ it "replaces the base URL when given an absolute URL" do
+ @rest.create_url("http://chef-rulez.example.com:9000").should == URI.parse("http://chef-rulez.example.com:9000")
end
- end
-
- describe "get_rest" do
- it "should create a url from the path and base url" do
- URI.should_receive(:parse).with("url/monkey")
- @rest.stub!(:run_request)
+ it "makes a :GET request with the composed url object" do
+ @rest.should_receive(:api_request).with(:GET, @monkey_uri, {})
@rest.get_rest("monkey")
end
-
- it "should call run_request :GET with the composed url object" do
- URI.stub!(:parse).and_return(true)
- @rest.should_receive(:run_request).with(:GET, true, {}, false, 10, false).and_return(true)
- @rest.get_rest("monkey")
- end
- end
- describe "delete_rest" do
- it "should create a url from the path and base url" do
- URI.should_receive(:parse).with("url/monkey")
- @rest.stub!(:run_request)
- @rest.delete_rest("monkey")
+ it "makes a :GET reqest for a streaming download with the composed url" do
+ @rest.should_receive(:streaming_request).with(@monkey_uri, {})
+ @rest.get_rest("monkey", true)
end
-
- it "should call run_request :DELETE with the composed url object" do
- URI.stub!(:parse).and_return(true)
- @rest.should_receive(:run_request).with(:DELETE, true, {}).and_return(true)
+
+ it "makes a :DELETE request with the composed url object" do
+ @rest.should_receive(:api_request).with(:DELETE, @monkey_uri, {})
@rest.delete_rest("monkey")
end
- end
- describe "post_rest" do
- it "should create a url from the path and base url" do
- URI.should_receive(:parse).with("url/monkey")
- @rest.stub!(:run_request)
- @rest.post_rest("monkey", "data")
- end
-
- it "should call run_request :POST with the composed url object and data" do
- URI.stub!(:parse).and_return(true)
- @rest.should_receive(:run_request).with(:POST, true, {}, "data").and_return(true)
+ it "makes a :POST request with the composed url object and data" do
+ @rest.should_receive(:api_request).with(:POST, @monkey_uri, {}, "data")
@rest.post_rest("monkey", "data")
end
- end
- describe "put_rest" do
- it "should create a url from the path and base url" do
- URI.should_receive(:parse).with("url/monkey")
- @rest.stub!(:run_request)
- @rest.put_rest("monkey", "data")
- end
-
- it "should call run_request :PUT with the composed url object and data" do
- URI.stub!(:parse).and_return(true)
- @rest.should_receive(:run_request).with(:PUT, true, {}, "data").and_return(true)
+ it "makes a :PUT request with the composed url object and data" do
+ @rest.should_receive(:api_request).with(:PUT, @monkey_uri, {}, "data")
@rest.put_rest("monkey", "data")
end
end
- describe Chef::REST, "run_request method" do
- before(:each) do
- @url_mock = mock("URI", :null_object => true)
- @url_mock.stub!(:host).and_return("one")
- @url_mock.stub!(:port).and_return("80")
- @url_mock.stub!(:path).and_return("/")
- @url_mock.stub!(:query).and_return("foo=bar")
- @url_mock.stub!(:scheme).and_return("https")
- @url_mock.stub!(:to_s).and_return("https://one:80/?foo=bar")
- @http_response_mock = mock("Net::HTTPSuccess", :null_object => true)
- @http_response_mock.stub!(:kind_of?).with(Net::HTTPSuccess).and_return(true)
- @http_response_mock.stub!(:body).and_return("ninja")
- @http_response_mock.stub!(:error!).and_return(true)
- @http_response_mock.stub!(:header).and_return({ 'Content-Length' => "5" })
- @http_mock = mock("Net::HTTP", :null_object => true)
- @http_mock.stub!(:verify_mode=).and_return(true)
- @http_mock.stub!(:read_timeout=).and_return(true)
- @http_mock.stub!(:use_ssl=).with(true).and_return(true)
- @data_mock = mock("Data", :null_object => true)
- @data_mock.stub!(:to_json).and_return('{ "one": "two" }')
- @request_mock = mock("Request", :null_object => true)
- @request_mock.stub!(:body=).and_return(true)
- @request_mock.stub!(:method).and_return(true)
- @request_mock.stub!(:path).and_return(true)
- @http_mock.stub!(:request).and_return(@http_response_mock)
- @tf_mock = mock(Tempfile, { :print => true, :close => true, :write => true })
- Tempfile.stub!(:new).with("chef-rest").and_return(@tf_mock)
+
+ describe "when configured to authenticate to the Chef server" do
+ before do
+ @url = URI.parse("http://chef.example.com:4000")
+ Chef::Config[:node_name] = "webmonkey.example.com"
+ Chef::Config[:client_key] = CHEF_SPEC_DATA + "/ssl/private_key.pem"
+ @rest = Chef::REST.new(@url)
end
-
- def do_run_request(method=:GET, data=false, limit=10, raw=false)
- Net::HTTP.stub!(:new).and_return(@http_mock)
- @rest.run_request(method, @url_mock, {}, data, limit, raw)
+
+ it "configures itself to use the node_name and client_key in the config by default" do
+ @rest.client_name.should == "webmonkey.example.com"
+ @rest.signing_key_filename.should == CHEF_SPEC_DATA + "/ssl/private_key.pem"
end
- it "should always include the X-Chef-Version header" do
- Net::HTTP::Get.should_receive(:new).with("/?foo=bar",
- { 'Accept' => 'application/json', 'X-Chef-Version' => Chef::VERSION }
- ).and_return(@request_mock)
- do_run_request
+ it "provides access to the raw key data" do
+ @rest.signing_key.should == SIGNING_KEY_DOT_PEM
end
-
- it "should raise an exception if the redirect limit is 0" do
- lambda { @rest.run_request(:GET, "/", {}, false, 0)}.should raise_error(ArgumentError)
+
+ it "does not error out when initialized without credentials" do
+ @rest = Chef::REST.new(@url, nil, nil) #should_not raise_error hides the bt from you, so screw it.
+ @rest.client_name.should be_nil
+ @rest.signing_key.should be_nil
end
-
- it "should use SSL if the url starts with https" do
- @url_mock.should_receive(:scheme).and_return("https")
- @http_mock.should_receive(:use_ssl=).with(true).and_return(true)
- do_run_request
+
+ it "indicates that requests should not be signed when it has no credentials" do
+ @rest = Chef::REST.new(@url, nil, nil)
+ @rest.sign_requests?.should be_false
end
-
- it "should set the OpenSSL Verify Mode to verify_none if requested" do
- @http_mock.should_receive(:verify_mode=).and_return(true)
- do_run_request
+
+ end
+
+ context "when making REST requests" do
+ before(:each) do
+ Chef::Config[:ssl_client_cert] = nil
+ Chef::Config[:ssl_client_key] = nil
+ @url = URI.parse("https://one:80/?foo=bar")
+
+ @http_response = Net::HTTPSuccess.new("1.1", "200", "successful rest req")
+ @http_response.stub!(:read_body)
+ @http_response.stub!(:body).and_return("ninja")
+ @http_response.add_field("Content-Length", "5")
+
+ @http_client = Net::HTTP.new(@url.host, @url.port)
+ Net::HTTP.stub!(:new).and_return(@http_client)
+ @http_client.stub!(:request).and_yield(@http_response).and_return(@http_response)
end
-
- describe "with OpenSSL Verify Mode set to :verify peer" do
- before(:each) do
- Chef::Config[:ssl_verify_mode] = :verify_peer
- @url_mock.should_receive(:scheme).and_return("https")
+
+ describe "using the run_request API" do
+ it "should build a new HTTP GET request" do
+ request = Net::HTTP::Get.new(@url.path)
+ Net::HTTP::Get.should_receive(:new).with("/?foo=bar",
+ { 'Accept' => 'application/json', 'X-Chef-Version' => Chef::VERSION }
+ ).and_return(request)
+ @rest.run_request(:GET, @url, {})
end
- after(:each) do
- Chef::Config[:ssl_verify_mode] = :verify_none
+ it "should build a new HTTP POST request" do
+ request = Net::HTTP::Post.new(@url.path)
+
+ Net::HTTP::Post.should_receive(:new).with("/?foo=bar",
+ { 'Accept' => 'application/json', "Content-Type" => 'application/json', 'X-Chef-Version' => Chef::VERSION }
+ ).and_return(request)
+ @rest.run_request(:POST, @url, {}, {:one=>:two})
+ request.body.should == '{"one":"two"}'
end
- it "should set the OpenSSL Verify Mode to verify_peer if requested" do
- @http_mock.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER).and_return(true)
- do_run_request
+ it "should build a new HTTP PUT request" do
+ request = Net::HTTP::Put.new(@url.path)
+ Net::HTTP::Put.should_receive(:new).with("/?foo=bar",
+ { 'Accept' => 'application/json', "Content-Type" => 'application/json', 'X-Chef-Version' => Chef::VERSION }
+ ).and_return(request)
+ @rest.run_request(:PUT, @url, {}, {:one=>:two})
+ request.body.should == '{"one":"two"}'
end
- it "should set the CA path if that is set in the configuration" do
- Chef::Config[:ssl_ca_path] = File.join(File.dirname(__FILE__), "..", "data", "ssl")
- @http_mock.should_receive(:ca_path=).with(Chef::Config[:ssl_ca_path]).and_return(true)
- do_run_request
- Chef::Config[:ssl_ca_path] = nil
+ it "should build a new HTTP DELETE request" do
+ request = Net::HTTP::Delete.new(@url.path)
+ Net::HTTP::Delete.should_receive(:new).with("/?foo=bar",
+ { 'Accept' => 'application/json', 'X-Chef-Version' => Chef::VERSION }
+ ).and_return(request)
+ @rest.run_request(:DELETE, @url)
end
- it "should set the CA file if that is set in the configuration" do
- Chef::Config[:ssl_ca_file] = File.join(File.dirname(__FILE__), "..", "data", "ssl", "5e707473.0")
- @http_mock.should_receive(:ca_file=).with(Chef::Config[:ssl_ca_file]).and_return(true)
- do_run_request
- Chef::Config[:ssl_ca_file] = nil
+ it "should raise an error if the method is not GET/PUT/POST/DELETE" do
+ lambda { @rest.api_request(:MONKEY, @url) }.should raise_error(ArgumentError)
end
- end
- describe "with a client SSL cert" do
- before(:each) do
- Chef::Config[:ssl_client_cert] = "/etc/chef/client-cert.pem"
- Chef::Config[:ssl_client_key] = "/etc/chef/client-cert.key"
- File.stub!(:exists?).with("/etc/chef/client-cert.pem").and_return(true)
- File.stub!(:exists?).with("/etc/chef/client-cert.key").and_return(true)
- File.stub!(:read).with("/etc/chef/client-cert.pem").and_return("monkey magic client")
- File.stub!(:read).with("/etc/chef/client-cert.key").and_return("monkey magic key")
- OpenSSL::X509::Certificate.stub!(:new).and_return("monkey magic client data")
- OpenSSL::PKey::RSA.stub!(:new).and_return("monkey magic key data")
+ it "returns the response body when the response is successful but content-type is not JSON" do
+ @rest.run_request(:GET, @url).should == "ninja"
end
- it "should check that the client cert file exists" do
- File.should_receive(:exists?).with("/etc/chef/client-cert.pem").and_return(true)
- do_run_request
+ it "should call read_body without a block if the request is not raw" do
+ @http_response.should_receive(:read_body)
+ @rest.run_request(:GET, @url, {}, nil, false)
end
- it "should read the cert file" do
- File.should_receive(:read).with("/etc/chef/client-cert.pem").and_return("monkey magic client")
- do_run_request
+ it "should inflate the body as to an object if JSON is returned" do
+ @http_response.add_field("content-type", "application/json")
+ JSON.should_receive(:parse).with("ninja").and_return("ohai2u_success")
+ @rest.run_request(:GET, @url, {}).should == "ohai2u_success"
end
- it "should read the cert into OpenSSL" do
- OpenSSL::X509::Certificate.should_receive(:new).and_return("monkey magic client data")
- do_run_request
+ it "should call run_request again on a Redirect response" do
+ http_response = Net::HTTPFound.new("1.1", "302", "bob is taking care of that one for me today")
+ http_response.add_field("location", @url.path)
+ http_response.stub!(:read_body)
+
+ @http_client.stub!(:request).and_yield(http_response).and_return(http_response)
+ lambda { @rest.run_request(:GET, @url) }.should raise_error(Chef::Exceptions::RedirectLimitExceeded)
end
- it "should set the cert" do
- @http_mock.should_receive(:cert=).and_return(true)
- do_run_request
+ it "should call run_request again on a Permanent Redirect response" do
+ http_response = Net::HTTPMovedPermanently.new("1.1", "301", "That's Bob's job")
+ http_response.add_field("location", @url.path)
+ http_response.stub!(:read_body)
+ @http_client.stub!(:request).and_yield(http_response).and_return(http_response)
+ lambda { @rest.run_request(:GET, @url) }.should raise_error(Chef::Exceptions::RedirectLimitExceeded)
end
- it "should read the key file" do
- File.should_receive(:read).with("/etc/chef/client-cert.key").and_return("monkey magic key")
- do_run_request
+ it "should show the JSON error message on an unsuccessful request" do
+ http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth")
+ http_response.add_field("content-type", "application/json")
+ http_response.stub!(:body).and_return('{ "error":[ "Ears get sore!", "Not even four" ] }')
+ http_response.stub!(:read_body)
+ @http_client.stub!(:request).and_yield(http_response).and_return(http_response)
+ lambda {@rest.run_request(:GET, @url)}.should raise_error(Net::HTTPFatalError)
+ @log_stringio.string.should match(Regexp.escape('WARN -- : HTTP Request Returned 500 drooling from inside of mouth: Ears get sore!, Not even four'))
end
- it "should read the key into OpenSSL" do
- OpenSSL::PKey::RSA.should_receive(:new).and_return("monkey magic key data")
- do_run_request
+ it "should raise an exception on an unsuccessful request" do
+ @http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth")
+ http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth")
+ http_response.stub!(:read_body)
+ @http_client.stub!(:request).and_yield(http_response).and_return(http_response)
+ lambda {@rest.run_request(:GET, @url)}.should raise_error(Net::HTTPFatalError)
end
- it "should set the key" do
- @http_mock.should_receive(:key=).and_return(true)
- do_run_request
+ describe "streaming downloads to a tempfile" do
+ before do
+ @tempfile = Tempfile.open("chef-rspec-rest_spec-line-#{__LINE__}--")
+ Tempfile.stub!(:new).with("chef-rest").and_return(@tempfile)
+ Tempfile.stub!(:open).and_return(@tempfile)
+ end
+
+ after do
+ @tempfile.rspec_reset
+ @tempfile.close!
+ end
+
+ it "should build a new HTTP GET request without the application/json accept header" do
+ Net::HTTP::Get.should_receive(:new).with("/?foo=bar", {'X-Chef-Version' => Chef::VERSION}).and_return(@request_mock)
+ @rest.run_request(:GET, @url, {}, false, nil, true)
+ end
+
+ it "should create a tempfile for the output of a raw request" do
+ @rest.run_request(:GET, @url, {}, false, nil, true).should equal(@tempfile)
+ end
+
+ it "should read the body of the response in chunks on a raw request" do
+ @http_response.should_receive(:read_body).and_return(true)
+ @rest.run_request(:GET, @url, {}, false, nil, true)
+ end
+
+ it "should populate the tempfile with the value of the raw request" do
+ @http_response_mock.stub!(:read_body).and_yield("ninja")
+ @tempfile.should_receive(:write).with("ninja").once.and_return(true)
+ @rest.run_request(:GET, @url, {}, false, nil, true)
+ end
+
+ it "should close the tempfile if we're doing a raw request" do
+ @tempfile.should_receive(:close).once.and_return(true)
+ @rest.run_request(:GET, @url, {}, false, nil, true)
+ end
+
+ it "should not raise a divide by zero exception if the size is 0" do
+ @http_response_mock.stub!(:header).and_return({ 'Content-Length' => "5" })
+ @http_response_mock.stub!(:read_body).and_yield('')
+ lambda { @rest.run_request(:GET, @url, {}, false, nil, true) }.should_not raise_error(ZeroDivisionError)
+ end
+
+ it "should not raise a divide by zero exception if the Content-Length is 0" do
+ @http_response_mock.stub!(:header).and_return({ 'Content-Length' => "0" })
+ @http_response_mock.stub!(:read_body).and_yield("ninja")
+ lambda { @rest.run_request(:GET, @url, {}, false, nil, true) }.should_not raise_error(ZeroDivisionError)
+ end
+
end
end
- it "should set a read timeout based on the rest_timeout config option" do
- Chef::Config[:rest_timeout] = 10
- @http_mock.should_receive(:read_timeout=).with(10).and_return(true)
- do_run_request
- end
-
- it "should set the cookie for this request if one exists for the given host:port" do
- @rest.cookies = { "#{@url_mock.host}:#{@url_mock.port}" => "cookie monster" }
- Net::HTTP::Get.should_receive(:new).with("/?foo=bar",
- { 'Accept' => 'application/json', 'X-Chef-Version' => Chef::VERSION, 'Cookie' => 'cookie monster' }
- ).and_return(@request_mock)
- do_run_request
- @rest.cookies = Hash.new
- end
-
- it "should build a new HTTP GET request" do
- Net::HTTP::Get.should_receive(:new).with("/?foo=bar",
- { 'Accept' => 'application/json', 'X-Chef-Version' => Chef::VERSION }
- ).and_return(@request_mock)
- do_run_request
- end
-
- it "should build a new HTTP POST request" do
- Net::HTTP::Post.should_receive(:new).with("/?foo=bar",
- { 'Accept' => 'application/json', "Content-Type" => 'application/json', 'X-Chef-Version' => Chef::VERSION }
- ).and_return(@request_mock)
- do_run_request(:POST, @data_mock)
- end
-
- it "should build a new HTTP PUT request" do
- Net::HTTP::Put.should_receive(:new).with("/?foo=bar",
- { 'Accept' => 'application/json', "Content-Type" => 'application/json', 'X-Chef-Version' => Chef::VERSION }
- ).and_return(@request_mock)
- do_run_request(:PUT, @data_mock)
- end
-
- it "should build a new HTTP DELETE request" do
- Net::HTTP::Delete.should_receive(:new).with("/?foo=bar",
- { 'Accept' => 'application/json', 'X-Chef-Version' => Chef::VERSION }
- ).and_return(@request_mock)
- do_run_request(:DELETE)
- end
-
- it "should raise an error if the method is not GET/PUT/POST/DELETE" do
- lambda { do_run_request(:MONKEY) }.should raise_error(ArgumentError)
- end
-
- it "should run an http request" do
- @http_mock.should_receive(:request).and_return(@http_response_mock)
- do_run_request
- end
-
- it "should return the body of the response on success" do
- do_run_request.should eql("ninja")
- end
-
- it "should inflate the body as to an object if JSON is returned" do
- @http_response_mock.stub!(:[]).with('content-type').and_return("application/json")
- JSON.should_receive(:parse).with("ninja").and_return(true)
- do_run_request
- end
-
- it "should call run_request again on a Redirect response" do
- @http_response_mock.stub!(:kind_of?).with(Net::HTTPSuccess).and_return(false)
- @http_response_mock.stub!(:kind_of?).with(Net::HTTPFound).and_return(true)
- @http_response_mock.stub!(:[]).with('location').and_return(@url_mock.path)
- lambda { do_run_request(method=:GET, data=false, limit=1) }.should raise_error(ArgumentError)
- end
+ describe "as JSON API requests" do
+ it "should always include the X-Chef-Version header" do
+ Net::HTTP::Get.should_receive(:new).with("/?foo=bar",
+ { 'Accept' => 'application/json', 'X-Chef-Version' => Chef::VERSION }
+ ).and_return(@request_mock)
+ @rest.api_request(:GET, @url, {})
+ end
- it "should call run_request again on a Permanent Redirect response" do
- @http_response_mock.stub!(:kind_of?).with(Net::HTTPSuccess).and_return(false)
- @http_response_mock.stub!(:kind_of?).with(Net::HTTPFound).and_return(false)
- @http_response_mock.stub!(:kind_of?).with(Net::HTTPMovedPermanently).and_return(true)
- @http_response_mock.stub!(:[]).with('location').and_return(@url_mock.path)
- lambda { do_run_request(method=:GET, data=false, limit=1) }.should raise_error(ArgumentError)
- end
+ it "should set the cookie for this request if one exists for the given host:port" do
+ Chef::REST::CookieJar.instance["#{@url.host}:#{@url.port}"] = "cookie monster"
+ Net::HTTP::Get.should_receive(:new).with("/?foo=bar",
+ { 'Accept' => 'application/json', 'X-Chef-Version' => Chef::VERSION, 'Cookie' => 'cookie monster' }
+ ).and_return(@request_mock)
+ @rest.api_request(:GET, @url, {})
+ end
- it "should show the JSON error message on an unsuccessful request" do
- @http_response_mock.stub!(:kind_of?).with(Net::HTTPSuccess).and_return(false)
- @http_response_mock.stub!(:kind_of?).with(Net::HTTPFound).and_return(false)
- @http_response_mock.stub!(:kind_of?).with(Net::HTTPMovedPermanently).and_return(false)
- @http_response_mock.stub!(:[]).with('content-type').and_return('application/json')
- @http_response_mock.stub!(:body).and_return('{ "error":[ "Ears get sore!", "Not even four" ] }')
- @http_response_mock.stub!(:code).and_return(500)
- @http_response_mock.stub!(:message).and_return('Server Error')
- ## BUGBUG - this should absolutely be working, but it.. isn't.
- #Chef::Log.should_receive(:warn).with("HTTP Request Returned 500 Server Error: Ears get sore!, Not even four")
- @http_response_mock.should_receive(:error!)
- do_run_request
- end
-
- it "should raise an exception on an unsuccessful request" do
- @http_response_mock.stub!(:kind_of?).with(Net::HTTPSuccess).and_return(false)
- @http_response_mock.stub!(:kind_of?).with(Net::HTTPFound).and_return(false)
- @http_response_mock.stub!(:kind_of?).with(Net::HTTPMovedPermanently).and_return(false)
- @http_response_mock.should_receive(:error!)
- do_run_request
- end
-
- it "should build a new HTTP GET request without the application/json accept header for raw reqs" do
- Net::HTTP::Get.should_receive(:new).with("/?foo=bar", {'X-Chef-Version' => Chef::VERSION}).and_return(@request_mock)
- do_run_request(:GET, false, 10, true)
+ it "should build a new HTTP GET request" do
+ Net::HTTP::Get.should_receive(:new).with("/?foo=bar",
+ { 'Accept' => 'application/json', 'X-Chef-Version' => Chef::VERSION }
+ ).and_return(@request_mock)
+ @rest.api_request(:GET, @url, {})
+ end
+
+ it "should build a new HTTP POST request" do
+ request = Net::HTTP::Post.new(@url.path)
+
+ Net::HTTP::Post.should_receive(:new).with("/?foo=bar",
+ { 'Accept' => 'application/json', "Content-Type" => 'application/json', 'X-Chef-Version' => Chef::VERSION }
+ ).and_return(request)
+ @rest.api_request(:POST, @url, {}, {:one=>:two})
+ request.body.should == '{"one":"two"}'
+ end
+
+ it "should build a new HTTP PUT request" do
+ request = Net::HTTP::Put.new(@url.path)
+ Net::HTTP::Put.should_receive(:new).with("/?foo=bar",
+ { 'Accept' => 'application/json', "Content-Type" => 'application/json', 'X-Chef-Version' => Chef::VERSION }
+ ).and_return(request)
+ @rest.api_request(:PUT, @url, {}, {:one=>:two})
+ request.body.should == '{"one":"two"}'
+ end
+
+ it "should build a new HTTP DELETE request" do
+ Net::HTTP::Delete.should_receive(:new).with("/?foo=bar",
+ { 'Accept' => 'application/json', 'X-Chef-Version' => Chef::VERSION }
+ ).and_return(@request_mock)
+ @rest.api_request(:DELETE, @url)
+ end
+
+ it "should raise an error if the method is not GET/PUT/POST/DELETE" do
+ lambda { @rest.api_request(:MONKEY, @url) }.should raise_error(ArgumentError)
+ end
+
+ it "returns nil when the response is successful but content-type is not JSON" do
+ @rest.api_request(:GET, @url).should == "ninja"
+ end
+
+ it "should inflate the body as to an object if JSON is returned" do
+ @http_response.add_field('content-type', "application/json")
+ @http_response.stub!(:body).and_return('{"ohai2u":"json_api"}')
+ @rest.api_request(:GET, @url, {}).should == {"ohai2u"=>"json_api"}
+ end
+
+ it "should call run_request again on a Redirect response" do
+ http_response = Net::HTTPFound.new("1.1", "302", "bob is taking care of that one for me today")
+ http_response.add_field("location", @url.path)
+ http_response.stub!(:read_body)
+
+ @http_client.stub!(:request).and_yield(http_response).and_return(http_response)
+
+ lambda { @rest.api_request(:GET, @url) }.should raise_error(Chef::Exceptions::RedirectLimitExceeded)
+ end
+
+ it "should call run_request again on a Permanent Redirect response" do
+ http_response = Net::HTTPMovedPermanently.new("1.1", "301", "That's Bob's job")
+ http_response.add_field("location", @url.path)
+ http_response.stub!(:read_body)
+ @http_client.stub!(:request).and_yield(http_response).and_return(http_response)
+
+ lambda { @rest.api_request(:GET, @url) }.should raise_error(Chef::Exceptions::RedirectLimitExceeded)
+ end
+
+ it "should show the JSON error message on an unsuccessful request" do
+ http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth")
+ http_response.add_field("content-type", "application/json")
+ http_response.stub!(:body).and_return('{ "error":[ "Ears get sore!", "Not even four" ] }')
+ http_response.stub!(:read_body)
+ @http_client.stub!(:request).and_yield(http_response).and_return(http_response)
+
+ lambda {@rest.run_request(:GET, @url)}.should raise_error(Net::HTTPFatalError)
+ @log_stringio.string.should match(Regexp.escape('WARN -- : HTTP Request Returned 500 drooling from inside of mouth: Ears get sore!, Not even four'))
+ end
+
+ it "should raise an exception on an unsuccessful request" do
+ http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth")
+ http_response.stub!(:body)
+ http_response.stub!(:read_body)
+ @http_client.stub!(:request).and_yield(http_response).and_return(http_response)
+ lambda {@rest.api_request(:GET, @url)}.should raise_error(Net::HTTPFatalError)
+ end
end
-
- it "should create a tempfile for the output of a raw request" do
- @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock)
- Tempfile.should_receive(:new).with("chef-rest").and_return(@tf_mock)
- do_run_request(:GET, false, 10, true).should eql(@tf_mock)
+
+ context "when streaming downloads to a tempfile" do
+ before do
+ @tempfile = Tempfile.open("chef-rspec-rest_spec-line-#{__LINE__}--")
+ Tempfile.stub!(:new).with("chef-rest").and_return(@tempfile)
+ @http_response = Net::HTTPSuccess.new("1.1",200, "it-works")
+ @http_response.stub!(:read_body)
+ @http_client.stub!(:request).and_yield(@http_response).and_return(@http_response)
+ end
+
+ after do
+ @tempfile.rspec_reset
+ @tempfile.close!
+ end
+
+ it " build a new HTTP GET request without the application/json accept header" do
+ Net::HTTP::Get.should_receive(:new).with("/?foo=bar", {'X-Chef-Version' => Chef::VERSION}).and_return(@request_mock)
+ @rest.streaming_request(@url, {})
+ end
+
+ it "returns a tempfile containing the streamed response body" do
+ @rest.streaming_request(@url, {}).should equal(@tempfile)
+ end
+
+ it "writes the response body to a tempfile" do
+ @http_response.stub!(:read_body).and_yield("real").and_yield("ultimate").and_yield("power")
+ @rest.streaming_request(@url, {})
+ IO.read(@tempfile.path).chomp.should == "realultimatepower"
+ end
+
+ it "closes the tempfile" do
+ @rest.streaming_request(@url, {})
+ @tempfile.should be_closed
+ end
+
+ it "yields the tempfile containing the streamed response body and then unlinks it when given a block" do
+ @http_response.stub!(:read_body).and_yield("real").and_yield("ultimate").and_yield("power")
+ tempfile_path = nil
+ @rest.streaming_request(@url, {}) do |tempfile|
+ tempfile_path = tempfile.path
+ File.exist?(tempfile.path).should be_true
+ IO.read(@tempfile.path).chomp.should == "realultimatepower"
+ end
+ File.exist?(tempfile_path).should be_false
+ end
+
+ it "does not raise a divide by zero exception if the content's actual size is 0" do
+ @http_response.add_field('Content-Length', "5")
+ @http_response.stub!(:read_body).and_yield('')
+ lambda { @rest.streaming_request(@url, {}) }.should_not raise_error(ZeroDivisionError)
+ end
+
+ it "does not raise a divide by zero exception when the Content-Length is 0" do
+ @http_response.add_field('Content-Length', "0")
+ @http_response.stub!(:read_body).and_yield("ninja")
+ lambda { @rest.streaming_request(@url, {}) }.should_not raise_error(ZeroDivisionError)
+ end
+
+ it "fetches a file and yields the tempfile it is streamed to" do
+ @http_response.stub!(:read_body).and_yield("real").and_yield("ultimate").and_yield("power")
+ tempfile_path = nil
+ @rest.fetch("cookbooks/a_cookbook") do |tempfile|
+ tempfile_path = tempfile.path
+ IO.read(@tempfile.path).chomp.should == "realultimatepower"
+ end
+ File.exist?(tempfile_path).should be_false
+ end
+
+ it "closes and unlinks the tempfile if there is an error while streaming the content to the tempfile" do
+ @tempfile.stub!(:write).and_raise(IOError)
+ @rest.fetch("cookbooks/a_cookbook") {|tmpfile| "shouldn't get here"}
+ @tempfile.path.should be_nil
+ end
+
+ it "closes and unlinks the tempfile when the response is a redirect" do
+ Tempfile.rspec_reset
+ tempfile = mock("die", :path => "/tmp/ragefist", :close => true)
+ tempfile.should_receive(:close!).at_least(2).times
+ Tempfile.stub!(:new).with("chef-rest").and_return(tempfile)
+
+ http_response = Net::HTTPFound.new("1.1", "302", "bob is taking care of that one for me today")
+ http_response.add_field("location", @url.path)
+ http_response.stub!(:read_body)
+
+ @http_client.stub!(:request).and_yield(http_response).and_yield(@http_response).and_return(http_response, @http_response)
+ @rest.fetch("cookbooks/a_cookbook") {|tmpfile| "shouldn't get here"}
+ end
+
+ it "passes the original block to the redirected request" do
+ http_response = Net::HTTPFound.new("1.1", "302", "bob is taking care of that one for me today")
+ http_response.add_field("location","/that-thing-is-here-now")
+ http_response.stub!(:read_body)
+
+ block_called = false
+ @http_client.stub!(:request).and_yield(@http_response).and_return(http_response, @http_response)
+ @rest.fetch("cookbooks/a_cookbook") do |tmpfile|
+ block_called = true
+ end
+ block_called.should be_true
+ end
end
-
- it "should read the body of the response in chunks on a raw request" do
- @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock)
- @http_response_mock.should_receive(:read_body).and_return(true)
- do_run_request(:GET, false, 10, true)
+ end
+
+ context "when following redirects" do
+ before do
+ Chef::Config[:node_name] = "webmonkey.example.com"
+ Chef::Config[:client_key] = CHEF_SPEC_DATA + "/ssl/private_key.pem"
+ @rest = Chef::REST.new(@url)
end
-
- it "should populate the tempfile with the value of the raw request" do
- @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock)
- @http_response_mock.stub!(:read_body).and_yield("ninja")
- @tf_mock.should_receive(:write, "ninja").once.and_return(true)
- do_run_request(:GET, false, 10, true)
+
+ it "raises a RedirectLimitExceeded when redirected more than 10 times" do
+ redirected = lambda {@rest.follow_redirect { redirected.call }}
+ lambda {redirected.call}.should raise_error(Chef::Exceptions::RedirectLimitExceeded)
end
-
- it "should close the tempfile if we're doing a raw request" do
- @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock)
- @tf_mock.should_receive(:close).once.and_return(true)
- do_run_request(:GET, false, 10, true)
+
+ it "does not count redirects from previous calls against the redirect limit" do
+ total_redirects = 0
+ redirected = lambda do
+ @rest.follow_redirect do
+ total_redirects += 1
+ redirected.call unless total_redirects >= 9
+ end
+ end
+ lambda {redirected.call}.should_not raise_error(Chef::Exceptions::RedirectLimitExceeded)
+ total_redirects = 0
+ lambda {redirected.call}.should_not raise_error(Chef::Exceptions::RedirectLimitExceeded)
end
-
- it "should not raise a divide by zero exception if the size is 0" do
- @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock)
- @http_response_mock.stub!(:header).and_return({ 'Content-Length' => "5" })
- @http_response_mock.stub!(:read_body).and_yield('')
- lambda { do_run_request(:GET, false, 10, true) }.should_not raise_error(ZeroDivisionError)
+
+ it "does not sign the redirected request when sign_on_redirect is false" do
+ @rest.sign_on_redirect = false
+ @rest.follow_redirect { @rest.sign_requests?.should be_false }
end
-
- it "should not raise a divide by zero exception if the Content-Length is 0" do
- @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock)
- @http_response_mock.stub!(:header).and_return({ 'Content-Length' => "0" })
- @http_response_mock.stub!(:read_body).and_yield("ninja")
- lambda { do_run_request(:GET, false, 10, true) }.should_not raise_error(ZeroDivisionError)
+
+ it "resets sign_requests to the original value after following an unsigned redirect" do
+ @rest.sign_on_redirect = false
+ @rest.sign_requests?.should be_true
+
+ @rest.follow_redirect { @rest.sign_requests?.should be_false }
+ @rest.sign_requests?.should be_true
end
-
- it "should call read_body without a block if the request is not raw" do
- @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock)
- @http_response_mock.should_receive(:read_body)
- do_run_request(:GET, false, 10, false)
+
+ it "configures the redirect limit" do
+ total_redirects = 0
+ redirected = lambda do
+ @rest.follow_redirect do
+ total_redirects += 1
+ redirected.call unless total_redirects >= 9
+ end
+ end
+ lambda {redirected.call}.should_not raise_error(Chef::Exceptions::RedirectLimitExceeded)
+
+ total_redirects = 0
+ @rest.redirect_limit = 3
+ lambda {redirected.call}.should raise_error(Chef::Exceptions::RedirectLimitExceeded)
end
end
-
end
-