# # Author:: Daniel DeLeo (<dan@opscode.com>) # Copyright:: Copyright (c) 2013 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. # See the License for the specific language governing permissions and # limitations under the License. # require 'spec_helper' require 'uri' CACHE_FILE_TRUNCATED_FRIENDLY_FILE_NAME_LENGTH = 64 CACHE_FILE_MD5_HEX_LENGTH = 32 CACHE_FILE_JSON_FILE_EXTENSION_LENGTH = 5 CACHE_FILE_PATH_LIMIT = CACHE_FILE_TRUNCATED_FRIENDLY_FILE_NAME_LENGTH + 1 + CACHE_FILE_MD5_HEX_LENGTH + CACHE_FILE_JSON_FILE_EXTENSION_LENGTH # {friendly}-{md5hex}.json == 102 describe Chef::Provider::RemoteFile::CacheControlData do let(:uri) { URI.parse("http://www.google.com/robots.txt") } subject(:cache_control_data) do Chef::Provider::RemoteFile::CacheControlData.load_and_validate(uri, current_file_checksum) end let(:cache_path) { "remote_file/http___www_google_com_robots_txt-9839677abeeadf0691026e0cabca2339.json" } # the checksum of the file we have on disk already let(:current_file_checksum) { "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } context "when loading data for an unknown URI" do before do Chef::FileCache.should_receive(:load).with(cache_path).and_raise(Chef::Exceptions::FileNotFound, "nope") end context "and there is no current copy of the file" do let(:current_file_checksum) { nil } it "returns empty cache control data" do cache_control_data.etag.should be_nil cache_control_data.mtime.should be_nil end end it "returns empty cache control data" do cache_control_data.etag.should be_nil cache_control_data.mtime.should be_nil end context "and the URI contains a password" do let(:uri) { URI.parse("http://bob:password@example.org/") } let(:cache_path) { "remote_file/http___bob_XXXX_example_org_-f121caacb74c05a35bcefdf578ed5fc9.json" } it "loads the cache data from a path based on a sanitized URI" do Chef::Provider::RemoteFile::CacheControlData.load_and_validate(uri, current_file_checksum) end end end describe "when loading data for a known URI" do # the checksum of the file last we fetched it. let(:last_fetched_checksum) { "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } let(:etag) { "\"a-strong-identifier\"" } let(:mtime) { "Tue, 21 May 2013 19:19:23 GMT" } let(:cache_json_data) do cache = {} cache["etag"] = etag cache["mtime"] = mtime cache["checksum"] = last_fetched_checksum cache.to_json end before do Chef::FileCache.should_receive(:load).with(cache_path).and_return(cache_json_data) end context "and there is no on-disk copy of the file" do let(:current_file_checksum) { nil } it "returns empty cache control data" do cache_control_data.etag.should be_nil cache_control_data.mtime.should be_nil end end context "and the cached checksum does not match the on-disk copy" do let(:current_file_checksum) { "e2a8938cc31754f6c067b35aab1d0d4864272e9bf8504536ef3e79ebf8432305" } it "returns empty cache control data" do cache_control_data.etag.should be_nil cache_control_data.mtime.should be_nil end end context "and the cached checksum matches the on-disk copy" do it "populates the cache control data" do cache_control_data.etag.should == etag cache_control_data.mtime.should == mtime end end context "and the cached checksum data is corrupted" do let(:cache_json_data) { '{"foo",,"bar" []}' } it "returns empty cache control data" do cache_control_data.etag.should be_nil cache_control_data.mtime.should be_nil end end end describe "when saving to disk" do let(:etag) { "\"a-strong-identifier\"" } let(:mtime) { "Tue, 21 May 2013 19:19:23 GMT" } let(:fetched_file_checksum) { "e2a8938cc31754f6c067b35aab1d0d4864272e9bf8504536ef3e79ebf8432305" } let(:expected_serialization_data) do data = {} data["etag"] = etag data["mtime"] = mtime data["checksum"] = fetched_file_checksum data end before do cache_control_data.etag = etag cache_control_data.mtime = mtime cache_control_data.checksum = fetched_file_checksum end it "serializes its attributes to JSON" do # we have to test this separately because ruby 1.8 hash order is unstable # so we can't count on the order of the keys in the json format. json_data = cache_control_data.json_data Chef::JSONCompat.from_json(json_data).should == expected_serialization_data end it "writes data to the cache" do json_data = cache_control_data.json_data Chef::FileCache.should_receive(:store).with(cache_path, json_data) cache_control_data.save end context "and the URI contains a password" do let(:uri) { URI.parse("http://bob:password@example.org/") } let(:cache_path) { "remote_file/http___bob_XXXX_example_org_-f121caacb74c05a35bcefdf578ed5fc9.json" } it "writes the data to the cache with a sanitized path name" do json_data = cache_control_data.json_data Chef::FileCache.should_receive(:store).with(cache_path, json_data) cache_control_data.save end end # Cover the very long remote file path case -- see CHEF-4422 where # local cache file names generated from the long uri exceeded # local file system path limits resulting in exceptions from # file system API's on both Windows and Unix systems. context "and the URI results in a file cache path that exceeds #{CACHE_FILE_PATH_LIMIT} characters in length" do let(:long_remote_path) { "http://www.bing.com/" + ('0' * (CACHE_FILE_TRUNCATED_FRIENDLY_FILE_NAME_LENGTH * 2 )) } let(:uri) { URI.parse(long_remote_path) } let(:truncated_remote_uri) { URI.parse(long_remote_path[0...CACHE_FILE_TRUNCATED_FRIENDLY_FILE_NAME_LENGTH]) } let(:truncated_file_cache_path) do cache_control_data_truncated = Chef::Provider::RemoteFile::CacheControlData.load_and_validate(truncated_remote_uri, current_file_checksum) cache_control_data_truncated.send('sanitized_cache_file_basename')[0...CACHE_FILE_TRUNCATED_FRIENDLY_FILE_NAME_LENGTH] end it "truncates the file cache path to 102 characters" do normalized_cache_path = cache_control_data.send('sanitized_cache_file_basename') Chef::FileCache.should_receive(:store).with("remote_file/" + normalized_cache_path, cache_control_data.json_data) cache_control_data.save normalized_cache_path.length.should == CACHE_FILE_PATH_LIMIT end it "uses a file cache path that starts with the first #{CACHE_FILE_TRUNCATED_FRIENDLY_FILE_NAME_LENGTH} characters of the URI" do normalized_cache_path = cache_control_data.send('sanitized_cache_file_basename') truncated_file_cache_path.length.should == CACHE_FILE_TRUNCATED_FRIENDLY_FILE_NAME_LENGTH normalized_cache_path.start_with?(truncated_file_cache_path).should == true end end end end