diff options
author | danielsdeleo <dan@getchef.com> | 2015-04-23 11:44:24 -0700 |
---|---|---|
committer | danielsdeleo <dan@getchef.com> | 2015-05-20 15:13:56 -0700 |
commit | e6062caaefe82ff351690af15e882f02efc0f91b (patch) | |
tree | 850f5ed39e9bd7e33303bf907c4ea7389a6a33d7 /chef-config/spec | |
parent | b6f9e5feff3b97576534d70dc2873c4fd62d28e4 (diff) | |
download | chef-e6062caaefe82ff351690af15e882f02efc0f91b.tar.gz |
Move Chef::Config into a subproject
Diffstat (limited to 'chef-config/spec')
-rw-r--r-- | chef-config/spec/spec_helper.rb | 75 | ||||
-rw-r--r-- | chef-config/spec/unit/config_spec.rb | 581 | ||||
-rw-r--r-- | chef-config/spec/unit/path_helper_spec.rb | 291 |
3 files changed, 947 insertions, 0 deletions
diff --git a/chef-config/spec/spec_helper.rb b/chef-config/spec/spec_helper.rb new file mode 100644 index 0000000000..df9461cde9 --- /dev/null +++ b/chef-config/spec/spec_helper.rb @@ -0,0 +1,75 @@ +require 'chef-config/windows' + +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # These two settings work together to allow you to limit a spec run + # to individual examples or groups you care about by tagging them with + # `:focus` metadata. When nothing is tagged with `:focus`, all examples + # get run. + config.filter_run :focus + config.run_all_when_everything_filtered = true + + config.filter_run_excluding :windows_only => true unless ChefConfig.windows? + config.filter_run_excluding :unix_only => true if ChefConfig.windows? + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax + # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = 'doc' + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + # config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +end diff --git a/chef-config/spec/unit/config_spec.rb b/chef-config/spec/unit/config_spec.rb new file mode 100644 index 0000000000..395fa2618e --- /dev/null +++ b/chef-config/spec/unit/config_spec.rb @@ -0,0 +1,581 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Kyle Goodwin (<kgoodwin@primerevenue.com>) +# Copyright:: Copyright (c) 2008 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 'chef-config/config' + +RSpec.describe ChefConfig::Config do + before(:each) do + ChefConfig::Config.reset + + # By default, treat deprecation warnings as errors in tests. + ChefConfig::Config.treat_deprecation_warnings_as_errors(true) + + # Set environment variable so the setting persists in child processes + ENV['CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS'] = "1" + end + + describe "config attribute writer: chef_server_url" do + before do + ChefConfig::Config.chef_server_url = "https://junglist.gen.nz" + end + + it "sets the server url" do + expect(ChefConfig::Config.chef_server_url).to eq("https://junglist.gen.nz") + end + + context "when the url has a leading space" do + before do + ChefConfig::Config.chef_server_url = " https://junglist.gen.nz" + end + + it "strips the space from the url when setting" do + expect(ChefConfig::Config.chef_server_url).to eq("https://junglist.gen.nz") + end + + end + + context "when the url is a frozen string" do + before do + ChefConfig::Config.chef_server_url = " https://junglist.gen.nz".freeze + end + + it "strips the space from the url when setting without raising an error" do + expect(ChefConfig::Config.chef_server_url).to eq("https://junglist.gen.nz") + end + end + end + + describe "when configuring formatters" do + # if TTY and not(force-logger) + # formatter = configured formatter or default formatter + # formatter goes to STDOUT/ERR + # if log file is writeable + # log level is configured level or info + # log location is file + # else + # log level is warn + # log location is STDERR + # end + # elsif not(TTY) and force formatter + # formatter = configured formatter or default formatter + # if log_location specified + # formatter goes to log_location + # else + # formatter goes to STDOUT/ERR + # end + # else + # formatter = "null" + # log_location = configured-value or defualt + # log_level = info or defualt + # end + # + it "has an empty list of formatters by default" do + expect(ChefConfig::Config.formatters).to eq([]) + end + + it "configures a formatter with a short name" do + ChefConfig::Config.add_formatter(:doc) + expect(ChefConfig::Config.formatters).to eq([[:doc, nil]]) + end + + it "configures a formatter with a file output" do + ChefConfig::Config.add_formatter(:doc, "/var/log/formatter.log") + expect(ChefConfig::Config.formatters).to eq([[:doc, "/var/log/formatter.log"]]) + end + + end + + [ false, true ].each do |is_windows| + + context "On #{is_windows ? 'Windows' : 'Unix'}" do + def to_platform(*args) + ChefConfig::Config.platform_specific_path(*args) + end + + before :each do + allow(ChefConfig).to receive(:windows?).and_return(is_windows) + end + + describe "class method: platform_specific_path" do + if is_windows + it "should return a windows path on windows systems" do + path = "/etc/chef/cookbooks" + allow(ChefConfig::Config).to receive(:env).and_return({ 'SYSTEMDRIVE' => 'C:' }) + # match on a regex that looks for the base path with an optional + # system drive at the beginning (c:) + # system drive is not hardcoded b/c it can change and b/c it is not present on linux systems + expect(ChefConfig::Config.platform_specific_path(path)).to eq("C:\\chef\\cookbooks") + end + else + it "should return given path on non-windows systems" do + path = "/etc/chef/cookbooks" + expect(ChefConfig::Config.platform_specific_path(path)).to eq("/etc/chef/cookbooks") + end + end + end + + describe "default values" do + let :primary_cache_path do + if is_windows + "#{ChefConfig::Config.env['SYSTEMDRIVE']}\\chef" + else + "/var/chef" + end + end + + let :secondary_cache_path do + if is_windows + "#{ChefConfig::Config[:user_home]}\\.chef" + else + "#{ChefConfig::Config[:user_home]}/.chef" + end + end + + before do + if is_windows + allow(ChefConfig::Config).to receive(:env).and_return({ 'SYSTEMDRIVE' => 'C:' }) + ChefConfig::Config[:user_home] = 'C:\Users\charlie' + else + ChefConfig::Config[:user_home] = '/Users/charlie' + end + + allow(ChefConfig::Config).to receive(:path_accessible?).and_return(false) + end + + describe "ChefConfig::Config[:chef_server_root]" do + context "when chef_server_url isn't set manually" do + it "returns the default of 'https://localhost:443'" do + expect(ChefConfig::Config[:chef_server_root]).to eq("https://localhost:443") + end + end + + context "when chef_server_url matches '../organizations/*' without a trailing slash" do + before do + ChefConfig::Config[:chef_server_url] = "https://example.com/organizations/myorg" + end + it "returns the full URL without /organizations/*" do + expect(ChefConfig::Config[:chef_server_root]).to eq("https://example.com") + end + end + + context "when chef_server_url matches '../organizations/*' with a trailing slash" do + before do + ChefConfig::Config[:chef_server_url] = "https://example.com/organizations/myorg/" + end + it "returns the full URL without /organizations/*" do + expect(ChefConfig::Config[:chef_server_root]).to eq("https://example.com") + end + end + + context "when chef_server_url matches '..organizations..' but not '../organizations/*'" do + before do + ChefConfig::Config[:chef_server_url] = "https://organizations.com/organizations" + end + it "returns the full URL without any modifications" do + expect(ChefConfig::Config[:chef_server_root]).to eq(ChefConfig::Config[:chef_server_url]) + end + end + + context "when chef_server_url is a standard URL without the string organization(s)" do + before do + ChefConfig::Config[:chef_server_url] = "https://example.com/some_other_string" + end + it "returns the full URL without any modifications" do + expect(ChefConfig::Config[:chef_server_root]).to eq(ChefConfig::Config[:chef_server_url]) + end + end + end + + describe "ChefConfig::Config[:cache_path]" do + context "when /var/chef exists and is accessible" do + it "defaults to /var/chef" do + allow(ChefConfig::Config).to receive(:path_accessible?).with(to_platform("/var/chef")).and_return(true) + expect(ChefConfig::Config[:cache_path]).to eq(primary_cache_path) + end + end + + context "when /var/chef does not exist and /var is accessible" do + it "defaults to /var/chef" do + allow(File).to receive(:exists?).with(to_platform("/var/chef")).and_return(false) + allow(ChefConfig::Config).to receive(:path_accessible?).with(to_platform("/var")).and_return(true) + expect(ChefConfig::Config[:cache_path]).to eq(primary_cache_path) + end + end + + context "when /var/chef does not exist and /var is not accessible" do + it "defaults to $HOME/.chef" do + allow(File).to receive(:exists?).with(to_platform("/var/chef")).and_return(false) + allow(ChefConfig::Config).to receive(:path_accessible?).with(to_platform("/var")).and_return(false) + expect(ChefConfig::Config[:cache_path]).to eq(secondary_cache_path) + end + end + + context "when /var/chef exists and is not accessible" do + it "defaults to $HOME/.chef" do + allow(File).to receive(:exists?).with(to_platform("/var/chef")).and_return(true) + allow(File).to receive(:readable?).with(to_platform("/var/chef")).and_return(true) + allow(File).to receive(:writable?).with(to_platform("/var/chef")).and_return(false) + + expect(ChefConfig::Config[:cache_path]).to eq(secondary_cache_path) + end + end + + context "when chef is running in local mode" do + before do + ChefConfig::Config.local_mode = true + end + + context "and config_dir is /a/b/c" do + before do + ChefConfig::Config.config_dir to_platform('/a/b/c') + end + + it "cache_path is /a/b/c/local-mode-cache" do + expect(ChefConfig::Config.cache_path).to eq(to_platform('/a/b/c/local-mode-cache')) + end + end + + context "and config_dir is /a/b/c/" do + before do + ChefConfig::Config.config_dir to_platform('/a/b/c/') + end + + it "cache_path is /a/b/c/local-mode-cache" do + expect(ChefConfig::Config.cache_path).to eq(to_platform('/a/b/c/local-mode-cache')) + end + end + end + end + + it "ChefConfig::Config[:file_backup_path] defaults to /var/chef/backup" do + allow(ChefConfig::Config).to receive(:cache_path).and_return(primary_cache_path) + backup_path = is_windows ? "#{primary_cache_path}\\backup" : "#{primary_cache_path}/backup" + expect(ChefConfig::Config[:file_backup_path]).to eq(backup_path) + end + + it "ChefConfig::Config[:ssl_verify_mode] defaults to :verify_peer" do + expect(ChefConfig::Config[:ssl_verify_mode]).to eq(:verify_peer) + end + + it "ChefConfig::Config[:ssl_ca_path] defaults to nil" do + expect(ChefConfig::Config[:ssl_ca_path]).to be_nil + end + + # On Windows, we'll detect an omnibus build and set this to the + # cacert.pem included in the package, but it's nil if you're on Windows + # w/o omnibus (e.g., doing development on Windows, custom build, etc.) + if !is_windows + it "ChefConfig::Config[:ssl_ca_file] defaults to nil" do + expect(ChefConfig::Config[:ssl_ca_file]).to be_nil + end + end + + it "ChefConfig::Config[:data_bag_path] defaults to /var/chef/data_bags" do + allow(ChefConfig::Config).to receive(:cache_path).and_return(primary_cache_path) + data_bag_path = is_windows ? "#{primary_cache_path}\\data_bags" : "#{primary_cache_path}/data_bags" + expect(ChefConfig::Config[:data_bag_path]).to eq(data_bag_path) + end + + it "ChefConfig::Config[:environment_path] defaults to /var/chef/environments" do + allow(ChefConfig::Config).to receive(:cache_path).and_return(primary_cache_path) + environment_path = is_windows ? "#{primary_cache_path}\\environments" : "#{primary_cache_path}/environments" + expect(ChefConfig::Config[:environment_path]).to eq(environment_path) + end + + describe "setting the config dir" do + + context "when the config file is /etc/chef/client.rb" do + + before do + ChefConfig::Config.config_file = to_platform("/etc/chef/client.rb") + end + + it "config_dir is /etc/chef" do + expect(ChefConfig::Config.config_dir).to eq(to_platform("/etc/chef")) + end + + context "and chef is running in local mode" do + before do + ChefConfig::Config.local_mode = true + end + + it "config_dir is /etc/chef" do + expect(ChefConfig::Config.config_dir).to eq(to_platform("/etc/chef")) + end + end + + context "when config_dir is set to /other/config/dir/" do + before do + ChefConfig::Config.config_dir = to_platform("/other/config/dir/") + end + + it "yields the explicit value" do + expect(ChefConfig::Config.config_dir).to eq(to_platform("/other/config/dir/")) + end + end + + end + + context "when the user's home dir is /home/charlie/" do + before do + ChefConfig::Config.user_home = to_platform("/home/charlie") + end + + it "config_dir is /home/charlie/.chef/" do + expect(ChefConfig::Config.config_dir).to eq(ChefConfig::PathHelper.join(to_platform("/home/charlie/.chef"), '')) + end + + context "and chef is running in local mode" do + before do + ChefConfig::Config.local_mode = true + end + + it "config_dir is /home/charlie/.chef/" do + expect(ChefConfig::Config.config_dir).to eq(ChefConfig::PathHelper.join(to_platform("/home/charlie/.chef"), '')) + end + end + end + + end + + if is_windows + describe "finding the windows embedded dir" do + let(:default_config_location) { "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-11.6.0/lib/chef/config.rb" } + let(:alternate_install_location) { "c:/my/alternate/install/place/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-11.6.0/lib/chef/config.rb" } + let(:non_omnibus_location) { "c:/my/dev/stuff/lib/ruby/gems/1.9.1/gems/chef-11.6.0/lib/chef/config.rb" } + + let(:default_ca_file) { "c:/opscode/chef/embedded/ssl/certs/cacert.pem" } + + it "finds the embedded dir in the default location" do + allow(ChefConfig::Config).to receive(:_this_file).and_return(default_config_location) + expect(ChefConfig::Config.embedded_dir).to eq("c:/opscode/chef/embedded") + end + + it "finds the embedded dir in a custom install location" do + allow(ChefConfig::Config).to receive(:_this_file).and_return(alternate_install_location) + expect(ChefConfig::Config.embedded_dir).to eq("c:/my/alternate/install/place/chef/embedded") + end + + it "doesn't error when not in an omnibus install" do + allow(ChefConfig::Config).to receive(:_this_file).and_return(non_omnibus_location) + expect(ChefConfig::Config.embedded_dir).to be_nil + end + + it "sets the ssl_ca_cert path if the cert file is available" do + allow(ChefConfig::Config).to receive(:_this_file).and_return(default_config_location) + allow(File).to receive(:exist?).with(default_ca_file).and_return(true) + expect(ChefConfig::Config.ssl_ca_file).to eq(default_ca_file) + end + end + end + end + + describe "ChefConfig::Config[:user_home]" do + it "should set when HOME is provided" do + expected = to_platform("/home/kitten") + allow(ChefConfig::PathHelper).to receive(:home).and_return(expected) + expect(ChefConfig::Config[:user_home]).to eq(expected) + end + + it "falls back to the current working directory when HOME and USERPROFILE is not set" do + allow(ChefConfig::PathHelper).to receive(:home).and_return(nil) + expect(ChefConfig::Config[:user_home]).to eq(Dir.pwd) + end + end + + describe "ChefConfig::Config[:encrypted_data_bag_secret]" do + let(:db_secret_default_path){ to_platform("/etc/chef/encrypted_data_bag_secret") } + + before do + allow(File).to receive(:exist?).with(db_secret_default_path).and_return(secret_exists) + end + + context "/etc/chef/encrypted_data_bag_secret exists" do + let(:secret_exists) { true } + it "sets the value to /etc/chef/encrypted_data_bag_secret" do + expect(ChefConfig::Config[:encrypted_data_bag_secret]).to eq db_secret_default_path + end + end + + context "/etc/chef/encrypted_data_bag_secret does not exist" do + let(:secret_exists) { false } + it "sets the value to nil" do + expect(ChefConfig::Config[:encrypted_data_bag_secret]).to be_nil + end + end + end + + describe "ChefConfig::Config[:event_handlers]" do + it "sets a event_handlers to an empty array by default" do + expect(ChefConfig::Config[:event_handlers]).to eq([]) + end + it "should be able to add custom handlers" do + o = Object.new + ChefConfig::Config[:event_handlers] << o + expect(ChefConfig::Config[:event_handlers]).to be_include(o) + end + end + + describe "ChefConfig::Config[:user_valid_regex]" do + context "on a platform that is not Windows" do + it "allows one letter usernames" do + any_match = ChefConfig::Config[:user_valid_regex].any? { |regex| regex.match('a') } + expect(any_match).to be_truthy + end + end + end + + describe "ChefConfig::Config[:internal_locale]" do + let(:shell_out) do + cmd = instance_double("Mixlib::ShellOut", exitstatus: 0, stdout: locales, error!: nil) + allow(cmd).to receive(:run_command).and_return(cmd) + cmd + end + + let(:locales) { locale_array.join("\n") } + + before do + allow(Mixlib::ShellOut).to receive(:new).with("locale -a").and_return(shell_out) + end + + shared_examples_for "a suitable locale" do + it "returns an English UTF-8 locale" do + expect(ChefConfig.logger).to_not receive(:warn).with(/Please install an English UTF-8 locale for Chef to use/) + expect(ChefConfig.logger).to_not receive(:debug).with(/Defaulting to locale en_US.UTF-8 on Windows/) + expect(ChefConfig.logger).to_not receive(:debug).with(/No usable locale -a command found/) + expect(ChefConfig::Config.guess_internal_locale).to eq expected_locale + end + end + + context "when the result includes 'C.UTF-8'" do + include_examples "a suitable locale" do + let(:locale_array) { [expected_locale, "en_US.UTF-8"] } + let(:expected_locale) { "C.UTF-8" } + end + end + + context "when the result includes 'en_US.UTF-8'" do + include_examples "a suitable locale" do + let(:locale_array) { ["en_CA.UTF-8", expected_locale, "en_NZ.UTF-8"] } + let(:expected_locale) { "en_US.UTF-8" } + end + end + + context "when the result includes 'en_US.utf8'" do + include_examples "a suitable locale" do + let(:locale_array) { ["en_CA.utf8", "en_US.utf8", "en_NZ.utf8"] } + let(:expected_locale) { "en_US.UTF-8" } + end + end + + context "when the result includes 'en.UTF-8'" do + include_examples "a suitable locale" do + let(:locale_array) { ["en.ISO8859-1", expected_locale] } + let(:expected_locale) { "en.UTF-8" } + end + end + + context "when the result includes 'en_*.UTF-8'" do + include_examples "a suitable locale" do + let(:locale_array) { [expected_locale, "en_CA.UTF-8", "en_GB.UTF-8"] } + let(:expected_locale) { "en_AU.UTF-8" } + end + end + + context "when the result includes 'en_*.utf8'" do + include_examples "a suitable locale" do + let(:locale_array) { ["en_AU.utf8", "en_CA.utf8", "en_GB.utf8"] } + let(:expected_locale) { "en_AU.UTF-8" } + end + end + + context "when the result does not include 'en_*.UTF-8'" do + let(:locale_array) { ["af_ZA", "af_ZA.ISO8859-1", "af_ZA.ISO8859-15", "af_ZA.UTF-8"] } + + it "should fall back to C locale" do + expect(ChefConfig.logger).to receive(:warn).with("Please install an English UTF-8 locale for Chef to use, falling back to C locale and disabling UTF-8 support.") + expect(ChefConfig::Config.guess_internal_locale).to eq 'C' + end + end + + context "on error" do + let(:locale_array) { [] } + + let(:shell_out_cmd) { instance_double("Mixlib::ShellOut") } + + before do + allow(Mixlib::ShellOut).to receive(:new).and_return(shell_out_cmd) + allow(shell_out_cmd).to receive(:run_command) + allow(shell_out_cmd).to receive(:error!).and_raise(Mixlib::ShellOut::ShellCommandFailed, "this is an error") + end + + it "should default to 'en_US.UTF-8'" do + if is_windows + expect(ChefConfig.logger).to receive(:debug).with("Defaulting to locale en_US.UTF-8 on Windows, until it matters that we do something else.") + else + expect(ChefConfig.logger).to receive(:debug).with("No usable locale -a command found, assuming you have en_US.UTF-8 installed.") + end + expect(ChefConfig::Config.guess_internal_locale).to eq "en_US.UTF-8" + end + end + end + end + end + + describe "Treating deprecation warnings as errors" do + + context "when using our default RSpec configuration" do + + it "defaults to treating deprecation warnings as errors" do + expect(ChefConfig::Config[:treat_deprecation_warnings_as_errors]).to be(true) + end + + it "sets CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS environment variable" do + expect(ENV['CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS']).to eq("1") + end + + it "treats deprecation warnings as errors in child processes when testing" do + # Doing a full integration test where we launch a child process is slow + # and liable to break for weird reasons (bundler env stuff, etc.), so + # we're just checking that the presence of the environment variable + # causes treat_deprecation_warnings_as_errors to be set to true after a + # config reset. + ChefConfig::Config.reset + expect(ChefConfig::Config[:treat_deprecation_warnings_as_errors]).to be(true) + end + + end + + context "outside of our test environment" do + + before do + ENV.delete('CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS') + ChefConfig::Config.reset + end + + it "defaults to NOT treating deprecation warnings as errors" do + expect(ChefConfig::Config[:treat_deprecation_warnings_as_errors]).to be(false) + end + end + + + end + +end diff --git a/chef-config/spec/unit/path_helper_spec.rb b/chef-config/spec/unit/path_helper_spec.rb new file mode 100644 index 0000000000..3e6213597a --- /dev/null +++ b/chef-config/spec/unit/path_helper_spec.rb @@ -0,0 +1,291 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 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 'chef-config/path_helper' +require 'spec_helper' + +RSpec.describe ChefConfig::PathHelper do + + let(:path_helper) { described_class } + + shared_examples_for "common_functionality" do + describe "join" do + + it "joins starting with '' resolve to absolute paths" do + expect(path_helper.join('', 'a', 'b')).to eq("#{path_helper.path_separator}a#{path_helper.path_separator}b") + end + + it "joins ending with '' add a / to the end" do + expect(path_helper.join('a', 'b', '')).to eq("a#{path_helper.path_separator}b#{path_helper.path_separator}") + end + + end + + describe "dirname" do + it "dirname('abc') is '.'" do + expect(path_helper.dirname('abc')).to eq('.') + end + it "dirname('/') is '/'" do + expect(path_helper.dirname(path_helper.path_separator)).to eq(path_helper.path_separator) + end + it "dirname('a/b/c') is 'a/b'" do + expect(path_helper.dirname(path_helper.join('a', 'b', 'c'))).to eq(path_helper.join('a', 'b')) + end + it "dirname('a/b/c/') is 'a/b'" do + expect(path_helper.dirname(path_helper.join('a', 'b', 'c', ''))).to eq(path_helper.join('a', 'b')) + end + it "dirname('/a/b/c') is '/a/b'" do + expect(path_helper.dirname(path_helper.join('', 'a', 'b', 'c'))).to eq(path_helper.join('', 'a', 'b')) + end + end + end + + context "on windows" do + + before(:each) do + allow(ChefConfig).to receive(:windows?).and_return(true) + end + + include_examples("common_functionality") + + it "path_separator is \\" do + expect(path_helper.path_separator).to eq('\\') + end + + describe "platform-specific #join behavior" do + + it "joins components on Windows when some end with unix separators" do + expect(path_helper.join('C:\\foo/', "bar", "baz")).to eq('C:\\foo\\bar\\baz') + end + + it "joins components when some end with separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expected = "C:#{expected}" + expect(path_helper.join('C:\\foo\\', "bar", "baz")).to eq(expected) + end + + it "joins components when some end and start with separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expected = "C:#{expected}" + expect(path_helper.join('C:\\foo\\', "bar/", "/baz")).to eq(expected) + end + + it "joins components that don't end in separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expected = "C:#{expected}" + expect(path_helper.join('C:\\foo', "bar", "baz")).to eq(expected) + end + + end + + + it "cleanpath changes slashes into backslashes and leaves backslashes alone" do + expect(path_helper.cleanpath('/a/b\\c/d/')).to eq('\\a\\b\\c\\d') + end + + it "cleanpath does not remove leading double backslash" do + expect(path_helper.cleanpath('\\\\a/b\\c/d/')).to eq('\\\\a\\b\\c\\d') + end + + end + + context "on unix" do + + before(:each) do + allow(ChefConfig).to receive(:windows?).and_return(false) + end + + include_examples("common_functionality") + + it "path_separator is /" do + expect(path_helper.path_separator).to eq('/') + end + + it "cleanpath removes extra slashes alone" do + expect(path_helper.cleanpath('/a///b/c/d/')).to eq('/a/b/c/d') + end + + describe "platform-specific #join behavior" do + + it "joins components when some end with separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expect(path_helper.join("/foo/", "bar", "baz")).to eq(expected) + end + + it "joins components when some end and start with separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expect(path_helper.join("/foo/", "bar/", "/baz")).to eq(expected) + end + + it "joins components that don't end in separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expect(path_helper.join("/foo", "bar", "baz")).to eq(expected) + end + + end + + end + + describe "validate_path" do + context "on windows" do + before(:each) do + # pass by default + allow(ChefConfig).to receive(:windows?).and_return(true) + allow(path_helper).to receive(:printable?).and_return(true) + allow(path_helper).to receive(:windows_max_length_exceeded?).and_return(false) + end + + it "returns the path if the path passes the tests" do + expect(path_helper.validate_path("C:\\ThisIsRigged")).to eql("C:\\ThisIsRigged") + end + + it "does not raise an error if everything looks great" do + expect { path_helper.validate_path("C:\\cool path\\dude.exe") }.not_to raise_error + end + + it "raises an error if the path has invalid characters" do + allow(path_helper).to receive(:printable?).and_return(false) + expect { path_helper.validate_path("Newline!\n") }.to raise_error(ChefConfig::InvalidPath) + end + + it "Adds the \\\\?\\ prefix if the path exceeds MAX_LENGTH and does not have it" do + long_path = "C:\\" + "a" * 250 + "\\" + "b" * 250 + prefixed_long_path = "\\\\?\\" + long_path + allow(path_helper).to receive(:windows_max_length_exceeded?).and_return(true) + expect(path_helper.validate_path(long_path)).to eql(prefixed_long_path) + end + end + end + + describe "windows_max_length_exceeded?" do + it "returns true if the path is too long (259 + NUL) for the API" do + expect(path_helper.windows_max_length_exceeded?("C:\\" + "a" * 250 + "\\" + "b" * 6)).to be_truthy + end + + it "returns false if the path is not too long (259 + NUL) for the standard API" do + expect(path_helper.windows_max_length_exceeded?("C:\\" + "a" * 250 + "\\" + "b" * 5)).to be_falsey + end + + it "returns false if the path is over 259 characters but uses the \\\\?\\ prefix" do + expect(path_helper.windows_max_length_exceeded?("\\\\?\\C:\\" + "a" * 250 + "\\" + "b" * 250)).to be_falsey + end + end + + describe "printable?" do + it "returns true if the string contains no non-printable characters" do + expect(path_helper.printable?("C:\\Program Files (x86)\\Microsoft Office\\Files.lst")).to be_truthy + end + + it "returns true when given 'abc' in unicode" do + expect(path_helper.printable?("\u0061\u0062\u0063")).to be_truthy + end + + it "returns true when given japanese unicode" do + expect(path_helper.printable?("\uff86\uff87\uff88")).to be_truthy + end + + it "returns false if the string contains a non-printable character" do + expect(path_helper.printable?("\my files\work\notes.txt")).to be_falsey + end + + # This isn't necessarily a requirement, but here to be explicit about functionality. + it "returns false if the string contains a newline or tab" do + expect(path_helper.printable?("\tThere's no way,\n\t *no* way,\n\t that you came from my loins.\n")).to be_falsey + end + end + + describe "canonical_path" do + context "on windows", :windows_only do + it "returns an absolute path with backslashes instead of slashes" do + expect(path_helper.canonical_path("\\\\?\\C:/windows/win.ini")).to eq("\\\\?\\c:\\windows\\win.ini") + end + + it "adds the \\\\?\\ prefix if it is missing" do + expect(path_helper.canonical_path("C:/windows/win.ini")).to eq("\\\\?\\c:\\windows\\win.ini") + end + + it "returns a lowercase path" do + expect(path_helper.canonical_path("\\\\?\\C:\\CASE\\INSENSITIVE")).to eq("\\\\?\\c:\\case\\insensitive") + end + end + + context "not on windows", :unix_only do + it "returns a canonical path" do + expect(path_helper.canonical_path("/etc//apache.d/sites-enabled/../sites-available/default")).to eq("/etc/apache.d/sites-available/default") + end + end + end + + describe "paths_eql?" do + it "returns true if the paths are the same" do + allow(path_helper).to receive(:canonical_path).with("bandit").and_return("c:/bandit/bandit") + allow(path_helper).to receive(:canonical_path).with("../bandit/bandit").and_return("c:/bandit/bandit") + expect(path_helper.paths_eql?("bandit", "../bandit/bandit")).to be_truthy + end + + it "returns false if the paths are different" do + allow(path_helper).to receive(:canonical_path).with("bandit").and_return("c:/Bo/Bandit") + allow(path_helper).to receive(:canonical_path).with("../bandit/bandit").and_return("c:/bandit/bandit") + expect(path_helper.paths_eql?("bandit", "../bandit/bandit")).to be_falsey + end + end + + describe "escape_glob" do + it "escapes characters reserved by glob" do + path = "C:\\this\\*path\\[needs]\\escaping?" + escaped_path = "C:\\\\this\\\\\\*path\\\\\\[needs\\]\\\\escaping\\?" + expect(path_helper.escape_glob(path)).to eq(escaped_path) + end + + context "when given more than one argument" do + it "joins, cleanpaths, and escapes characters reserved by glob" do + args = ["this/*path", "[needs]", "escaping?"] + escaped_path = if ChefConfig.windows? + "this\\\\\\*path\\\\\\[needs\\]\\\\escaping\\?" + else + "this/\\*path/\\[needs\\]/escaping\\?" + end + expect(path_helper).to receive(:join).with(*args).and_call_original + expect(path_helper).to receive(:cleanpath).and_call_original + expect(path_helper.escape_glob(*args)).to eq(escaped_path) + end + end + end + + describe "all_homes" do + before do + stub_const('ENV', env) + allow(ChefConfig).to receive(:windows?).and_return(is_windows) + end + + context "on windows" do + let (:is_windows) { true } + end + + context "on unix" do + let (:is_windows) { false } + + context "when HOME is not set" do + let (:env) { {} } + it "returns an empty array" do + expect(path_helper.all_homes).to eq([]) + end + end + end + end +end |