diff options
author | Thom May <thom@chef.io> | 2017-03-16 17:43:45 +0000 |
---|---|---|
committer | Thom May <thom@chef.io> | 2017-03-16 20:23:40 +0000 |
commit | 6a16e21869e430352a6d4955843b79e3b6a63ff0 (patch) | |
tree | 15752ad125f2b6c92d57998896ffc9b3cf6c168a | |
parent | 1f23881a95d1dfa63158c7c35d3f8723df97a758 (diff) | |
download | chef-6a16e21869e430352a6d4955843b79e3b6a63ff0.tar.gz |
Remove Chef::RESTtm/remove_chef_rest
Signed-off-by: Thom May <thom@chef.io>
-rw-r--r-- | RELEASE_NOTES.md | 3 | ||||
-rw-r--r-- | lib/chef/application/solo.rb | 1 | ||||
-rw-r--r-- | lib/chef/rest.rb | 210 | ||||
-rw-r--r-- | spec/functional/rest_spec.rb | 95 | ||||
-rw-r--r-- | spec/unit/rest/auth_credentials_spec.rb | 292 | ||||
-rw-r--r-- | spec/unit/rest_spec.rb | 753 |
6 files changed, 3 insertions, 1351 deletions
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 0219369b7b..28468a1667 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -89,3 +89,6 @@ process of merging the node attributes strings and other simple objects are dup' now correctly use the `node.dup` or `node.to_hash` methods, or you should mutate the object correctly through its precedence level like `node.default["some_string"] << "appending_this"`. +### The Chef::REST API has been removed + +It has been fully replaced with `Chef::ServerAPI` in chef-client code. diff --git a/lib/chef/application/solo.rb b/lib/chef/application/solo.rb index 1481338a9c..2705a930ae 100644 --- a/lib/chef/application/solo.rb +++ b/lib/chef/application/solo.rb @@ -23,7 +23,6 @@ require "chef/client" require "chef/config" require "chef/daemon" require "chef/log" -require "chef/rest" require "chef/config_fetcher" require "fileutils" require "chef/mixin/shell_out" diff --git a/lib/chef/rest.rb b/lib/chef/rest.rb deleted file mode 100644 index 0705ca9f5a..0000000000 --- a/lib/chef/rest.rb +++ /dev/null @@ -1,210 +0,0 @@ -#-- -# Author:: Adam Jacob (<adam@chef.io>) -# Author:: Thom May (<thom@clearairturbulence.org>) -# Author:: Nuo Yan (<nuo@chef.io>) -# Author:: Christopher Brown (<cb@chef.io>) -# Author:: Christopher Walters (<cw@chef.io>) -# Copyright:: Copyright 2009-2016, Chef Software 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. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require "tempfile" -require "chef/http" -class Chef - class HTTP; end - class REST < HTTP; end -end - -require "chef/http/authenticator" -require "chef/http/decompressor" -require "chef/http/json_input" -require "chef/http/json_to_model_output" -require "chef/http/cookie_manager" -require "chef/http/validate_content_length" -require "chef/config" -require "chef/exceptions" -require "chef/platform/query_helpers" -require "chef/http/remote_request_id" -require "chef/chef_class" - -class Chef - - # == Chef::REST - # Chef's custom REST client with built-in JSON support and RSA signed header - # authentication. - class REST < HTTP - - # Backwards compatibility for things that use - # Chef::REST::RESTRequest or its constants - RESTRequest = HTTP::HTTPRequest - - attr_accessor :url, :cookies, :sign_on_redirect, :redirect_limit - - attr_reader :authenticator - - # Create a REST client object. The supplied +url+ is used as the base for - # all subsequent requests. For example, when initialized with a base url - # http://localhost:4000, a call to +get_rest+ with 'nodes' will make an - # HTTP GET request to http://localhost:4000/nodes - def initialize(url, client_name = Chef::Config[:node_name], signing_key_filename = Chef::Config[:client_key], options = {}) - Chef.deprecated(:chef_rest, "Chef::REST is deprecated. Please use Chef::ServerAPI, or investigate Ridley or ChefAPI.") - - signing_key_filename = nil if chef_zero_uri?(url) - - options = options.dup - options[:client_name] = client_name - options[:signing_key_filename] = signing_key_filename - - super(url, options) - - @decompressor = Decompressor.new(options) - @authenticator = Authenticator.new(options) - @request_id = RemoteRequestID.new(options) - - @middlewares << JSONInput.new(options) - @middlewares << JSONToModelOutput.new(options) - @middlewares << CookieManager.new(options) - @middlewares << @decompressor - @middlewares << @authenticator - @middlewares << @request_id - - # ValidateContentLength should come after Decompressor - # because the order of middlewares is reversed when handling - # responses. - @middlewares << ValidateContentLength.new(options) - end - - def signing_key_filename - authenticator.signing_key_filename - end - - def auth_credentials - authenticator.auth_credentials - end - - def client_name - authenticator.client_name - end - - def signing_key - authenticator.raw_key - end - - def sign_requests? - authenticator.sign_requests? - end - - # Send an HTTP GET request to the path - # - # Using this method to +fetch+ a file is considered deprecated. - # - # === Parameters - # path:: The path to GET - # raw:: Whether you want the raw body returned, or JSON inflated. Defaults - # to JSON inflated. - def get(path, raw = false, headers = {}) - if raw - streaming_request(path, headers) - else - request(:GET, path, headers) - end - end - - alias :get_rest :get - - alias :delete_rest :delete - - alias :post_rest :post - - alias :put_rest :put - - # Streams a download to a tempfile, then yields the tempfile to a block. - # After the download, the tempfile will be closed and unlinked. - # If you rename the tempfile, it will not be deleted. - # Beware that if the server streams infinite content, this method will - # stream it until you run out of disk space. - def fetch(path, headers = {}) - streaming_request(create_url(path), headers) { |tmp_file| yield tmp_file } - end - - alias :api_request :request - - # Do a HTTP request where no middleware is loaded (e.g. JSON input/output - # conversion) but the standard Chef Authentication headers are added to the - # request. - def raw_http_request(method, path, headers, data) - url = create_url(path) - method, url, headers, data = @authenticator.handle_request(method, url, headers, data) - method, url, headers, data = @request_id.handle_request(method, url, headers, data) - response, rest_request, return_value = send_http_request(method, url, headers, data) - response.error! unless success_response?(response) - return_value - rescue Exception => exception - log_failed_request(response, return_value) unless response.nil? - - if exception.respond_to?(:chef_rest_request=) - exception.chef_rest_request = rest_request - end - raise - end - - # Deprecated: - # Responsibilities of this method have been split up. The #http_client is - # now responsible for making individual requests, while - # #retrying_http_errors handles error/retry logic. - def retriable_http_request(method, url, req_body, headers) - rest_request = Chef::HTTP::HTTPRequest.new(method, url, req_body, headers) - - Chef::Log.debug("Sending HTTP request via #{method} to #{url.host}:#{url.port}#{rest_request.path}") - - retrying_http_errors(url) do - yield rest_request - end - end - - # Customized streaming behavior; sets the accepted content type to "*/*" - # if not otherwise specified for compatibility purposes - def streaming_request(url, headers, &block) - headers["Accept"] ||= "*/*" - super - end - - alias :retriable_rest_request :retriable_http_request - - def follow_redirect - unless @sign_on_redirect - @authenticator.sign_request = false - end - super - ensure - @authenticator.sign_request = true - end - - public :create_url - - ############################################################################ - # DEPRECATED - ############################################################################ - - def decompress_body(body) - @decompressor.decompress_body(body) - end - - def authentication_headers(method, url, json_body = nil) - authenticator.authentication_headers(method, url, json_body) - end - - end -end diff --git a/spec/functional/rest_spec.rb b/spec/functional/rest_spec.rb deleted file mode 100644 index 14e76087c4..0000000000 --- a/spec/functional/rest_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -# -# Author:: Lamont Granquist (<lamont@chef.io>) -# Copyright:: Copyright 2014-2016, Chef Software, 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. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require "spec_helper" -require "tiny_server" -require "support/shared/functional/http" - -describe Chef::REST do - include ChefHTTPShared - - let(:http_client) { described_class.new(source) } - let(:http_client_disable_gzip) { described_class.new(source, Chef::Config[:node_name], Chef::Config[:client_key], { :disable_gzip => true } ) } - - shared_examples_for "downloads requests correctly" do - it "successfully downloads a streaming request" do - tempfile = http_client.streaming_request(source, {}) - tempfile.close - expect(Digest::MD5.hexdigest(binread(tempfile.path))).to eq(Digest::MD5.hexdigest(expected_content)) - end - - it "successfully downloads a GET request" do - tempfile = http_client.get(source, {}) - tempfile.close - expect(Digest::MD5.hexdigest(binread(tempfile.path))).to eq(Digest::MD5.hexdigest(expected_content)) - end - end - - shared_examples_for "validates content length and throws an exception" do - it "fails validation on a streaming download" do - expect { http_client.streaming_request(source, {}) }.to raise_error(Chef::Exceptions::ContentLengthMismatch) - end - - it "fails validation on a GET request" do - expect { http_client.get(source, {}) }.to raise_error(Chef::Exceptions::ContentLengthMismatch) - end - end - - shared_examples_for "an endpoint that 403s" do - it "fails with a Net::HTTPServerException on a streaming download" do - expect { http_client.streaming_request(source, {}) }.to raise_error(Net::HTTPServerException) - end - - it "fails with a Net::HTTPServerException on a GET request" do - expect { http_client.get(source, {}) }.to raise_error(Net::HTTPServerException) - end - end - - # see CHEF-5100 - shared_examples_for "a 403 after a successful request when reusing the request object" do - it "fails with a Net::HTTPServerException on a streaming download" do - tempfile = http_client.streaming_request(source, {}) - tempfile.close - expect(Digest::MD5.hexdigest(binread(tempfile.path))).to eq(Digest::MD5.hexdigest(expected_content)) - expect { http_client.streaming_request(source2, {}) }.to raise_error(Net::HTTPServerException) - end - - it "fails with a Net::HTTPServerException on a GET request" do - tempfile = http_client.get(source, {}) - tempfile.close - expect(Digest::MD5.hexdigest(binread(tempfile.path))).to eq(Digest::MD5.hexdigest(expected_content)) - expect { http_client.get(source2, {}) }.to raise_error(Net::HTTPServerException) - end - end - - before do - Chef::Config[:node_name] = "webmonkey.example.com" - Chef::Config[:client_key] = CHEF_SPEC_DATA + "/ssl/private_key.pem" - Chef::Config[:treat_deprecation_warnings_as_errors] = false - end - - before(:each) do - start_tiny_server - end - - after(:each) do - stop_tiny_server - end - - it_behaves_like "downloading all the things" -end diff --git a/spec/unit/rest/auth_credentials_spec.rb b/spec/unit/rest/auth_credentials_spec.rb deleted file mode 100644 index 2728463c81..0000000000 --- a/spec/unit/rest/auth_credentials_spec.rb +++ /dev/null @@ -1,292 +0,0 @@ -# -# Author:: Adam Jacob (<adam@chef.io>) -# Author:: Christopher Brown (<cb@chef.io>) -# Author:: Daniel DeLeo (<dan@chef.io>) -# Copyright:: Copyright 2008-2016, Chef Software Inc. -# Copyright:: Copyright 2010-2016, Chef Software 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. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require "spec_helper" -require "uri" -require "net/https" - -describe Chef::REST::AuthCredentials do - before do - @key_file_fixture = CHEF_SPEC_DATA + "/ssl/private_key.pem" - @key = OpenSSL::PKey::RSA.new(IO.read(@key_file_fixture).strip) - @auth_credentials = Chef::REST::AuthCredentials.new("client-name", @key) - end - - it "has a client name" do - expect(@auth_credentials.client_name).to eq("client-name") - end - - it "loads the private key when initialized with the path to the key" do - expect(@auth_credentials.key).to respond_to(:private_encrypt) - expect(@auth_credentials.key).to eq(@key) - end - - describe "when loading the private key" do - it "strips extra whitespace before checking the key" do - key_file_fixture = CHEF_SPEC_DATA + "/ssl/private_key_with_whitespace.pem" - expect { Chef::REST::AuthCredentials.new("client-name", @key_file_fixture) }.not_to raise_error - end - end - - describe "generating signature headers for a request" do - before do - @request_time = Time.at(1270920860) - @request_params = { :http_method => :POST, :path => "/clients", :body => '{"some":"json"}', :host => "localhost" } - allow(Chef::Config).to( - receive(:[]).with(:authentication_protocol_version).and_return(protocol_version)) - end - - context "when configured for version 1.0 of the authn protocol" do - let(:protocol_version) { "1.0" } - - it "generates signature headers for the request" do - allow(Time).to receive(:now).and_return(@request_time) - actual = @auth_credentials.signature_headers(@request_params) - expect(actual["HOST"]).to eq("localhost") - expect(actual["X-OPS-AUTHORIZATION-1"]).to eq("kBssX1ENEwKtNYFrHElN9vYGWS7OeowepN9EsYc9csWfh8oUovryPKDxytQ/") - expect(actual["X-OPS-AUTHORIZATION-2"]).to eq("Wc2/nSSyxdWJjjfHzrE+YrqNQTaArOA7JkAf5p75eTUonCWcvNPjFrZVgKGS") - expect(actual["X-OPS-AUTHORIZATION-3"]).to eq("yhzHJQh+lcVA9wwARg5Hu9q+ddS8xBOdm3Vp5atl5NGHiP0loiigMYvAvzPO") - expect(actual["X-OPS-AUTHORIZATION-4"]).to eq("r9853eIxwYMhn5hLGhAGFQznJbE8+7F/lLU5Zmk2t2MlPY8q3o1Q61YD8QiJ") - expect(actual["X-OPS-AUTHORIZATION-5"]).to eq("M8lIt53ckMyUmSU0DDURoiXLVkE9mag/6Yq2tPNzWq2AdFvBqku9h2w+DY5k") - expect(actual["X-OPS-AUTHORIZATION-6"]).to eq("qA5Rnzw5rPpp3nrWA9jKkPw4Wq3+4ufO2Xs6w7GCjA==") - expect(actual["X-OPS-CONTENT-HASH"]).to eq("1tuzs5XKztM1ANrkGNPah6rW9GY=") - expect(actual["X-OPS-SIGN"]).to match(%r{(version=1\.0)|(algorithm=sha1;version=1.0;)}) - expect(actual["X-OPS-TIMESTAMP"]).to eq("2010-04-10T17:34:20Z") - expect(actual["X-OPS-USERID"]).to eq("client-name") - end - end - - context "when configured for version 1.1 of the authn protocol" do - let(:protocol_version) { "1.1" } - - it "generates the correct signature for version 1.1" do - allow(Time).to receive(:now).and_return(@request_time) - actual = @auth_credentials.signature_headers(@request_params) - expect(actual["HOST"]).to eq("localhost") - expect(actual["X-OPS-CONTENT-HASH"]).to eq("1tuzs5XKztM1ANrkGNPah6rW9GY=") - expect(actual["X-OPS-SIGN"]).to eq("algorithm=sha1;version=1.1;") - expect(actual["X-OPS-TIMESTAMP"]).to eq("2010-04-10T17:34:20Z") - expect(actual["X-OPS-USERID"]).to eq("client-name") - - # mixlib-authN will test the actual signature stuff for each version of - # the protocol so we won't test it again here. - end - end - end -end - -describe Chef::REST::RESTRequest do - let(:url) { URI.parse("http://chef.example.com:4000/?q=chef_is_awesome") } - - def new_request(method = nil) - method ||= :POST - Chef::REST::RESTRequest.new(method, url, @req_body, @headers) - end - - before do - @auth_credentials = Chef::REST::AuthCredentials.new("client-name", CHEF_SPEC_DATA + "/ssl/private_key.pem") - @req_body = '{"json_data":"as_a_string"}' - @headers = { "Content-type" => "application/json", - "Accept" => "application/json", - "Accept-Encoding" => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, - "Host" => "chef.example.com:4000" } - @request = Chef::REST::RESTRequest.new(:POST, url, @req_body, @headers) - end - - it "stores the url it was created with" do - expect(@request.url).to eq(url) - end - - it "stores the HTTP method" do - expect(@request.method).to eq(:POST) - end - - it "adds the chef version header" do - expect(@request.headers).to eq(@headers.merge("X-Chef-Version" => ::Chef::VERSION)) - end - - describe "configuring the HTTP request" do - let(:url) do - URI.parse("http://homie:theclown@chef.example.com:4000/?q=chef_is_awesome") - end - - it "configures GET requests" do - @req_body = nil - rest_req = new_request(:GET) - expect(rest_req.http_request).to be_a_kind_of(Net::HTTP::Get) - expect(rest_req.http_request.path).to eq("/?q=chef_is_awesome") - expect(rest_req.http_request.body).to be_nil - end - - it "configures POST requests, including the body" do - expect(@request.http_request).to be_a_kind_of(Net::HTTP::Post) - expect(@request.http_request.path).to eq("/?q=chef_is_awesome") - expect(@request.http_request.body).to eq(@req_body) - end - - it "configures PUT requests, including the body" do - rest_req = new_request(:PUT) - expect(rest_req.http_request).to be_a_kind_of(Net::HTTP::Put) - expect(rest_req.http_request.path).to eq("/?q=chef_is_awesome") - expect(rest_req.http_request.body).to eq(@req_body) - end - - it "configures DELETE requests" do - rest_req = new_request(:DELETE) - expect(rest_req.http_request).to be_a_kind_of(Net::HTTP::Delete) - expect(rest_req.http_request.path).to eq("/?q=chef_is_awesome") - expect(rest_req.http_request.body).to be_nil - end - - it "configures HTTP basic auth" do - rest_req = new_request(:GET) - expect(rest_req.http_request.to_hash["authorization"]).to eq(["Basic aG9taWU6dGhlY2xvd24="]) - end - end - - describe "configuring the HTTP client" do - it "configures the HTTP client for the host and port" do - http_client = new_request.http_client - expect(http_client.address).to eq("chef.example.com") - expect(http_client.port).to eq(4000) - end - - it "configures the HTTP client with the read timeout set in the config file" do - Chef::Config[:rest_timeout] = 9001 - expect(new_request.http_client.read_timeout).to eq(9001) - end - - describe "for proxy" do - before do - stub_const("ENV", "http_proxy" => "http://proxy.example.com:3128", - "https_proxy" => "http://sproxy.example.com:3129", - "http_proxy_user" => nil, - "http_proxy_pass" => nil, - "https_proxy_user" => nil, - "https_proxy_pass" => nil, - "no_proxy" => nil - ) - end - - describe "with :no_proxy nil" do - it "configures the proxy address and port when using http scheme" do - http_client = new_request.http_client - expect(http_client.proxy?).to eq(true) - expect(http_client.proxy_address).to eq("proxy.example.com") - expect(http_client.proxy_port).to eq(3128) - expect(http_client.proxy_user).to be_nil - expect(http_client.proxy_pass).to be_nil - end - - context "when the url has an https scheme" do - let(:url) { URI.parse("https://chef.example.com:4000/?q=chef_is_awesome") } - - it "configures the proxy address and port when using https scheme" do - http_client = new_request.http_client - expect(http_client.proxy?).to eq(true) - expect(http_client.proxy_address).to eq("sproxy.example.com") - expect(http_client.proxy_port).to eq(3129) - expect(http_client.proxy_user).to be_nil - expect(http_client.proxy_pass).to be_nil - end - end - end - - describe "with :no_proxy set" do - before do - stub_const("ENV", "no_proxy" => "10.*,*.example.com") - end - - it "does not configure the proxy address and port when using http scheme" do - http_client = new_request.http_client - expect(http_client.proxy?).to eq(false) - expect(http_client.proxy_address).to be_nil - expect(http_client.proxy_port).to be_nil - expect(http_client.proxy_user).to be_nil - expect(http_client.proxy_pass).to be_nil - end - - context "when the url has an https scheme" do - let(:url) { URI.parse("https://chef.example.com:4000/?q=chef_is_awesome") } - - it "does not configure the proxy address and port when using https scheme" do - http_client = new_request.http_client - expect(http_client.proxy?).to eq(false) - expect(http_client.proxy_address).to be_nil - expect(http_client.proxy_port).to be_nil - expect(http_client.proxy_user).to be_nil - expect(http_client.proxy_pass).to be_nil - end - end - end - - describe "with :http_proxy_user and :http_proxy_pass set" do - before do - stub_const("ENV", "http_proxy" => "http://homie:theclown@proxy.example.com:3128") - end - - it "configures the proxy user and pass when using http scheme" do - http_client = new_request.http_client - expect(http_client.proxy?).to eq(true) - expect(http_client.proxy_user).to eq("homie") - expect(http_client.proxy_pass).to eq("theclown") - end - - context "when the url has an https scheme" do - let(:url) { URI.parse("https://chef.example.com:4000/?q=chef_is_awesome") } - - it "does not configure the proxy user and pass when using https scheme" do - http_client = new_request.http_client - expect(http_client.proxy?).to eq(false) - expect(http_client.proxy_user).to be_nil - expect(http_client.proxy_pass).to be_nil - end - end - end - - describe "with :https_proxy_user and :https_proxy_pass set" do - before do - stub_const("ENV", "http_proxy" => "http://proxy.example.com:3128", - "https_proxy" => "https://homie:theclown@sproxy.example.com:3129" - ) - end - - it "does not configure the proxy user and pass when using http scheme" do - http_client = new_request.http_client - expect(http_client.proxy?).to eq(true) - expect(http_client.proxy_user).to be_nil - expect(http_client.proxy_pass).to be_nil - end - - context "when the url has an https scheme" do - let(:url) { URI.parse("https://chef.example.com:4000/?q=chef_is_awesome") } - - it "configures the proxy user and pass when using https scheme" do - http_client = new_request.http_client - expect(http_client.proxy?).to eq(true) - expect(http_client.proxy_user).to eq("homie") - expect(http_client.proxy_pass).to eq("theclown") - end - end - end - end - end -end diff --git a/spec/unit/rest_spec.rb b/spec/unit/rest_spec.rb deleted file mode 100644 index ea3bd88023..0000000000 --- a/spec/unit/rest_spec.rb +++ /dev/null @@ -1,753 +0,0 @@ -# -# Author:: Adam Jacob (<adam@chef.io>) -# Author:: Christopher Brown (<cb@chef.io>) -# Author:: Daniel DeLeo (<dan@chef.io>) -# Copyright:: Copyright 2008-2016, Chef Software Inc. -# Copyright:: Copyright 2010-2016, Chef Software 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. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require "spec_helper" -require "uri" -require "net/https" -require "stringio" - -SIGNING_KEY_DOT_PEM = "-----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-----" - -describe Chef::REST do - let(:base_url) { "http://chef.example.com:4000" } - - let(:monkey_uri) { URI.parse("http://chef.example.com:4000/monkey") } - - let(:log_stringio) { StringIO.new } - - let(:request_id) { "1234" } - - let(:rest) do - allow(Chef::REST::CookieJar).to receive(:instance).and_return({}) - allow(Chef::RequestID.instance).to receive(:request_id).and_return(request_id) - rest = Chef::REST.new(base_url, nil, nil) - Chef::REST::CookieJar.instance.clear - rest - end - - let(:standard_read_headers) { { "Accept" => "application/json", "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-REMOTE-REQUEST-ID" => request_id, "X-Ops-Server-API-Version" => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION } } - let(:standard_write_headers) { { "Accept" => "application/json", "Content-Type" => "application/json", "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-REMOTE-REQUEST-ID" => request_id, "X-Ops-Server-API-Version" => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION } } - - before(:each) do - Chef::Log.init(log_stringio) - Chef::Config[:treat_deprecation_warnings_as_errors] = false - end - - it "should have content length validation middleware after compressor middleware" do - middlewares = rest.instance_variable_get(:@middlewares) - content_length = middlewares.find_index { |e| e.is_a? Chef::HTTP::ValidateContentLength } - decompressor = middlewares.find_index { |e| e.is_a? Chef::HTTP::Decompressor } - - expect(content_length).not_to be_nil - expect(decompressor).not_to be_nil - expect(decompressor < content_length).to be_truthy - end - - it "should allow the options hash to be frozen" do - options = {}.freeze - # should not raise any exception - Chef::REST.new(base_url, nil, nil, options) - end - - it "emits a deprecation warning" do - Chef::Config[:treat_deprecation_warnings_as_errors] = true - expect { Chef::REST.new(base_url) }.to raise_error Chef::Exceptions::DeprecatedFeatureError, - /Chef::REST is deprecated. Please use Chef::ServerAPI, or investigate Ridley or ChefAPI./ - end - - context "when created with a chef zero URL" do - - let(:url) { "chefzero://localhost:1" } - - it "does not load the signing key" do - expect { Chef::REST.new(url) }.to_not raise_error - end - end - - 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 - expect(rest.create_url("foo/bar/baz")).to eq(URI.parse(base_url + "/foo/bar/baz")) - end - - it "replaces the base URL when given an absolute URL" do - expect(rest.create_url("http://chef-rulez.example.com:9000")).to eq(URI.parse("http://chef-rulez.example.com:9000")) - end - - it "makes a :GET request with the composed url object" do - expect(rest).to receive(:send_http_request). - with(:GET, monkey_uri, standard_read_headers, false). - and_return([1, 2, 3]) - expect(rest).to receive(:apply_response_middleware).with(1, 2, 3).and_return([1, 2, 3]) - expect(rest).to receive("success_response?".to_sym).with(1).and_return(true) - rest.get_rest("monkey") - end - - it "makes a :GET reqest for a streaming download with the composed url" do - expect(rest).to receive(:streaming_request).with("monkey", {}) - rest.get_rest("monkey", true) - end - - it "makes a :DELETE request with the composed url object" do - expect(rest).to receive(:send_http_request). - with(:DELETE, monkey_uri, standard_read_headers, false). - and_return([1, 2, 3]) - expect(rest).to receive(:apply_response_middleware).with(1, 2, 3).and_return([1, 2, 3]) - expect(rest).to receive("success_response?".to_sym).with(1).and_return(true) - rest.delete_rest("monkey") - end - - it "makes a :POST request with the composed url object and data" do - expect(rest).to receive(:send_http_request). - with(:POST, monkey_uri, standard_write_headers, "\"data\""). - and_return([1, 2, 3]) - expect(rest).to receive(:apply_response_middleware).with(1, 2, 3).and_return([1, 2, 3]) - expect(rest).to receive("success_response?".to_sym).with(1).and_return(true) - rest.post_rest("monkey", "data") - end - - it "makes a :PUT request with the composed url object and data" do - expect(rest).to receive(:send_http_request). - with(:PUT, monkey_uri, standard_write_headers, "\"data\""). - and_return([1, 2, 3]) - expect(rest).to receive(:apply_response_middleware).with(1, 2, 3).and_return([1, 2, 3]) - expect(rest).to receive("success_response?".to_sym).with(1).and_return(true) - rest.put_rest("monkey", "data") - end - end - - describe "legacy API" do - let(:rest) do - Chef::REST.new(base_url) - end - - before(:each) do - Chef::Config[:node_name] = "webmonkey.example.com" - Chef::Config[:client_key] = CHEF_SPEC_DATA + "/ssl/private_key.pem" - end - - it "responds to raw_http_request as a public method" do - expect(rest.public_methods.map(&:to_s)).to include("raw_http_request") - end - - it "calls the authn middleware" do - data = "\"secure data\"" - - auth_headers = standard_write_headers.merge({ "auth_done" => "yep" }) - - expect(rest.authenticator).to receive(:handle_request). - with(:POST, monkey_uri, standard_write_headers, data). - and_return([:POST, monkey_uri, auth_headers, data]) - expect(rest).to receive(:send_http_request). - with(:POST, monkey_uri, auth_headers, data). - and_return([1, 2, 3]) - expect(rest).to receive("success_response?".to_sym).with(1).and_return(true) - rest.raw_http_request(:POST, monkey_uri, standard_write_headers, data) - end - - it "sets correct authn headers" do - data = "\"secure data\"" - method, uri, auth_headers, d = rest.authenticator.handle_request(:POST, monkey_uri, standard_write_headers, data) - - expect(rest).to receive(:send_http_request). - with(:POST, monkey_uri, auth_headers, data). - and_return([1, 2, 3]) - expect(rest).to receive("success_response?".to_sym).with(1).and_return(true) - rest.raw_http_request(:POST, monkey_uri, standard_write_headers, data) - end - end - - describe "when configured to authenticate to the Chef server" do - let(:base_url) { URI.parse("http://chef.example.com:4000") } - - let(:rest) do - Chef::REST.new(base_url) - end - - before do - Chef::Config[:node_name] = "webmonkey.example.com" - Chef::Config[:client_key] = CHEF_SPEC_DATA + "/ssl/private_key.pem" - end - - it "configures itself to use the node_name and client_key in the config by default" do - expect(rest.client_name).to eq("webmonkey.example.com") - expect(rest.signing_key_filename).to eq(CHEF_SPEC_DATA + "/ssl/private_key.pem") - end - - it "provides access to the raw key data" do - expect(rest.signing_key).to eq(SIGNING_KEY_DOT_PEM) - end - - it "does not error out when initialized without credentials" do - rest = Chef::REST.new(base_url, nil, nil) #should_not raise_error hides the bt from you, so screw it. - expect(rest.client_name).to be_nil - expect(rest.signing_key).to be_nil - end - - it "indicates that requests should not be signed when it has no credentials" do - rest = Chef::REST.new(base_url, nil, nil) - expect(rest.sign_requests?).to be_falsey - end - - it "raises PrivateKeyMissing when the key file doesn't exist" do - expect { Chef::REST.new(base_url, "client-name", "/dev/null/nothing_here") }.to raise_error(Chef::Exceptions::PrivateKeyMissing) - end - - it "raises InvalidPrivateKey when the key file doesnt' look like a key" do - invalid_key_file = CHEF_SPEC_DATA + "/bad-config.rb" - expect { Chef::REST.new(base_url, "client-name", invalid_key_file) }.to raise_error(Chef::Exceptions::InvalidPrivateKey) - end - - it "can take private key as a sting :raw_key in options during initializaton" do - expect(Chef::REST.new(base_url, "client-name", nil, :raw_key => SIGNING_KEY_DOT_PEM).signing_key).to eq(SIGNING_KEY_DOT_PEM) - end - - it "raises InvalidPrivateKey when the key passed as string :raw_key in options doesnt' look like a key" do - expect { Chef::REST.new(base_url, "client-name", nil, :raw_key => "bad key string") }.to raise_error(Chef::Exceptions::InvalidPrivateKey) - end - - end - - context "when making REST requests" do - let(:body) { "ninja" } - - let(:http_response) do - http_response = Net::HTTPSuccess.new("1.1", "200", "successful rest req") - allow(http_response).to receive(:read_body) - allow(http_response).to receive(:body).and_return(body) - http_response["Content-Length"] = body.bytesize.to_s - http_response - end - - let(:host_header) { "one" } - - let(:url) { URI.parse("http://one:80/?foo=bar") } - - let(:base_url) { "http://chef.example.com:4000" } - - let!(:http_client) do - http_client = Net::HTTP.new(url.host, url.port) - allow(http_client).to receive(:request).and_yield(http_response).and_return(http_response) - http_client - end - - let(:rest) do - allow(Net::HTTP).to receive(:new).and_return(http_client) - allow(Chef::REST::CookieJar).to receive(:instance).and_return({}) - allow(Chef::RequestID.instance).to receive(:request_id).and_return(request_id) - rest = Chef::REST.new(base_url, nil, nil) - Chef::REST::CookieJar.instance.clear - rest - end - - before(:each) do - Chef::Config[:ssl_client_cert] = nil - Chef::Config[:ssl_client_key] = nil - end - - describe "as JSON API requests" do - let(:request_mock) { {} } - - let(:base_headers) do #FIXME: huh? - { - "Accept" => "application/json", - "X-Chef-Version" => Chef::VERSION, - "Accept-Encoding" => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, - "Host" => host_header, - "X-REMOTE-REQUEST-ID" => request_id, - "X-Ops-Server-API-Version" => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION, - } - end - - before do - allow(Net::HTTP::Get).to receive(:new).and_return(request_mock) - end - - it "should always include the X-Chef-Version header" do - expect(Net::HTTP::Get).to receive(:new).with("/?foo=bar", base_headers).and_return(request_mock) - rest.request(:GET, url, {}) - end - - it "should always include the X-Remote-Request-Id header" do - expect(Net::HTTP::Get).to receive(:new).with("/?foo=bar", base_headers).and_return(request_mock) - rest.request(:GET, url, {}) - end - - it "sets the user agent to chef-client" do - # XXX: must reset to default b/c knife changes the UA - Chef::REST::RESTRequest.user_agent = Chef::REST::RESTRequest::DEFAULT_UA - rest.request(:GET, url, {}) - expect(request_mock["User-Agent"]).to match(/^Chef Client\/#{Chef::VERSION}/) - end - - # CHEF-3140 - context "when configured to disable compression" do - let(:rest) do - allow(Net::HTTP).to receive(:new).and_return(http_client) - Chef::REST.new(base_url, nil, nil, :disable_gzip => true) - end - - it "does not accept encoding gzip" do - expect(rest.send(:build_headers, :GET, url, {})).not_to have_key("Accept-Encoding") - end - - it "does not decompress a response encoded as gzip" do - http_response.add_field("content-encoding", "gzip") - request = Net::HTTP::Get.new(url.path) - expect(Net::HTTP::Get).to receive(:new).and_return(request) - # will raise a Zlib error if incorrect - expect(rest.request(:GET, url, {})).to eq("ninja") - end - end - - context "when configured with custom http headers" do - let(:custom_headers) do - { - "X-Custom-ChefSecret" => "sharpknives", - "X-Custom-RequestPriority" => "extremely low", - } - end - - before(:each) do - Chef::Config[:custom_http_headers] = custom_headers - end - - after(:each) do - Chef::Config[:custom_http_headers] = nil - end - - it "should set them on the http request" do - url_string = an_instance_of(String) - header_hash = hash_including(custom_headers) - expect(Net::HTTP::Get).to receive(:new).with(url_string, header_hash) - rest.request(:GET, url, {}) - end - end - - context "when setting cookies" do - let(:rest) do - allow(Net::HTTP).to receive(:new).and_return(http_client) - Chef::REST::CookieJar.instance["#{url.host}:#{url.port}"] = "cookie monster" - allow(Chef::RequestID.instance).to receive(:request_id).and_return(request_id) - rest = Chef::REST.new(base_url, nil, nil) - rest - end - - it "should set the cookie for this request if one exists for the given host:port" do - expect(Net::HTTP::Get).to receive(:new).with("/?foo=bar", base_headers.merge("Cookie" => "cookie monster")).and_return(request_mock) - rest.request(:GET, url, {}) - end - end - - it "should build a new HTTP GET request" do - expect(Net::HTTP::Get).to receive(:new).with("/?foo=bar", base_headers).and_return(request_mock) - rest.request(:GET, url, {}) - end - - it "should build a new HTTP POST request" do - request = Net::HTTP::Post.new(url.path) - expected_headers = base_headers.merge("Content-Type" => "application/json", "Content-Length" => "13") - - expect(Net::HTTP::Post).to receive(:new).with("/?foo=bar", expected_headers).and_return(request) - rest.request(:POST, url, {}, { :one => :two }) - expect(request.body).to eq('{"one":"two"}') - end - - it "should build a new HTTP PUT request" do - request = Net::HTTP::Put.new(url.path) - expected_headers = base_headers.merge("Content-Type" => "application/json", "Content-Length" => "13") - expect(Net::HTTP::Put).to receive(:new).with("/?foo=bar", expected_headers).and_return(request) - rest.request(:PUT, url, {}, { :one => :two }) - expect(request.body).to eq('{"one":"two"}') - end - - it "should build a new HTTP DELETE request" do - expect(Net::HTTP::Delete).to receive(:new).with("/?foo=bar", base_headers).and_return(request_mock) - rest.request(:DELETE, url) - end - - it "should raise an error if the method is not GET/PUT/POST/DELETE" do - expect { rest.request(:MONKEY, url) }.to raise_error(ArgumentError) - end - - it "returns nil when the response is successful but content-type is not JSON" do - expect(rest.request(:GET, url)).to eq("ninja") - end - - it "should fail if the response is truncated" do - http_response["Content-Length"] = (body.bytesize + 99).to_s - expect { rest.request(:GET, url) }.to raise_error(Chef::Exceptions::ContentLengthMismatch) - end - - context "when JSON is returned" do - let(:body) { '{"ohai2u":"json_api"}' } - it "should inflate the body as to an object" do - http_response.add_field("content-type", "application/json") - expect(rest.request(:GET, url, {})).to eq({ "ohai2u" => "json_api" }) - end - - it "should fail if the response is truncated" do - http_response.add_field("content-type", "application/json") - http_response["Content-Length"] = (body.bytesize + 99).to_s - expect { rest.request(:GET, url, {}) }.to raise_error(Chef::Exceptions::ContentLengthMismatch) - end - end - - %w{ HTTPFound HTTPMovedPermanently HTTPSeeOther HTTPUseProxy HTTPTemporaryRedirect HTTPMultipleChoice }.each do |resp_name| - describe "when encountering a #{resp_name} redirect" do - let(:http_response) do - resp_cls = Net.const_get(resp_name) - resp_code = Net::HTTPResponse::CODE_TO_OBJ.keys.detect { |k| Net::HTTPResponse::CODE_TO_OBJ[k] == resp_cls } - http_response = Net::HTTPFound.new("1.1", resp_code, "bob is somewhere else again") - http_response.add_field("location", url.path) - allow(http_response).to receive(:read_body) - http_response - end - it "should call request again" do - - expect { rest.request(:GET, url) }.to raise_error(Chef::Exceptions::RedirectLimitExceeded) - - [:PUT, :POST, :DELETE].each do |method| - expect { rest.request(method, url) }.to raise_error(Chef::Exceptions::InvalidRedirect) - end - end - end - end - - context "when the response is 304 NotModified" do - let (:http_response) do - http_response = Net::HTTPNotModified.new("1.1", "304", "it's the same as when you asked 5 minutes ago") - allow(http_response).to receive(:read_body) - http_response - end - - it "should return `false`" do - expect(rest.request(:GET, url)).to be_falsey - end - end - - describe "when the request fails" do - before do - @original_log_level = Chef::Log.level - Chef::Log.level = :info - end - - after do - Chef::Log.level = @original_log_level - end - - context "on an unsuccessful response with a JSON error" do - let(:http_response) do - http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth") - http_response.add_field("content-type", "application/json") - allow(http_response).to receive(:body).and_return('{ "error":[ "Ears get sore!", "Not even four" ] }') - allow(http_response).to receive(:read_body) - http_response - end - - it "should show the JSON error message" do - allow(rest).to receive(:sleep) - - expect { rest.request(:GET, url) }.to raise_error(Net::HTTPFatalError) - expect(log_stringio.string).to match(Regexp.escape("INFO: HTTP Request Returned 500 drooling from inside of mouth: Ears get sore!, Not even four")) - end - end - - context "on an unsuccessful response with a JSON error that is compressed" do - let(:http_response) do - http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth") - http_response.add_field("content-type", "application/json") - http_response.add_field("content-encoding", "deflate") - unzipped_body = '{ "error":[ "Ears get sore!", "Not even four" ] }' - gzipped_body = Zlib::Deflate.deflate(unzipped_body) - gzipped_body.force_encoding(Encoding::BINARY) if "strings".respond_to?(:force_encoding) - - allow(http_response).to receive(:body).and_return gzipped_body - allow(http_response).to receive(:read_body) - http_response - end - - before do - allow(rest).to receive(:sleep) - allow(rest).to receive(:http_retry_count).and_return(0) - end - - it "decompresses the JSON error message" do - expect { rest.request(:GET, url) }.to raise_error(Net::HTTPFatalError) - expect(log_stringio.string).to match(Regexp.escape("INFO: HTTP Request Returned 500 drooling from inside of mouth: Ears get sore!, Not even four")) - end - - it "fails when the compressed body is truncated" do - http_response["Content-Length"] = (body.bytesize + 99).to_s - expect { rest.request(:GET, url) }.to raise_error(Chef::Exceptions::ContentLengthMismatch) - end - end - - context "on a generic unsuccessful request" do - let(:http_response) do - http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth") - allow(http_response).to receive(:body) - allow(http_response).to receive(:read_body) - http_response - end - - it "retries then throws an exception" do - allow(rest).to receive(:sleep) - expect { rest.request(:GET, url) }.to raise_error(Net::HTTPFatalError) - count = Chef::Config[:http_retry_count] - expect(log_stringio.string).to match(Regexp.escape("ERROR: Server returned error 500 for #{url}, retrying #{count}/#{count}")) - end - end - end - end # as JSON API requests - - context "when streaming downloads to a tempfile" do - let!(:tempfile) { Tempfile.open("chef-rspec-rest_spec-line-@{__LINE__}--") } - - let(:request_mock) { {} } - - let(:http_response) do - http_response = Net::HTTPSuccess.new("1.1", "200", "it-works") - - allow(http_response).to receive(:read_body) - expect(http_response).not_to receive(:body) - http_response["Content-Length"] = "0" # call set_content_length (in test), if otherwise - http_response - end - - def set_content_length - content_length = 0 - http_response.read_body do |chunk| - content_length += chunk.bytesize - end - http_response["Content-Length"] = content_length.to_s - end - - before do - allow(Tempfile).to receive(:new).with("chef-rest").and_return(tempfile) - allow(Net::HTTP::Get).to receive(:new).and_return(request_mock) - end - - after do - tempfile.close! - end - - it " build a new HTTP GET request without the application/json accept header" do - expected_headers = { "Accept" => "*/*", - "X-Chef-Version" => Chef::VERSION, - "Accept-Encoding" => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, - "Host" => host_header, - "X-REMOTE-REQUEST-ID" => request_id, - "X-Ops-Server-API-Version" => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION, - } - expect(Net::HTTP::Get).to receive(:new).with("/?foo=bar", expected_headers).and_return(request_mock) - rest.streaming_request(url, {}) - end - - it "build a new HTTP GET request with the X-Remote-Request-Id header" do - expected_headers = { "Accept" => "*/*", - "X-Chef-Version" => Chef::VERSION, - "Accept-Encoding" => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, - "Host" => host_header, - "X-REMOTE-REQUEST-ID" => request_id, - "X-Ops-Server-API-Version" => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION, - } - expect(Net::HTTP::Get).to receive(:new).with("/?foo=bar", expected_headers).and_return(request_mock) - rest.streaming_request(url, {}) - end - - it "returns a tempfile containing the streamed response body" do - expect(rest.streaming_request(url, {})).to equal(tempfile) - end - - it "writes the response body to a tempfile" do - allow(http_response).to receive(:read_body).and_yield("real").and_yield("ultimate").and_yield("power") - set_content_length - rest.streaming_request(url, {}) - expect(IO.read(tempfile.path).chomp).to eq("realultimatepower") - end - - it "closes the tempfile" do - rest.streaming_request(url, {}) - expect(tempfile).to be_closed - end - - it "yields the tempfile containing the streamed response body and then unlinks it when given a block" do - allow(http_response).to receive(:read_body).and_yield("real").and_yield("ultimate").and_yield("power") - set_content_length - tempfile_path = nil - rest.streaming_request(url, {}) do |tempfile| - tempfile_path = tempfile.path - expect(File.exist?(tempfile.path)).to be_truthy - expect(IO.read(tempfile.path).chomp).to eq("realultimatepower") - end - expect(File.exist?(tempfile_path)).to be_falsey - end - - it "does not raise a divide by zero exception if the content's actual size is 0" do - http_response["Content-Length"] = "5" - allow(http_response).to receive(:read_body).and_yield("") - expect { rest.streaming_request(url, {}) }.to raise_error(Chef::Exceptions::ContentLengthMismatch) - end - - it "does not raise a divide by zero exception when the Content-Length is 0" do - http_response["Content-Length"] = "0" - allow(http_response).to receive(:read_body).and_yield("ninja") - expect { rest.streaming_request(url, {}) }.to raise_error(Chef::Exceptions::ContentLengthMismatch) - end - - it "it raises an exception when the download is truncated" do - http_response["Content-Length"] = (body.bytesize + 99).to_s - allow(http_response).to receive(:read_body).and_yield("ninja") - expect { rest.streaming_request(url, {}) }.to raise_error(Chef::Exceptions::ContentLengthMismatch) - end - - it "fetches a file and yields the tempfile it is streamed to" do - allow(http_response).to receive(:read_body).and_yield("real").and_yield("ultimate").and_yield("power") - set_content_length - tempfile_path = nil - rest.fetch("cookbooks/a_cookbook") do |tempfile| - tempfile_path = tempfile.path - expect(IO.read(tempfile.path).chomp).to eq("realultimatepower") - end - expect(File.exist?(tempfile_path)).to be_falsey - end - - it "closes and unlinks the tempfile if there is an error while streaming the content to the tempfile" do - path = tempfile.path - expect(path).not_to be_nil - allow(tempfile).to receive(:write).and_raise(IOError) - rest.fetch("cookbooks/a_cookbook") { |tmpfile| "shouldn't get here" } - expect(File.exists?(path)).to be_falsey - end - - it "closes and unlinks the tempfile when the response is a redirect" do - tempfile = double("A tempfile", :path => "/tmp/ragefist", :close => true, :binmode => true) - expect(tempfile).to receive(:close!).at_least(1).times - allow(Tempfile).to receive(:new).with("chef-rest").and_return(tempfile) - - redirect = Net::HTTPFound.new("1.1", "302", "bob is taking care of that one for me today") - redirect.add_field("location", url.path) - allow(redirect).to receive(:read_body) - - expect(http_client).to receive(:request).and_yield(redirect).and_return(redirect) - expect(http_client).to receive(:request).and_yield(http_response).and_return(http_response) - rest.fetch("cookbooks/a_cookbook") { |tmpfile| "shouldn't get here" } - end - - it "passes the original block to the redirected request" do - http_redirect = Net::HTTPFound.new("1.1", "302", "bob is taking care of that one for me today") - http_redirect.add_field("location", "/that-thing-is-here-now") - allow(http_redirect).to receive(:read_body) - - block_called = false - allow(http_client).to receive(:request).and_yield(http_response).and_return(http_redirect, http_response) - rest.fetch("cookbooks/a_cookbook") do |tmpfile| - block_called = true - end - expect(block_called).to be_truthy - end - end - end # when making REST requests - - context "when following redirects" do - let(:rest) do - Chef::REST.new(base_url) - end - - before do - Chef::Config[:node_name] = "webmonkey.example.com" - Chef::Config[:client_key] = CHEF_SPEC_DATA + "/ssl/private_key.pem" - end - - it "raises a RedirectLimitExceeded when redirected more than 10 times" do - redirected = lambda { rest.follow_redirect { redirected.call } } - expect { redirected.call }.to raise_error(Chef::Exceptions::RedirectLimitExceeded) - end - - 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 - expect { redirected.call }.not_to raise_error - total_redirects = 0 - expect { redirected.call }.not_to raise_error - end - - it "does not sign the redirected request when sign_on_redirect is false" do - rest.sign_on_redirect = false - rest.follow_redirect { expect(rest.sign_requests?).to be_falsey } - end - - it "resets sign_requests to the original value after following an unsigned redirect" do - rest.sign_on_redirect = false - expect(rest.sign_requests?).to be_truthy - - rest.follow_redirect { expect(rest.sign_requests?).to be_falsey } - expect(rest.sign_requests?).to be_truthy - end - - 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 - expect { redirected.call }.not_to raise_error - - total_redirects = 0 - rest.redirect_limit = 3 - expect { redirected.call }.to raise_error(Chef::Exceptions::RedirectLimitExceeded) - end - - end -end |