diff options
Diffstat (limited to 'spec')
54 files changed, 2913 insertions, 589 deletions
diff --git a/spec/functional/dsl/reboot_pending_spec.rb b/spec/functional/dsl/reboot_pending_spec.rb index 10d667f7bd..114754ccba 100644 --- a/spec/functional/dsl/reboot_pending_spec.rb +++ b/spec/functional/dsl/reboot_pending_spec.rb @@ -46,8 +46,11 @@ describe Chef::DSL::RebootPending, :windows_only do describe "reboot_pending?" do - context "when there is nothing to indicate a reboot is pending" do - it { expect(recipe.reboot_pending?).to be_false } + describe "when there is nothing to indicate a reboot is pending" do + it "should return false" do + pending "Found existing registry keys" unless registry_safe? + expect(recipe.reboot_pending?).to be_false + end end describe 'HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations' do diff --git a/spec/functional/provider/whyrun_safe_ruby_block_spec.rb b/spec/functional/provider/whyrun_safe_ruby_block_spec.rb new file mode 100644 index 0000000000..150d46d384 --- /dev/null +++ b/spec/functional/provider/whyrun_safe_ruby_block_spec.rb @@ -0,0 +1,51 @@ +# +# Author:: Serdar Sutay (<serdar@opscode.com>) +# Copyright:: Copyright (c) 2014 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' + +describe Chef::Resource::WhyrunSafeRubyBlock do + let(:node) { Chef::Node.new } + + let(:run_context) { + events = Chef::EventDispatch::Dispatcher.new + Chef::RunContext.new(node, {}, events) + } + + before do + $evil_global_evil_laugh = :wahwah + Chef::Config[:why_run] = true + end + + after do + Chef::Config[:why_run] = false + end + + describe "when testing the resource" do + let(:new_resource) do + r = Chef::Resource::WhyrunSafeRubyBlock.new("reload all", run_context) + r.block { $evil_global_evil_laugh = :mwahahaha } + r + end + + it "updates the evil laugh, even in why-run mode" do + new_resource.run_action(new_resource.action) + $evil_global_evil_laugh.should == :mwahahaha + new_resource.should be_updated + end + end +end diff --git a/spec/functional/rebooter_spec.rb b/spec/functional/rebooter_spec.rb new file mode 100644 index 0000000000..8006580d5c --- /dev/null +++ b/spec/functional/rebooter_spec.rb @@ -0,0 +1,105 @@ +# +# Author:: Chris Doherty <cdoherty@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef, 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' + +describe Chef::Platform::Rebooter do + + let(:reboot_info) do + { + :delay_mins => 5, + :requested_by => "reboot resource functional test", + :reason => "rebooter spec test" + } + end + + def create_resource + resource = Chef::Resource::Reboot.new(expected[:requested_by], run_context) + resource.delay_mins(expected[:delay_mins]) + resource.reason(expected[:reason]) + resource + end + + let(:run_context) do + node = Chef::Node.new + events = Chef::EventDispatch::Dispatcher.new + Chef::RunContext.new(node, {}, events) + end + + let(:expected) do + { + :windows => 'shutdown /r /t 5 /c "rebooter spec test"', + :linux => 'shutdown -r +5 "rebooter spec test"' + } + end + + let(:rebooter) { Chef::Platform::Rebooter } + + describe '#reboot_if_needed!' do + + it 'should not call #shell_out! when reboot has not been requested' do + expect(rebooter).to receive(:shell_out!).exactly(0).times + expect(rebooter).to receive(:reboot_if_needed!).once.and_call_original + rebooter.reboot_if_needed!(run_context.node) + end + + describe 'calling #shell_out! to reboot' do + + before(:each) do + run_context.request_reboot(reboot_info) + end + + after(:each) do + run_context.cancel_reboot + end + + shared_context 'test a reboot method' do + def test_rebooter_method(method_sym, is_windows, expected_reboot_str) + Chef::Platform.stub(:windows?).and_return(is_windows) + expect(rebooter).to receive(:shell_out!).once.with(expected_reboot_str) + expect(rebooter).to receive(method_sym).once.and_call_original + rebooter.send(method_sym, run_context.node) + end + end + + describe 'when using #reboot_if_needed!' do + include_context 'test a reboot method' + + it 'should produce the correct string on Windows' do + test_rebooter_method(:reboot_if_needed!, true, expected[:windows]) + end + + it 'should produce the correct (Linux-specific) string on non-Windows' do + test_rebooter_method(:reboot_if_needed!, false, expected[:linux]) + end + end + + describe 'when using #reboot!' do + include_context 'test a reboot method' + + it 'should produce the correct string on Windows' do + test_rebooter_method(:reboot!, true, expected[:windows]) + end + + it 'should produce the correct (Linux-specific) string on non-Windows' do + test_rebooter_method(:reboot!, false, expected[:linux]) + end + end + end + end +end diff --git a/spec/functional/resource/dsc_script_spec.rb b/spec/functional/resource/dsc_script_spec.rb new file mode 100644 index 0000000000..fa13296c02 --- /dev/null +++ b/spec/functional/resource/dsc_script_spec.rb @@ -0,0 +1,337 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# 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 'spec_helper' +require 'chef/mixin/shell_out' +require 'chef/mixin/windows_architecture_helper' + +describe Chef::Resource::DscScript, :windows_powershell_dsc_only do + include Chef::Mixin::WindowsArchitectureHelper + before(:all) do + @temp_dir = ::Dir.mktmpdir("dsc-functional-test") + end + + after(:all) do + ::FileUtils.rm_rf(@temp_dir) if ::Dir.exist?(@temp_dir) + end + + include Chef::Mixin::ShellOut + + def create_config_script_from_code(code, configuration_name, data = false) + script_code = data ? code : "Configuration '#{configuration_name}'\n{\n\t#{code}\n}\n" + data_suffix = data ? '_config_data' : '' + extension = data ? 'psd1' : 'ps1' + script_path = "#{@temp_dir}/dsc_functional_test#{data_suffix}.#{extension}" + ::File.open(script_path, 'wt') do | script | + script.write(script_code) + end + script_path + end + + def user_exists?(target_user) + result = false + begin + shell_out!("net user #{target_user}") + result = true + rescue Mixlib::ShellOut::ShellCommandFailed + end + result + end + + def delete_user(target_user) + begin + shell_out!("net user #{target_user} /delete") + rescue Mixlib::ShellOut::ShellCommandFailed + end + end + + let(:dsc_env_variable) { 'chefenvtest' } + let(:dsc_env_value1) { 'value1' } + let(:env_value2) { 'value2' } + let(:dsc_test_run_context) { + node = Chef::Node.new + node.automatic['platform'] = 'windows' + node.automatic['platform_version'] = '6.1' + node.automatic['kernel'][:machine] = + is_i386_process_on_x86_64_windows? ? :x86_64 : :i386 + node.automatic[:languages][:powershell][:version] = '4.0' + empty_events = Chef::EventDispatch::Dispatcher.new + Chef::RunContext.new(node, {}, empty_events) + } + let(:dsc_test_resource_name) { 'DSCTest' } + let(:dsc_test_resource_base) { + Chef::Resource::DscScript.new(dsc_test_resource_name, dsc_test_run_context) + } + let(:test_registry_key) { 'HKEY_LOCAL_MACHINE\Software\Chef\Spec\Functional\Resource\dsc_script_spec' } + let(:test_registry_value) { 'Registration' } + let(:test_registry_data1) { 'LL927' } + let(:test_registry_data2) { 'LL928' } + let(:dsc_code) { <<-EOH + Registry "ChefRegKey" + { + Key = '#{test_registry_key}' + ValueName = '#{test_registry_value}' + ValueData = '#{test_registry_data}' + Ensure = 'Present' + } +EOH + } + + let(:dsc_user_prefix) { 'dsc' } + let(:dsc_user_suffix) { 'chefx' } + let(:dsc_user) {"#{dsc_user_prefix}_usr_#{dsc_user_suffix}" } + let(:dsc_user_prefix_env_var_name) { 'dsc_user_env_prefix' } + let(:dsc_user_suffix_env_var_name) { 'dsc_user_env_suffix' } + let(:dsc_user_prefix_env_code) { "$env:#{dsc_user_prefix_env_var_name}"} + let(:dsc_user_suffix_env_code) { "$env:#{dsc_user_suffix_env_var_name}"} + let(:dsc_user_prefix_param_name) { 'dsc_user_prefix_param' } + let(:dsc_user_suffix_param_name) { 'dsc_user_suffix_param' } + let(:dsc_user_prefix_param_code) { "$#{dsc_user_prefix_param_name}"} + let(:dsc_user_suffix_param_code) { "$#{dsc_user_suffix_param_name}"} + let(:dsc_user_env_code) { "\"$(#{dsc_user_prefix_env_code})_usr_$(#{dsc_user_suffix_env_code})\""} + let(:dsc_user_param_code) { "\"$(#{dsc_user_prefix_param_code})_usr_$(#{dsc_user_suffix_param_code})\""} + + let(:config_flags) { nil } + let(:config_params) { <<-EOH + + [CmdletBinding()] + param + ( + $#{dsc_user_prefix_param_name}, + $#{dsc_user_suffix_param_name} + ) +EOH + } + + let(:config_param_section) { '' } + let(:dsc_user_code) { "'#{dsc_user}'" } + let(:dsc_user_prefix_code) { dsc_user_prefix } + let(:dsc_user_suffix_code) { dsc_user_suffix } + let(:dsc_script_environment_attribute) { nil } + let(:dsc_user_resources_code) { <<-EOH + #{config_param_section} +node localhost +{ +$testuser = #{dsc_user_code} +$testpassword = ConvertTo-SecureString -String "jf9a8m49jrajf4#" -AsPlainText -Force +$testcred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $testuser, $testpassword + +User dsctestusercreate +{ + UserName = $testuser + Password = $testcred + Description = "DSC test user" + Ensure = "Present" + Disabled = $false + PasswordNeverExpires = $true + PasswordChangeRequired = $false +} +} +EOH + } + + let(:dsc_user_config_data) { +<<-EOH +@{ + AllNodes = @( + @{ + NodeName = "localhost"; + PSDscAllowPlainTextPassword = $true + } + ) +} + +EOH + } + + let(:dsc_environment_env_var_name) { 'dsc_test_cwd' } + let(:dsc_environment_no_fail_not_etc_directory) { "#{ENV['systemroot']}\\system32" } + let(:dsc_environment_fail_etc_directory) { "#{ENV['systemroot']}\\system32\\drivers\\etc" } + let(:exception_message_signature) { 'LL927-LL928' } + let(:dsc_environment_config) {<<-EOH +if (($pwd.path -eq '#{dsc_environment_fail_etc_directory}') -and (test-path('#{dsc_environment_fail_etc_directory}'))) +{ + throw 'Signature #{exception_message_signature}: Purposefully failing because cwd == #{dsc_environment_fail_etc_directory}' +} +environment "whatsmydir" +{ + Name = '#{dsc_environment_env_var_name}' + Value = $pwd.path + Ensure = 'Present' +} +EOH + } + + let(:dsc_config_name) { + dsc_test_resource_base.name + } + let(:dsc_resource_from_code) { + dsc_test_resource_base.code(dsc_code) + dsc_test_resource_base + } + let(:config_name_value) { dsc_test_resource_base.name } + + let(:dsc_resource_from_path) { + dsc_test_resource_base.command(create_config_script_from_code(dsc_code, config_name_value)) + dsc_test_resource_base + } + + before(:each) do + test_key_resource = Chef::Resource::RegistryKey.new(test_registry_key, dsc_test_run_context) + test_key_resource.recursive(true) + test_key_resource.run_action(:delete_key) + end + + after(:each) do + test_key_resource = Chef::Resource::RegistryKey.new(test_registry_key, dsc_test_run_context) + test_key_resource.recursive(true) + test_key_resource.run_action(:delete_key) + end + + shared_examples_for 'a dsc_script resource with specified PowerShell configuration code' do + let(:test_registry_data) { test_registry_data1 } + it 'should create a registry key with a specific registry value and data' do + expect(dsc_test_resource.registry_key_exists?(test_registry_key)).to eq(false) + dsc_test_resource.run_action(:run) + expect(dsc_test_resource.registry_key_exists?(test_registry_key)).to eq(true) + expect(dsc_test_resource.registry_value_exists?(test_registry_key, {:name => test_registry_value, :type => :string, :data => test_registry_data})).to eq(true) + end + + it_should_behave_like 'a dsc_script resource with configuration affected by cwd' + end + + shared_examples_for 'a dsc_script resource with configuration affected by cwd' do + after(:each) do + removal_resource = Chef::Resource::DscScript.new(dsc_test_resource_name, dsc_test_run_context) + removal_resource.code <<-EOH +environment 'removethis' +{ + Name = '#{dsc_environment_env_var_name}' + Ensure = 'Absent' +} +EOH + removal_resource.run_action(:run) + end + let(:dsc_code) { dsc_environment_config } + it 'should not raise an exception if the cwd is not etc' do + dsc_test_resource.cwd(dsc_environment_no_fail_not_etc_directory) + expect {dsc_test_resource.run_action(:run)}.not_to raise_error + end + + it 'should raise an exception if the cwd is etc' do + dsc_test_resource.cwd(dsc_environment_fail_etc_directory) + expect {dsc_test_resource.run_action(:run)}.to raise_error(Chef::Exceptions::PowershellCmdletException) + begin + dsc_test_resource.run_action(:run) + rescue Chef::Exceptions::PowershellCmdletException => e + expect(e.message).to match(exception_message_signature) + end + end + end + + shared_examples_for 'a parameterized DSC configuration script' do + context 'when specifying environment variables in the environment attribute' do + let(:dsc_user_prefix_code) { dsc_user_prefix_env_code } + let(:dsc_user_suffix_code) { dsc_user_suffix_env_code } + it_behaves_like 'a dsc_script with configuration that uses environment variables' + end + end + + shared_examples_for 'a dsc_script with configuration data' do + context 'when using the configuration_data attribute' do + let(:configuration_data_attribute) { 'configuration_data' } + it_behaves_like 'a dsc_script with configuration data set via an attribute' + end + + context 'when using the configuration_data_script attribute' do + let(:configuration_data_attribute) { 'configuration_data_script' } + it_behaves_like 'a dsc_script with configuration data set via an attribute' + end + end + + shared_examples_for 'a dsc_script with configuration data set via an attribute' do + it 'should run a configuration script that creates a user' do + config_data_value = dsc_user_config_data + dsc_test_resource.configuration_name(config_name_value) + if configuration_data_attribute == 'configuration_data_script' + config_data_value = create_config_script_from_code(dsc_user_config_data, '', true) + end + dsc_test_resource.environment({dsc_user_prefix_env_var_name => dsc_user_prefix, + dsc_user_suffix_env_var_name => dsc_user_suffix}) + dsc_test_resource.send(configuration_data_attribute, config_data_value) + dsc_test_resource.flags(config_flags) + expect(user_exists?(dsc_user)).to eq(false) + expect {dsc_test_resource.run_action(:run)}.not_to raise_error + expect(user_exists?(dsc_user)).to eq(true) + end + end + + shared_examples_for 'a dsc_script with configuration data that takes parameters' do + context 'when script code takes parameters for configuration' do + let(:dsc_user_code) { dsc_user_param_code } + let(:config_param_section) { config_params } + let(:config_flags) {{:"#{dsc_user_prefix_param_name}" => "#{dsc_user_prefix}", :"#{dsc_user_suffix_param_name}" => "#{dsc_user_suffix}"}} + it 'does not directly contain the user name' do + configuration_script_content = ::File.open(dsc_test_resource.command) do | file | + file.read + end + expect(configuration_script_content.include?(dsc_user)).to be(false) + end + it_behaves_like 'a dsc_script with configuration data' + end + + end + + shared_examples_for 'a dsc_script with configuration data that uses environment variables' do + context 'when script code uses environment variables' do + let(:dsc_user_code) { dsc_user_env_code } + + it 'does not directly contain the user name' do + configuration_script_content = ::File.open(dsc_test_resource.command) do | file | + file.read + end + expect(configuration_script_content.include?(dsc_user)).to be(false) + end + it_behaves_like 'a dsc_script with configuration data' + end + end + + context 'when supplying configuration through the configuration attribute' do + let(:dsc_test_resource) { dsc_resource_from_code } + it_behaves_like 'a dsc_script resource with specified PowerShell configuration code' + end + + context 'when supplying configuration using the path attribute' do + let(:dsc_test_resource) { dsc_resource_from_path } + it_behaves_like 'a dsc_script resource with specified PowerShell configuration code' + end + + context 'when running a configuration that manages users' do + before(:each) do + delete_user(dsc_user) + end + + let(:dsc_code) { dsc_user_resources_code } + let(:config_name_value) { 'DSCTestConfig' } + let(:dsc_test_resource) { dsc_resource_from_path } + + it_behaves_like 'a dsc_script with configuration data' + it_behaves_like 'a dsc_script with configuration data that uses environment variables' + it_behaves_like 'a dsc_script with configuration data that takes parameters' + end +end diff --git a/spec/functional/resource/file_spec.rb b/spec/functional/resource/file_spec.rb index d6f56db3e9..99966f85c8 100644 --- a/spec/functional/resource/file_spec.rb +++ b/spec/functional/resource/file_spec.rb @@ -24,12 +24,18 @@ describe Chef::Resource::File do let(:file_base) { "file_spec" } let(:expected_content) { "Don't fear the ruby." } - def create_resource + def create_resource(opts={}) events = Chef::EventDispatch::Dispatcher.new node = Chef::Node.new run_context = Chef::RunContext.new(node, {}, events) - resource = Chef::Resource::File.new(path, run_context) - resource + + use_path = if opts[:use_relative_path] + File.basename(path) + else + path + end + + Chef::Resource::File.new(use_path, run_context) end let(:resource) do @@ -42,6 +48,10 @@ describe Chef::Resource::File do create_resource end + let(:resource_with_relative_path) do + create_resource(:use_relative_path => true) + end + let(:unmanaged_content) do "This is file content that is not managed by chef" end @@ -74,6 +84,19 @@ describe Chef::Resource::File do end end + # github issue 1842. + describe "when running action :create on a relative path" do + before do + resource_with_relative_path.run_action(:create) + end + + context "and the file exists" do + it "should run without an exception" do + resource_with_relative_path.run_action(:create) + end + end + end + describe "when running action :touch" do context "and the target file does not exist" do before do diff --git a/spec/functional/resource/link_spec.rb b/spec/functional/resource/link_spec.rb index 8e630d84f2..2220e973cf 100644 --- a/spec/functional/resource/link_spec.rb +++ b/spec/functional/resource/link_spec.rb @@ -72,8 +72,8 @@ describe Chef::Resource::Link do end end - def canonicalize(path) - windows? ? path.gsub('/', '\\') : path + def paths_eql?(path1, path2) + Chef::Util::PathHelper.paths_eql?(path1, path2) end def symlink(a, b) @@ -180,7 +180,7 @@ describe Chef::Resource::Link do it 'links to the target file' do symlink?(target_file).should be_true - readlink(target_file).should == canonicalize(to) + paths_eql?(readlink(target_file), to).should be_true end it 'marks the resource updated' do resource.should be_updated @@ -201,7 +201,7 @@ describe Chef::Resource::Link do it 'leaves the file linked' do symlink?(target_file).should be_true - readlink(target_file).should == canonicalize(to) + paths_eql?(readlink(target_file), to).should be_true end it 'does not mark the resource updated' do resource.should_not be_updated @@ -279,7 +279,7 @@ describe Chef::Resource::Link do before(:each) do symlink(to, target_file) symlink?(target_file).should be_true - readlink(target_file).should == canonicalize(to) + paths_eql?(readlink(target_file), to).should be_true end include_context 'create symbolic link is noop' include_context 'delete succeeds' @@ -294,7 +294,7 @@ describe Chef::Resource::Link do File.open(@other_target, 'w') { |file| file.write('eek') } symlink(@other_target, target_file) symlink?(target_file).should be_true - readlink(target_file).should == canonicalize(@other_target) + paths_eql?(readlink(target_file), @other_target).should be_true end after(:each) do File.delete(@other_target) @@ -311,7 +311,7 @@ describe Chef::Resource::Link do nonexistent = File.join(test_file_dir, make_tmpname('nonexistent_spec')) symlink(nonexistent, target_file) symlink?(target_file).should be_true - readlink(target_file).should == canonicalize(nonexistent) + paths_eql?(readlink(target_file), nonexistent).should be_true end include_context 'create symbolic link succeeds' include_context 'delete succeeds' @@ -393,7 +393,7 @@ describe Chef::Resource::Link do File.open(@other_target, "w") { |file| file.write("eek") } symlink(@other_target, to) symlink?(to).should be_true - readlink(to).should == canonicalize(@other_target) + paths_eql?(readlink(to), @other_target).should be_true end after(:each) do File.delete(@other_target) @@ -408,7 +408,7 @@ describe Chef::Resource::Link do @other_target = File.join(test_file_dir, make_tmpname("other_spec")) symlink(@other_target, to) symlink?(to).should be_true - readlink(to).should == canonicalize(@other_target) + paths_eql?(readlink(to), @other_target).should be_true end context 'and the link does not yet exist' do include_context 'create symbolic link succeeds' @@ -441,7 +441,7 @@ describe Chef::Resource::Link do before(:each) do symlink(to, target_file) symlink?(target_file).should be_true - readlink(target_file).should == canonicalize(to) + paths_eql?(readlink(target_file), to).should be_true end include_context 'create symbolic link is noop' include_context 'delete succeeds' @@ -450,7 +450,7 @@ describe Chef::Resource::Link do before(:each) do symlink(absolute_to, target_file) symlink?(target_file).should be_true - readlink(target_file).should == canonicalize(absolute_to) + paths_eql?(readlink(target_file), absolute_to).should be_true end include_context 'create symbolic link succeeds' include_context 'delete succeeds' @@ -478,7 +478,7 @@ describe Chef::Resource::Link do before(:each) do symlink(to, target_file) symlink?(target_file).should be_true - readlink(target_file).should == canonicalize(to) + paths_eql?(readlink(target_file), to).should be_true end include_context 'create hard link succeeds' it_behaves_like 'delete errors out' @@ -552,7 +552,7 @@ describe Chef::Resource::Link do File.open(@other_target, "w") { |file| file.write("eek") } symlink(@other_target, to) symlink?(to).should be_true - readlink(to).should == canonicalize(@other_target) + paths_eql?(readlink(to), @other_target).should be_true end after(:each) do File.delete(@other_target) @@ -564,7 +564,7 @@ describe Chef::Resource::Link do # OS X gets angry about this sort of link. Bug in OS X, IMO. pending('OS X/FreeBSD/AIX symlink? and readlink working on hard links to symlinks', :if => (os_x? or freebsd? or aix?)) do symlink?(target_file).should be_true - readlink(target_file).should == canonicalize(@other_target) + paths_eql?(readlink(target_file), @other_target).should be_true end end include_context 'delete is noop' @@ -575,7 +575,7 @@ describe Chef::Resource::Link do @other_target = File.join(test_file_dir, make_tmpname("other_spec")) symlink(@other_target, to) symlink?(to).should be_true - readlink(to).should == canonicalize(@other_target) + paths_eql?(readlink(to), @other_target).should be_true end context 'and the link does not yet exist' do it 'links to the target file' do @@ -588,7 +588,7 @@ describe Chef::Resource::Link do File.exists?(target_file).should be_false end symlink?(target_file).should be_true - readlink(target_file).should == canonicalize(@other_target) + paths_eql?(readlink(target_file), @other_target).should be_true end end include_context 'delete is noop' diff --git a/spec/functional/resource/reboot_spec.rb b/spec/functional/resource/reboot_spec.rb new file mode 100644 index 0000000000..735ca994c8 --- /dev/null +++ b/spec/functional/resource/reboot_spec.rb @@ -0,0 +1,103 @@ +# +# Author:: Chris Doherty <cdoherty@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef, 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' + +describe Chef::Resource::Reboot do + + let(:expected) do + { + :delay_mins => 5, + :requested_by => "reboot resource functional test", + :reason => "reboot resource spec test" + } + end + + def create_resource + node = Chef::Node.new + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, events) + resource = Chef::Resource::Reboot.new(expected[:requested_by], run_context) + resource.delay_mins(expected[:delay_mins]) + resource.reason(expected[:reason]) + resource + end + + let(:resource) do + create_resource + end + + shared_context 'testing run context modification' do + def test_reboot_action(resource) + reboot_info = resource.run_context.reboot_info + expect(reboot_info.keys.sort).to eq([:delay_mins, :reason, :requested_by, :timestamp]) + expect(reboot_info[:delay_mins]).to eq(expected[:delay_mins]) + expect(reboot_info[:reason]).to eq(expected[:reason]) + expect(reboot_info[:requested_by]).to eq(expected[:requested_by]) + + expect(resource.run_context.reboot_requested?).to be_true + end + end + + # the currently defined behavior for multiple calls to this resource is "last one wins." + describe 'the request_reboot_on_successful_run action' do + include_context 'testing run context modification' + + before do + resource.run_action(:request_reboot) + end + + after do + resource.run_context.cancel_reboot + end + + it 'should have modified the run context correctly' do + test_reboot_action(resource) + end + end + + describe 'the reboot_interrupt_run action' do + include_context 'testing run context modification' + + after do + resource.run_context.cancel_reboot + end + + it 'should have modified the run context correctly' do + # this doesn't actually test the flow of Chef::Client#do_run, unfortunately. + expect { + resource.run_action(:reboot_now) + }.to throw_symbol(:end_client_run_early) + + test_reboot_action(resource) + end + end + + describe "the cancel action" do + before do + resource.run_context.request_reboot(expected) + resource.run_action(:cancel) + end + + it 'should have cleared the reboot request' do + # arguably we shouldn't be querying RunContext's internal data directly. + expect(resource.run_context.reboot_info).to eq({}) + expect(resource.run_context.reboot_requested?).to be_false + end + end +end diff --git a/spec/functional/resource/user/dscl_spec.rb b/spec/functional/resource/user/dscl_spec.rb index 5f13bfcb0b..ba508e3258 100644 --- a/spec/functional/resource/user/dscl_spec.rb +++ b/spec/functional/resource/user/dscl_spec.rb @@ -21,7 +21,8 @@ require 'chef/mixin/shell_out' metadata = { :unix_only => true, :requires_root => true, - :provider => {:user => Chef::Provider::User::Dscl} + :provider => {:user => Chef::Provider::User::Dscl}, + :not_supported_on_mac_osx_106 => true, } describe "Chef::Resource::User with Chef::Provider::User::Dscl provider", metadata do diff --git a/spec/functional/util/path_helper_spec.rb b/spec/functional/util/path_helper_spec.rb new file mode 100644 index 0000000000..ccdf383c22 --- /dev/null +++ b/spec/functional/util/path_helper_spec.rb @@ -0,0 +1,37 @@ +# +# 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 'tmpdir' +require 'chef/util/path_helper' +require 'spec_helper' + +describe Chef::Util::PathHelper, "escape_glob" do + PathHelper = Chef::Util::PathHelper + + it "escapes the glob metacharacters so globbing succeeds" do + # make a dir + Dir.mktmpdir("\\silly[dir]") do |dir| + # add some files + files = ["some.rb", "file.txt", "names.csv"] + files.each do |file| + File.new(File.join(dir, file), 'w').close + end + + pattern = File.join(PathHelper.escape_glob(dir), "*") + Dir.glob(pattern).map { |x| File.basename(x) }.should match_array(files) + end + end +end diff --git a/spec/functional/util/powershell/cmdlet_spec.rb b/spec/functional/util/powershell/cmdlet_spec.rb new file mode 100644 index 0000000000..63d1ac09b5 --- /dev/null +++ b/spec/functional/util/powershell/cmdlet_spec.rb @@ -0,0 +1,114 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# +# Copyright:: 2014, Chef Software, Inc. +# +# 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 'json' +require File.expand_path('../../../../spec_helper', __FILE__) + +describe Chef::Util::Powershell::Cmdlet, :windows_only do + before(:all) do + ohai = Ohai::System.new + ohai.load_plugins + ohai.run_plugins(true, ['platform', 'kernel']) + @node = Chef::Node.new + @node.consume_external_attrs(ohai.data, {}) + end + let(:cmd_output_format) { :text } + let(:simple_cmdlet) { Chef::Util::Powershell::Cmdlet.new(@node, 'get-childitem', cmd_output_format, {:depth => 2}) } + let(:invalid_cmdlet) { Chef::Util::Powershell::Cmdlet.new(@node, 'get-idontexist', cmd_output_format) } + let(:cmdlet_get_item_requires_switch_or_argument) { Chef::Util::Powershell::Cmdlet.new(@node, 'get-item', cmd_output_format, {:depth => 2}) } + let(:cmdlet_alias_requires_switch_or_argument) { Chef::Util::Powershell::Cmdlet.new(@node, 'alias', cmd_output_format, {:depth => 2}) } + let(:etc_directory) { "#{ENV['systemroot']}\\system32\\drivers\\etc" } + let(:architecture_cmdlet) { Chef::Util::Powershell::Cmdlet.new(@node, "$env:PROCESSOR_ARCHITECTURE")} + + it "executes a simple process" do + result = simple_cmdlet.run + expect(result.succeeded?).to eq(true) + end + + it "#run does not raise a PowershellCmdletException exception if the command cannot be executed" do + expect {invalid_cmdlet.run}.not_to raise_error + end + + it "#run! raises a PowershellCmdletException exception if the command cannot be executed" do + expect {invalid_cmdlet.run!}.to raise_error(Chef::Exceptions::PowershellCmdletException) + end + + it "executes a 64-bit command on a 64-bit OS, 32-bit otherwise" do + os_arch = ENV['PROCESSOR_ARCHITEW6432'] + if os_arch.nil? + os_arch = ENV['PROCESSOR_ARCHITECTURE'] + end + + result = architecture_cmdlet.run + execution_arch = result.return_value + execution_arch.strip! + expect(execution_arch).to eq(os_arch) + end + + it "passes command line switches to the command" do + result = cmdlet_alias_requires_switch_or_argument.run({:name => 'ls'}) + expect(result.succeeded?).to eq(true) + end + + it "passes command line arguments to the command" do + result = cmdlet_alias_requires_switch_or_argument.run({},{},'ls') + expect(result.succeeded?).to eq(true) + end + + it "passes command line arguments and switches to the command" do + result = cmdlet_get_item_requires_switch_or_argument.run({:path => etc_directory},{},' | select-object -property fullname | format-table -hidetableheaders') + expect(result.succeeded?).to eq(true) + returned_directory = result.return_value + returned_directory.strip! + expect(returned_directory).to eq(etc_directory) + end + + it "passes execution options to the command" do + result = cmdlet_get_item_requires_switch_or_argument.run({},{:cwd => etc_directory},'. | select-object -property fullname | format-table -hidetableheaders') + expect(result.succeeded?).to eq(true) + returned_directory = result.return_value + returned_directory.strip! + expect(returned_directory).to eq(etc_directory) + end + + context "when returning json" do + let(:cmd_output_format) { :json } + it "returns json format data", :windows_powershell_dsc_only do + result = cmdlet_alias_requires_switch_or_argument.run({},{},'ls') + expect(result.succeeded?).to eq(true) + expect(lambda{JSON.parse(result.return_value)}).not_to raise_error + end + end + + context "when returning Ruby objects" do + let(:cmd_output_format) { :object } + it "returns object format data", :windows_powershell_dsc_only do + result = simple_cmdlet.run({},{:cwd => etc_directory}, 'hosts') + expect(result.succeeded?).to eq(true) + data = result.return_value + expect(data['Name']).to eq('hosts') + end + end + + context "when constructor is given invalid arguments" do + let(:cmd_output_format) { :invalid } + it "throws an exception if an invalid format is passed to the constructor" do + expect(lambda{simple_cmdlet}).to raise_error + end + end +end diff --git a/spec/integration/client/client_spec.rb b/spec/integration/client/client_spec.rb index 8a1a65249b..0144ae0ce3 100644 --- a/spec/integration/client/client_spec.rb +++ b/spec/integration/client/client_spec.rb @@ -16,7 +16,7 @@ describe "chef-client" do # machine that has omnibus chef installed. In that case we need to ensure # we're running `chef-client` from the source tree and not the external one. # cf. CHEF-4914 - let(:chef_client) { "ruby #{chef_dir}/chef-client" } + let(:chef_client) { "ruby '#{chef_dir}/chef-client'" } when_the_repository "has a cookbook with a no-op recipe" do before { file 'cookbooks/x/recipes/default.rb', '' } diff --git a/spec/integration/client/ipv6_spec.rb b/spec/integration/client/ipv6_spec.rb index f49b7b7711..76dd1938f7 100644 --- a/spec/integration/client/ipv6_spec.rb +++ b/spec/integration/client/ipv6_spec.rb @@ -76,7 +76,7 @@ END_CLIENT_RB let(:chef_dir) { File.join(File.dirname(__FILE__), "..", "..", "..", "bin") } - let(:chef_client_cmd) { %Q[ruby #{chef_dir}/chef-client -c "#{path_to('config/client.rb')}" -lwarn] } + let(:chef_client_cmd) { %Q[ruby '#{chef_dir}/chef-client' -c "#{path_to('config/client.rb')}" -lwarn] } after do FileUtils.rm_rf(cache_path) diff --git a/spec/integration/knife/cookbook_api_ipv6_spec.rb b/spec/integration/knife/cookbook_api_ipv6_spec.rb index 4191bb1731..c5b5b81abe 100644 --- a/spec/integration/knife/cookbook_api_ipv6_spec.rb +++ b/spec/integration/knife/cookbook_api_ipv6_spec.rb @@ -62,7 +62,7 @@ END_VALIDATION_PEM end let(:chef_dir) { File.join(File.dirname(__FILE__), "..", "..", "..", "bin") } - let(:knife) { "ruby #{chef_dir}/knife" } + let(:knife) { "ruby '#{chef_dir}/knife'" } let(:knife_config_flag) { "-c '#{path_to("config/knife.rb")}'" } diff --git a/spec/integration/recipes/lwrp_inline_resources_spec.rb b/spec/integration/recipes/lwrp_inline_resources_spec.rb index 9e2cf3fc8d..a0c13da6f7 100644 --- a/spec/integration/recipes/lwrp_inline_resources_spec.rb +++ b/spec/integration/recipes/lwrp_inline_resources_spec.rb @@ -16,7 +16,7 @@ describe "LWRPs with inline resources" do # machine that has omnibus chef installed. In that case we need to ensure # we're running `chef-client` from the source tree and not the external one. # cf. CHEF-4914 - let(:chef_client) { "ruby #{chef_dir}/chef-client" } + let(:chef_client) { "ruby '#{chef_dir}/chef-client'" } when_the_repository "has a cookbook with a nested LWRP" do before do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7c11957997..ed0a8f89f6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -107,8 +107,11 @@ RSpec.configure do |config| config.filter_run_excluding :not_supported_on_win2k3 => true if windows_win2k3? config.filter_run_excluding :not_supported_on_solaris => true if solaris? config.filter_run_excluding :win2k3_only => true unless windows_win2k3? + config.filter_run_excluding :windows_2008r2_or_later => true unless windows_2008r2_or_later? config.filter_run_excluding :windows64_only => true unless windows64? config.filter_run_excluding :windows32_only => true unless windows32? + config.filter_run_excluding :windows_powershell_dsc_only => true unless windows_powershell_dsc? + config.filter_run_excluding :windows_powershell_no_dsc_only => true unless ! windows_powershell_dsc? config.filter_run_excluding :windows_domain_joined_only => true unless windows_domain_joined? config.filter_run_excluding :solaris_only => true unless solaris? config.filter_run_excluding :system_windows_service_gem_only => true unless system_windows_service_gem? diff --git a/spec/support/platform_helpers.rb b/spec/support/platform_helpers.rb index a7c616d7a7..f8cad6de7f 100644 --- a/spec/support/platform_helpers.rb +++ b/spec/support/platform_helpers.rb @@ -52,6 +52,30 @@ def windows_win2k3? (host['version'] && host['version'].start_with?("5.2")) end +def windows_2008r2_or_later? + return false unless windows? + wmi = WmiLite::Wmi.new + host = wmi.first_of('Win32_OperatingSystem') + version = host['version'] + return false unless version + components = version.split('.').map do | component | + component.to_i + end + components.length >=2 && components[0] >= 6 && components[1] >= 1 +end + +def windows_powershell_dsc? + return false unless windows? + supports_dsc = false + begin + wmi = WmiLite::Wmi.new('root/microsoft/windows/desiredstateconfiguration') + lcm = wmi.query("SELECT * FROM meta_class WHERE __this ISA 'MSFT_DSCLocalConfigurationManager'") + supports_dsc = !! lcm + rescue WmiLite::WmiException + end + supports_dsc +end + def mac_osx_106? if File.exists? "/usr/bin/sw_vers" result = shell_out("/usr/bin/sw_vers") diff --git a/spec/support/shared/functional/file_resource.rb b/spec/support/shared/functional/file_resource.rb index 804830fcdc..72b72912bd 100644 --- a/spec/support/shared/functional/file_resource.rb +++ b/spec/support/shared/functional/file_resource.rb @@ -284,6 +284,7 @@ shared_examples_for "a file resource" do before do Chef::Config[:why_run] = true + Chef::Config[:ssl_verify_mode] = :verify_none end after do @@ -333,6 +334,10 @@ shared_examples_for "file resource not pointing to a real file" do !symlink?(file_path) && File.file?(file_path) end + before do + Chef::Config[:ssl_verify_mode] = :verify_none + end + describe "when force_unlink is set to true" do it ":create unlinks the target" do real_file?(path).should be_false @@ -363,6 +368,7 @@ shared_examples_for "a configured file resource" do before do Chef::Log.level = :info + Chef::Config[:ssl_verify_mode] = :verify_none end # note the stripping of the drive letter from the tmpdir on windows diff --git a/spec/support/shared/integration/integration_helper.rb b/spec/support/shared/integration/integration_helper.rb index 465633b9e0..b42f7f69d9 100644 --- a/spec/support/shared/integration/integration_helper.rb +++ b/spec/support/shared/integration/integration_helper.rb @@ -118,7 +118,10 @@ module IntegrationSupport Chef::Config.delete("#{object_name}_path".to_sym) end Chef::Config.delete(:chef_repo_path) - FileUtils.remove_entry_secure(@repository_dir) + # TODO: "force" actually means "silence all exceptions". this + # silences a weird permissions error on Windows that we should track + # down, but for now there's no reason for it to blow up our CI. + FileUtils.remove_entry_secure(@repository_dir, force=Chef::Platform.windows?) ensure @repository_dir = nil end diff --git a/spec/support/shared/matchers.rb b/spec/support/shared/matchers.rb deleted file mode 100644 index 2e1c660c19..0000000000 --- a/spec/support/shared/matchers.rb +++ /dev/null @@ -1,17 +0,0 @@ - -require 'rspec/expectations' -require 'spec/support/platform_helpers' - -RSpec::Matchers.define :match_environment_variable do |varname| - match do |actual| - expected = if windows? && ENV[varname].nil? - # On Windows, if an environment variable is not set, the command - # `echo %VARNAME%` outputs %VARNAME% - "%#{varname}%" - else - ENV[varname].to_s - end - - actual == expected - end -end diff --git a/spec/support/shared/matchers/exit_with_code.rb b/spec/support/shared/matchers/exit_with_code.rb new file mode 100644 index 0000000000..957586c85d --- /dev/null +++ b/spec/support/shared/matchers/exit_with_code.rb @@ -0,0 +1,28 @@ +require 'rspec/expectations' + +# Lifted from http://stackoverflow.com/questions/1480537/how-can-i-validate-exits-and-aborts-in-rspec +RSpec::Matchers.define :exit_with_code do |exp_code| + actual = nil + match do |block| + begin + block.call + rescue SystemExit => e + actual = e.status + end + actual and actual == exp_code + end + + failure_message_for_should do |block| + "expected block to call exit(#{exp_code}) but exit" + + (actual.nil? ? " not called" : "(#{actual}) was called") + end + + failure_message_for_should_not do |block| + "expected block not to call exit(#{exp_code})" + end + + description do + "expect block to call exit(#{exp_code})" + end + +end diff --git a/spec/support/shared/matchers/match_environment_variable.rb b/spec/support/shared/matchers/match_environment_variable.rb new file mode 100644 index 0000000000..c8c905f44a --- /dev/null +++ b/spec/support/shared/matchers/match_environment_variable.rb @@ -0,0 +1,17 @@ + +require 'rspec/expectations' +require 'spec/support/platform_helpers' + +RSpec::Matchers.define :match_environment_variable do |varname| + match do |actual| + expected = if windows? && ENV[varname].nil? + # On Windows, if an environment variable is not set, the command + # `echo %VARNAME%` outputs %VARNAME% + "%#{varname}%" + else + ENV[varname].to_s + end + + actual == expected + end +end diff --git a/spec/unit/application/apply.rb b/spec/unit/application/apply.rb index 32c98c6ed6..62a53c2a31 100644 --- a/spec/unit/application/apply.rb +++ b/spec/unit/application/apply.rb @@ -20,7 +20,7 @@ require 'spec_helper' describe Chef::Application::Apply do before do - @app = Chef::Application::Recipe.new + @app = Chef::Application::Apply.new @app.stub(:configure_logging).and_return(true) @recipe_text = "package 'nyancat'" Chef::Config[:solo] = true @@ -73,4 +73,14 @@ describe Chef::Application::Apply do @recipe_fh.path.should == @app.instance_variable_get(:@recipe_filename) end end + describe "recipe_file_arg" do + before do + ARGV.clear + end + it "should exit and log message" do + Chef::Log.should_receive(:debug).with(/^No recipe file provided/) + lambda { @app.run }.should raise_error(SystemExit) { |e| e.status.should == 1 } + end + + end end diff --git a/spec/unit/config_spec.rb b/spec/unit/config_spec.rb index af71c43b77..41411669e6 100644 --- a/spec/unit/config_spec.rb +++ b/spec/unit/config_spec.rb @@ -242,8 +242,8 @@ describe Chef::Config do Chef::Config[:file_backup_path].should == backup_path end - it "Chef::Config[:ssl_verify_mode] defaults to :verify_none" do - Chef::Config[:ssl_verify_mode].should == :verify_none + it "Chef::Config[:ssl_verify_mode] defaults to :verify_peer" do + Chef::Config[:ssl_verify_mode].should == :verify_peer end it "Chef::Config[:ssl_ca_path] defaults to nil" do diff --git a/spec/unit/dsl/data_query_spec.rb b/spec/unit/dsl/data_query_spec.rb index 8a985437b7..78cd5569e8 100644 --- a/spec/unit/dsl/data_query_spec.rb +++ b/spec/unit/dsl/data_query_spec.rb @@ -86,123 +86,21 @@ describe Chef::DSL::DataQuery do end context "when the item is encrypted" do - let(:default_secret) { "abc123SECRET" } - - let(:encoded_data) { Chef::EncryptedDataBagItem.encrypt_data_bag_item(raw_data, default_secret) } - - let(:item) do - item = Chef::DataBagItem.new - item.data_bag(bag_name) - item.raw_data = encoded_data - item - end + let(:secret) { "abc123SECRET" } + let(:enc_data_bag) { double("Chef::EncryptedDataBagItem") } before do allow( Chef::DataBagItem ).to receive(:load).with(bag_name, item_name).and_return(item) + expect(language).to receive(:encrypted?).and_return(true) + expect( Chef::EncryptedDataBagItem ).to receive(:load_secret).and_return(secret) end - shared_examples_for "encryption detected" do - let(:encoded_data) do - Chef::Config[:data_bag_encrypt_version] = version - Chef::EncryptedDataBagItem.encrypt_data_bag_item(raw_data, default_secret) - end - - before do - allow( Chef::EncryptedDataBagItem ).to receive(:load_secret).and_return(default_secret) - end - - it "detects encrypted data bag" do - expect( encryptor ).to receive(:encryptor_keys).at_least(:once).and_call_original - expect( Chef::Log ).to receive(:debug).with(/Data bag item looks encrypted/) - language.data_bag_item(bag_name, item_name) - end - end - - context "when encryption version is 1" do - include_examples "encryption detected" do - let(:version) { 1 } - let(:encryptor) { Chef::EncryptedDataBagItem::Encryptor::Version1Encryptor } - end - end - - context "when encryption version is 2" do - include_examples "encryption detected" do - let(:version) { 2 } - let(:encryptor) { Chef::EncryptedDataBagItem::Encryptor::Version2Encryptor } - end + it "detects encrypted data bag" do + expect( Chef::EncryptedDataBagItem ).to receive(:new).with(raw_data, secret).and_return(enc_data_bag) + expect( Chef::Log ).to receive(:debug).with(/Data bag item looks encrypted/) + expect(language.data_bag_item(bag_name, item_name)).to eq(enc_data_bag) end - context "when encryption version is 3", :ruby_20_only do - include_examples "encryption detected" do - let(:version) { 3 } - let(:encryptor) { Chef::EncryptedDataBagItem::Encryptor::Version3Encryptor } - end - end - - shared_examples_for "an encrypted data bag item" do - it "returns an encrypted data bag item" do - expect( language.data_bag_item(bag_name, item_name, secret) ).to be_a_kind_of(Chef::EncryptedDataBagItem) - end - - it "decrypts the contents of the data bag item" do - expect( language.data_bag_item(bag_name, item_name, secret).to_hash ).to eql raw_data - end - end - - context "when a secret is supplied" do - include_examples "an encrypted data bag item" do - let(:secret) { default_secret } - end - end - - context "when a secret is not supplied" do - before do - allow( Chef::Config ).to receive(:[]).and_call_original - expect( Chef::Config ).to receive(:[]).with(:encrypted_data_bag_secret).and_return(path) - expect( Chef::EncryptedDataBagItem ).to receive(:load_secret).and_call_original - end - - context "when a secret is located at Chef::Config[:encrypted_data_bag_secret]" do - let(:path) { "/tmp/my_secret" } - - before do - expect( File ).to receive(:exist?).with(path).and_return(true) - expect( IO ).to receive(:read).with(path).and_return(default_secret) - end - - include_examples "an encrypted data bag item" do - let(:secret) { nil } - end - end - - shared_examples_for "no secret file" do - it "should fail to load the data bag item" do - expect( Chef::Log ).to receive(:error).with(/Failed to load secret for encrypted data bag item/) - expect( Chef::Log ).to receive(:error).with(/Failed to load data bag item/) - expect{ language.data_bag_item(bag_name, item_name) }.to raise_error(error_type, error_message) - end - end - - context "when Chef::Config[:encrypted_data_bag_secret] is not configured" do - include_examples "no secret file" do - let(:path) { nil } - let(:error_type) { ArgumentError } - let(:error_message) { /No secret specified and no secret found/ } - end - end - - context "when Chef::Config[:encrypted_data_bag_secret] does not exist" do - include_examples "no secret file" do - before do - expect( File ).to receive(:exist?).with(path).and_return(false) - end - - let(:path) { "/tmp/my_secret" } - let(:error_type) { Errno::ENOENT } - let(:error_message) { /file not found/ } - end - end - end end end end diff --git a/spec/unit/dsl/reboot_pending_spec.rb b/spec/unit/dsl/reboot_pending_spec.rb index 8576ae168a..0d643514e0 100644 --- a/spec/unit/dsl/reboot_pending_spec.rb +++ b/spec/unit/dsl/reboot_pending_spec.rb @@ -21,7 +21,7 @@ require "spec_helper" describe Chef::DSL::RebootPending do describe "reboot_pending?" do - describe "in isoloation" do + describe "in isolation" do let(:recipe) { Object.new.extend(Chef::DSL::RebootPending) } before do @@ -74,12 +74,6 @@ describe Chef::DSL::RebootPending do end end - context "platform is not supported" do - it 'should raise an exception' do - recipe.stub_chain(:node, :[]).with(:platform).and_return('msdos') - expect { recipe.reboot_pending? }.to raise_error(Chef::Exceptions::UnsupportedPlatform) - end - end end # describe in isolation describe "in a recipe" do diff --git a/spec/unit/encrypted_data_bag_item/check_encrypted_spec.rb b/spec/unit/encrypted_data_bag_item/check_encrypted_spec.rb new file mode 100644 index 0000000000..1da5efb36e --- /dev/null +++ b/spec/unit/encrypted_data_bag_item/check_encrypted_spec.rb @@ -0,0 +1,95 @@ +# +# Author:: Tyler Ball (<tball@getchef.com>) +# Copyright:: Copyright (c) 2010-2014 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/encrypted_data_bag_item/check_encrypted' + +class CheckEncryptedTester + include Chef::EncryptedDataBagItem::CheckEncrypted +end + +describe Chef::EncryptedDataBagItem::CheckEncrypted do + + let(:tester) { CheckEncryptedTester.new } + + it "detects the item is not encrypted when the data is empty" do + expect(tester.encrypted?({})).to eq(false) + end + + it "detects the item is not encrypted when the data only contains an id" do + expect(tester.encrypted?({id: "foo"})).to eq(false) + end + + context "when the item is encrypted" do + + let(:default_secret) { "abc123SECRET" } + let(:item_name) { "item_name" } + let(:raw_data) {{ + "id" => item_name, + "greeting" => "hello", + "nested" => { + "a1" => [1, 2, 3], + "a2" => { "b1" => true } + } + }} + + let(:version) { 1 } + let(:encoded_data) do + Chef::Config[:data_bag_encrypt_version] = version + Chef::EncryptedDataBagItem.encrypt_data_bag_item(raw_data, default_secret) + end + + it "does not detect encryption when the item version is unknown" do + # It shouldn't be possible for someone to normally encrypt an item with an unknown version - they would have to + # do something funky like encrypting it and then manually changing the version + modified_encoded_data = encoded_data + modified_encoded_data["greeting"]["version"] = 4 + expect(tester.encrypted?(modified_encoded_data)).to eq(false) + end + + shared_examples_for "encryption detected" do + it "detects encrypted data bag" do + expect( encryptor ).to receive(:encryptor_keys).at_least(:once).and_call_original + expect(tester.encrypted?(encoded_data)).to eq(true) + end + end + + context "when encryption version is 1" do + include_examples "encryption detected" do + let(:version) { 1 } + let(:encryptor) { Chef::EncryptedDataBagItem::Encryptor::Version1Encryptor } + end + end + + context "when encryption version is 2" do + include_examples "encryption detected" do + let(:version) { 2 } + let(:encryptor) { Chef::EncryptedDataBagItem::Encryptor::Version2Encryptor } + end + end + + context "when encryption version is 3", :ruby_20_only do + include_examples "encryption detected" do + let(:version) { 3 } + let(:encryptor) { Chef::EncryptedDataBagItem::Encryptor::Version3Encryptor } + end + end + + end + +end diff --git a/spec/unit/knife/bootstrap_spec.rb b/spec/unit/knife/bootstrap_spec.rb index 78be9632f6..d5c668753e 100644 --- a/spec/unit/knife/bootstrap_spec.rb +++ b/spec/unit/knife/bootstrap_spec.rb @@ -30,6 +30,7 @@ describe Chef::Knife::Bootstrap do k.merge_configs k.ui.stub(:stderr).and_return(stderr) + allow(k).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(false) k end @@ -221,10 +222,6 @@ describe Chef::Knife::Bootstrap do k end - # Include a data bag secret in the options to prevent Bootstrap from - # attempting to access /etc/chef/encrypted_data_bag_secret, which - # can fail when the file exists but can't be accessed by the user - # running the tests. let(:options){ ["--bootstrap-no-proxy", setting, "-s", "foo"] } let(:template_file) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "no_proxy.erb")) } let(:rendered_template) do @@ -290,7 +287,6 @@ describe Chef::Knife::Bootstrap do describe "specifying the encrypted data bag secret key" do let(:secret) { "supersekret" } - let(:secret_file) { File.join(CHEF_SPEC_DATA, 'bootstrap', 'encrypted_data_bag_secret') } let(:options) { [] } let(:bootstrap_template) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "secret.erb")) } let(:rendered_template) do @@ -299,59 +295,55 @@ describe Chef::Knife::Bootstrap do knife.render_template end - context "via --secret" do - let(:options){ ["--secret", secret] } - - it "creates a secret file" do - rendered_template.should match(%r{#{secret}}) - end + it "creates a secret file" do + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(true) + expect(knife).to receive(:read_secret).and_return(secret) + rendered_template.should match(%r{#{secret}}) + end - it "renders the client.rb with an encrypted_data_bag_secret entry" do - rendered_template.should match(%r{encrypted_data_bag_secret\s*"/etc/chef/encrypted_data_bag_secret"}) - end + it "renders the client.rb with an encrypted_data_bag_secret entry" do + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(true) + expect(knife).to receive(:read_secret).and_return(secret) + rendered_template.should match(%r{encrypted_data_bag_secret\s*"/etc/chef/encrypted_data_bag_secret"}) end - context "via --secret-file" do - let(:options) { ["--secret-file", secret_file] } - let(:secret) { IO.read(secret_file) } + end - it "creates a secret file" do - rendered_template.should match(%r{#{secret}}) - end + describe "when transferring trusted certificates" do + let(:trusted_certs_dir) { File.join(CHEF_SPEC_DATA, 'trusted_certs') } - it "renders the client.rb with an encrypted_data_bag_secret entry" do - rendered_template.should match(%r{encrypted_data_bag_secret\s*"/etc/chef/encrypted_data_bag_secret"}) - end + let(:rendered_template) do + knife.merge_configs + knife.render_template end - context "secret via config" do - before do - Chef::Config[:knife][:secret] = secret - end - - it "creates a secret file" do - rendered_template.should match(%r{#{secret}}) - end + before do + Chef::Config[:trusted_certs_dir] = trusted_certs_dir + IO.stub(:read).and_call_original + IO.stub(:read).with(File.expand_path(Chef::Config[:validation_key])).and_return("") + end - it "renders the client.rb with an encrypted_data_bag_secret entry" do - rendered_template.should match(%r{encrypted_data_bag_secret\s*"/etc/chef/encrypted_data_bag_secret"}) - end + def certificates + Dir[File.join(trusted_certs_dir, "*.{crt,pem}")] end - context "secret-file via config" do - let(:secret) { IO.read(secret_file) } + it "creates /etc/chef/trusted_certs" do + rendered_template.should match(%r{mkdir -p /etc/chef/trusted_certs}) + end - before do - Chef::Config[:knife][:secret_file] = secret_file + it "copies the certificates in the directory" do + certificates.each do |cert| + IO.should_receive(:read).with(File.expand_path(cert)) end - it "creates a secret file" do - rendered_template.should match(%r{#{secret}}) + certificates.each do |cert| + rendered_template.should match(%r{cat > /etc/chef/trusted_certs/#{File.basename(cert)} <<'EOP'}) end + end - it "renders the client.rb with an encrypted_data_bag_secret entry" do - rendered_template.should match(%r{encrypted_data_bag_secret\s*"/etc/chef/encrypted_data_bag_secret"}) - end + it "doesn't create /etc/chef/trusted_certs if :trusted_certs_dir is empty" do + Dir.should_receive(:glob).with(File.join(trusted_certs_dir, "*.{crt,pem}")).and_return([]) + rendered_template.should_not match(%r{mkdir -p /etc/chef/trusted_certs}) end end diff --git a/spec/unit/knife/core/bootstrap_context_spec.rb b/spec/unit/knife/core/bootstrap_context_spec.rb index 064f8c5621..266991a7dd 100644 --- a/spec/unit/knife/core/bootstrap_context_spec.rb +++ b/spec/unit/knife/core/bootstrap_context_spec.rb @@ -29,9 +29,10 @@ describe Chef::Knife::Core::BootstrapContext do :validation_client_name => 'chef-validator-testing' } end - let(:secret_file) { File.join(CHEF_SPEC_DATA, 'bootstrap', 'encrypted_data_bag_secret') } - subject(:bootstrap_context) { described_class.new(config, run_list, chef_config) } + let(:secret) { nil } + + subject(:bootstrap_context) { described_class.new(config, run_list, chef_config, secret) } it "runs chef with the first-boot.json in the _default environment" do bootstrap_context.start_chef.should eq "chef-client -j /etc/chef/first-boot.json -E _default" @@ -105,26 +106,9 @@ EXPECTED end describe "when an encrypted_data_bag_secret is provided" do - context "via config[:secret]" do - let(:chef_config) do - { - :knife => {:secret => "supersekret" } - } - end - it "reads the encrypted_data_bag_secret" do - bootstrap_context.encrypted_data_bag_secret.should eq "supersekret" - end - end - - context "via config[:secret_file]" do - let(:chef_config) do - { - :knife => {:secret_file => secret_file} - } - end - it "reads the encrypted_data_bag_secret" do - bootstrap_context.encrypted_data_bag_secret.should eq IO.read(secret_file) - end + let(:secret) { "supersekret" } + it "reads the encrypted_data_bag_secret" do + bootstrap_context.encrypted_data_bag_secret.should eq "supersekret" end end diff --git a/spec/unit/knife/data_bag_create_spec.rb b/spec/unit/knife/data_bag_create_spec.rb index 984be8e58a..c31c88577d 100644 --- a/spec/unit/knife/data_bag_create_spec.rb +++ b/spec/unit/knife/data_bag_create_spec.rb @@ -20,97 +20,89 @@ require 'spec_helper' require 'tempfile' -module ChefSpecs - class ChefRest - attr_reader :args_received - def initialize - @args_received = [] - end - - def post_rest(*args) - @args_received << args - end +describe Chef::Knife::DataBagCreate do + let(:knife) do + k = Chef::Knife::DataBagCreate.new + allow(k).to receive(:rest).and_return(rest) + allow(k.ui).to receive(:stdout).and_return(stdout) + k end -end + let(:rest) { double("Chef::REST") } + let(:stdout) { StringIO.new } -describe Chef::Knife::DataBagCreate do - before do - Chef::Config[:node_name] = "webmonkey.example.com" - @knife = Chef::Knife::DataBagCreate.new - @rest = ChefSpecs::ChefRest.new - @knife.stub(:rest).and_return(@rest) - @stdout = StringIO.new - @knife.ui.stub(:stdout).and_return(@stdout) - end + let(:bag_name) { "sudoing_admins" } + let(:item_name) { "ME" } + + let(:secret) { "abc123SECRET" } + let(:raw_hash) {{ "login_name" => "alphaomega", "id" => item_name }} - it "creates a data bag when given one argument" do - @knife.name_args = ['sudoing_admins'] - @rest.should_receive(:post_rest).with("data", {"name" => "sudoing_admins"}) - @knife.ui.should_receive(:info).with("Created data_bag[sudoing_admins]") + let(:config) { {} } - @knife.run + before do + Chef::Config[:node_name] = "webmonkey.example.com" + knife.name_args = [bag_name, item_name] + allow(knife).to receive(:config).and_return(config) end it "tries to create a data bag with an invalid name when given one argument" do - @knife.name_args = ['invalid&char'] - @knife.should_receive(:exit).with(1) - - @knife.run + knife.name_args = ['invalid&char'] + expect(Chef::DataBag).to receive(:validate_name!).with(knife.name_args[0]).and_raise(Chef::Exceptions::InvalidDataBagName) + expect {knife.run}.to exit_with_code(1) end - it "creates a data bag item when given two arguments" do - @knife.name_args = ['sudoing_admins', 'ME'] - user_supplied_hash = {"login_name" => "alphaomega", "id" => "ME"} - data_bag_item = Chef::DataBagItem.from_hash(user_supplied_hash) - data_bag_item.data_bag("sudoing_admins") - @knife.should_receive(:create_object).and_yield(user_supplied_hash) - @rest.should_receive(:post_rest).with("data", {'name' => 'sudoing_admins'}).ordered - @rest.should_receive(:post_rest).with("data/sudoing_admins", data_bag_item).ordered + context "when given one argument" do + before do + knife.name_args = [bag_name] + end + + it "creates a data bag" do + expect(rest).to receive(:post_rest).with("data", {"name" => bag_name}) + expect(knife.ui).to receive(:info).with("Created data_bag[#{bag_name}]") - @knife.run + knife.run + end end - describe "encrypted data bag items" do - before(:each) do - @secret = "abc123SECRET" - @plain_data = {"login_name" => "alphaomega", "id" => "ME"} - @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data, - @secret) - @knife.name_args = ['sudoing_admins', 'ME'] - @knife.should_receive(:create_object).and_yield(@plain_data) - data_bag_item = Chef::DataBagItem.from_hash(@enc_data) - data_bag_item.data_bag("sudoing_admins") - - # Random IV is used each time the data bag item is encrypted, so values - # will not be equal if we re-encrypt. - Chef::EncryptedDataBagItem.should_receive(:encrypt_data_bag_item).and_return(@enc_data) - - @rest.should_receive(:post_rest).with("data", {'name' => 'sudoing_admins'}).ordered - @rest.should_receive(:post_rest).with("data/sudoing_admins", data_bag_item).ordered - - @secret_file = Tempfile.new("encrypted_data_bag_secret_file_test") - @secret_file.puts(@secret) - @secret_file.flush + context "no secret is specified for encryption" do + let(:item) do + item = Chef::DataBagItem.from_hash(raw_hash) + item.data_bag(bag_name) + item end - after do - @secret_file.close - @secret_file.unlink + it "creates a data bag item" do + expect(knife).to receive(:create_object).and_yield(raw_hash) + expect(knife).to receive(:encryption_secret_provided?).and_return(false) + expect(rest).to receive(:post_rest).with("data", {'name' => bag_name}).ordered + expect(rest).to receive(:post_rest).with("data/#{bag_name}", item).ordered + + knife.run end + end + + context "a secret is specified for encryption" do + let(:encoded_data) { Chef::EncryptedDataBagItem.encrypt_data_bag_item(raw_hash, secret) } - it "creates an encrypted data bag item via --secret" do - @knife.stub(:config).and_return({:secret => @secret}) - @knife.run + let(:item) do + item = Chef::DataBagItem.from_hash(encoded_data) + item.data_bag(bag_name) + item end - it "creates an encrypted data bag item via --secret_file" do - secret_file = Tempfile.new("encrypted_data_bag_secret_file_test") - secret_file.puts(@secret) - secret_file.flush - @knife.stub(:config).and_return({:secret_file => secret_file.path}) - @knife.run + it "creates an encrypted data bag item" do + expect(knife).to receive(:create_object).and_yield(raw_hash) + expect(knife).to receive(:encryption_secret_provided?).and_return(true) + expect(knife).to receive(:read_secret).and_return(secret) + expect(Chef::EncryptedDataBagItem) + .to receive(:encrypt_data_bag_item) + .with(raw_hash, secret) + .and_return(encoded_data) + expect(rest).to receive(:post_rest).with("data", {"name" => bag_name}).ordered + expect(rest).to receive(:post_rest).with("data/#{bag_name}", item).ordered + + knife.run end end diff --git a/spec/unit/knife/data_bag_edit_spec.rb b/spec/unit/knife/data_bag_edit_spec.rb index 866ca99174..6f19b5e63e 100644 --- a/spec/unit/knife/data_bag_edit_spec.rb +++ b/spec/unit/knife/data_bag_edit_spec.rb @@ -21,73 +21,107 @@ require 'tempfile' describe Chef::Knife::DataBagEdit do before do - @plain_data = {"login_name" => "alphaomega", "id" => "item_name"} - @edited_data = { - "login_name" => "rho", "id" => "item_name", - "new_key" => "new_value" } + Chef::Config[:node_name] = "webmonkey.example.com" + knife.name_args = [bag_name, item_name] + allow(knife).to receive(:config).and_return(config) + end + + let(:knife) do + k = Chef::Knife::DataBagEdit.new + allow(k).to receive(:rest).and_return(rest) + allow(k.ui).to receive(:stdout).and_return(stdout) + k + end + + let(:raw_hash) { {"login_name" => "alphaomega", "id" => "item_name"} } + let(:db) { Chef::DataBagItem.from_hash(raw_hash)} + let(:raw_edited_hash) { {"login_name" => "rho", "id" => "item_name", "new_key" => "new_value"} } + + let(:rest) { double("Chef::REST") } + let(:stdout) { StringIO.new } - Chef::Config[:node_name] = "webmonkey.example.com" + let(:bag_name) { "sudoing_admins" } + let(:item_name) { "ME" } - @knife = Chef::Knife::DataBagEdit.new - @rest = double('chef-rest-mock') - @knife.stub(:rest).and_return(@rest) + let(:secret) { "abc123SECRET" } - @stdout = StringIO.new - @knife.stub(:stdout).and_return(@stdout) - @log = Chef::Log - @knife.name_args = ['bag_name', 'item_name'] + let(:config) { {} } + + let(:is_encrypted?) { false } + let(:transmitted_hash) { raw_edited_hash } + let(:data_to_edit) { db } + + shared_examples_for "editing a data bag" do + it "correctly edits then uploads the data bag" do + expect(Chef::DataBagItem).to receive(:load).with(bag_name, item_name).and_return(db) + expect(knife).to receive(:encrypted?).with(db.raw_data).and_return(is_encrypted?) + expect(knife).to receive(:edit_data).with(data_to_edit).and_return(raw_edited_hash) + expect(rest).to receive(:put_rest).with("data/#{bag_name}/#{item_name}", transmitted_hash).ordered + + knife.run + end end it "requires data bag and item arguments" do - @knife.name_args = [] - lambda { @knife.run }.should raise_error(SystemExit) - @stdout.string.should match(/^You must supply the data bag and an item to edit/) + knife.name_args = [] + expect(stdout).to receive(:puts).twice.with(anything) + expect {knife.run}.to exit_with_code(1) + expect(stdout.string).to eq("") end - it "saves edits on a data bag item" do - Chef::DataBagItem.stub(:load).with('bag_name', 'item_name').and_return(@plain_data) - @knife.should_receive(:edit_data).with(@plain_data).and_return(@edited_data) - @rest.should_receive(:put_rest).with("data/bag_name/item_name", @edited_data).ordered - @knife.run + context "when no secret is provided" do + include_examples "editing a data bag" end - describe "encrypted data bag items" do - before(:each) do - @secret = "abc123SECRET" - @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data, - @secret) - @enc_edited_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@edited_data, - @secret) - Chef::DataBagItem.stub(:load).with('bag_name', 'item_name').and_return(@enc_data) - - # Random IV is used each time the data bag item is encrypted, so values - # will not be equal if we encrypt same value twice. - Chef::EncryptedDataBagItem.should_receive(:encrypt_data_bag_item).and_return(@enc_edited_data) - - @secret_file = Tempfile.new("encrypted_data_bag_secret_file_test") - @secret_file.puts(@secret) - @secret_file.flush + context "when config[:print_after] is set" do + let(:config) { {:print_after => true} } + before do + expect(knife.ui).to receive(:output).with(raw_edited_hash) end - after do - @secret_file.close - @secret_file.unlink + include_examples "editing a data bag" + end + + context "when a secret is provided" do + let!(:enc_raw_hash) { Chef::EncryptedDataBagItem.encrypt_data_bag_item(raw_hash, secret) } + let!(:enc_edited_hash) { Chef::EncryptedDataBagItem.encrypt_data_bag_item(raw_edited_hash, secret) } + let(:transmitted_hash) { enc_edited_hash } + + before(:each) do + expect(knife).to receive(:read_secret).at_least(1).times.and_return(secret) + expect(Chef::EncryptedDataBagItem).to receive(:encrypt_data_bag_item).with(raw_edited_hash, secret).and_return(enc_edited_hash) end - it "decrypts and encrypts via --secret" do - @knife.stub(:config).and_return({:secret => @secret}) - @knife.should_receive(:edit_data).with(@plain_data).and_return(@edited_data) - @rest.should_receive(:put_rest).with("data/bag_name/item_name", @enc_edited_data).ordered + context "the data bag starts encrypted" do + let(:is_encrypted?) { true } + let(:db) { Chef::DataBagItem.from_hash(enc_raw_hash) } + # If the data bag is encrypted, it gets passed to `edit` as a hash. Otherwise, it gets passed as a DataBag + let (:data_to_edit) { raw_hash } + + before(:each) do + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(true) + end - @knife.run + include_examples "editing a data bag" end - it "decrypts and encrypts via --secret_file" do - @knife.stub(:config).and_return({:secret_file => @secret_file.path}) - @knife.should_receive(:edit_data).with(@plain_data).and_return(@edited_data) - @rest.should_receive(:put_rest).with("data/bag_name/item_name", @enc_edited_data).ordered + context "the data bag starts unencrypted" do + before(:each) do + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).exactly(0).times + expect(knife).to receive(:encryption_secret_provided?).and_return(true) + end - @knife.run + include_examples "editing a data bag" end end + + it "fails to edit an encrypted data bag if the secret is missing" do + expect(Chef::DataBagItem).to receive(:load).with(bag_name, item_name).and_return(db) + expect(knife).to receive(:encrypted?).with(db.raw_data).and_return(true) + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(false) + + expect(knife.ui).to receive(:fatal).with("You cannot edit an encrypted data bag without providing the secret.") + expect {knife.run}.to exit_with_code(1) + end + end diff --git a/spec/unit/knife/data_bag_from_file_spec.rb b/spec/unit/knife/data_bag_from_file_spec.rb index 1ad6b4712c..662af3f0d6 100644 --- a/spec/unit/knife/data_bag_from_file_spec.rb +++ b/spec/unit/knife/data_bag_from_file_spec.rb @@ -26,168 +26,145 @@ Chef::Knife::DataBagFromFile.load_deps describe Chef::Knife::DataBagFromFile do before :each do - Chef::Config[:node_name] = "webmonkey.example.com" - @knife = Chef::Knife::DataBagFromFile.new - @rest = double("Chef::REST") - @knife.stub(:rest).and_return(@rest) - @stdout = StringIO.new - @knife.ui.stub(:stdout).and_return(@stdout) - @tmp_dir = Dir.mktmpdir - @db_folder = File.join(@tmp_dir, 'data_bags', 'bag_name') - FileUtils.mkdir_p(@db_folder) - @db_file = Tempfile.new(["data_bag_from_file_test", ".json"], @db_folder) - @db_file2 = Tempfile.new(["data_bag_from_file_test2", ".json"], @db_folder) - @db_folder2 = File.join(@tmp_dir, 'data_bags', 'bag_name2') - FileUtils.mkdir_p(@db_folder2) - @db_file3 = Tempfile.new(["data_bag_from_file_test3", ".json"], @db_folder2) - @plain_data = { - "id" => "item_name", - "greeting" => "hello", - "nested" => { "a1" => [1, 2, 3], "a2" => { "b1" => true }} - } - @db_file.write(@plain_data.to_json) - @db_file.flush - @knife.instance_variable_set(:@name_args, ['bag_name', @db_file.path]) + Chef::Config[:node_name] = "webmonkey.example.com" + FileUtils.mkdir_p([db_folder, db_folder2]) + db_file.write(plain_data.to_json) + db_file.flush + allow(knife).to receive(:config).and_return(config) + allow(Chef::Knife::Core::ObjectLoader).to receive(:new).and_return(loader) end # We have to explicitly clean up Tempfile on Windows because it said so. after :each do - @db_file.close - @db_file2.close - @db_file3.close - FileUtils.rm_rf(@db_folder) - FileUtils.rm_rf(@db_folder2) - FileUtils.remove_entry_secure @tmp_dir + db_file.close + db_file2.close + db_file3.close + FileUtils.rm_rf(db_folder) + FileUtils.rm_rf(db_folder2) + FileUtils.remove_entry_secure tmp_dir end + let(:knife) do + k = Chef::Knife::DataBagFromFile.new + allow(k).to receive(:rest).and_return(rest) + allow(k.ui).to receive(:stdout).and_return(stdout) + k + end + + let(:tmp_dir) { Dir.mktmpdir } + let(:db_folder) { File.join(tmp_dir, data_bags_path, bag_name) } + let(:db_file) { Tempfile.new(["data_bag_from_file_test", ".json"], db_folder) } + let(:db_file2) { Tempfile.new(["data_bag_from_file_test2", ".json"], db_folder) } + let(:db_folder2) { File.join(tmp_dir, data_bags_path, bag_name2) } + let(:db_file3) { Tempfile.new(["data_bag_from_file_test3", ".json"], db_folder2) } + + def new_bag_expects(b = bag_name, d = plain_data) + data_bag = double + expect(data_bag).to receive(:data_bag).with(b) + expect(data_bag).to receive(:raw_data=).with(d) + expect(data_bag).to receive(:save) + expect(data_bag).to receive(:data_bag) + expect(data_bag).to receive(:id) + data_bag + end + + let(:loader) { double("Knife::Core::ObjectLoader") } + + let(:data_bags_path) { "data_bags" } + let(:plain_data) { { + "id" => "item_name", + "greeting" => "hello", + "nested" => { "a1" => [1, 2, 3], "a2" => { "b1" => true }} + } } + let(:enc_data) { Chef::EncryptedDataBagItem.encrypt_data_bag_item(plain_data, secret) } + + let(:rest) { double("Chef::REST") } + let(:stdout) { StringIO.new } + + let(:bag_name) { "sudoing_admins" } + let(:bag_name2) { "sudoing_admins2" } + let(:item_name) { "ME" } + + let(:secret) { "abc123SECRET" } + + let(:config) { {} } + it "loads from a file and saves" do - @knife.loader.should_receive(:load_from).with("data_bags", 'bag_name', @db_file.path).and_return(@plain_data) - dbag = Chef::DataBagItem.new - Chef::DataBagItem.stub(:new).and_return(dbag) - dbag.should_receive(:save) - @knife.run - - dbag.data_bag.should == 'bag_name' - dbag.raw_data.should == @plain_data + knife.name_args = [bag_name, db_file.path] + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, db_file.path).and_return(plain_data) + expect(Chef::DataBagItem).to receive(:new).and_return(new_bag_expects) + + knife.run end - it "loads all from a mutiple files and saves" do - @knife.name_args = [ 'bag_name', @db_file.path, @db_file2.path ] - @knife.loader.should_receive(:load_from).with("data_bags", 'bag_name', @db_file.path).and_return(@plain_data) - @knife.loader.should_receive(:load_from).with("data_bags", 'bag_name', @db_file2.path).and_return(@plain_data) - dbag = Chef::DataBagItem.new - Chef::DataBagItem.stub(:new).and_return(dbag) - dbag.should_receive(:save).twice - @knife.run - - dbag.data_bag.should == 'bag_name' - dbag.raw_data.should == @plain_data + it "loads all from multiple files and saves" do + knife.name_args = [ bag_name, db_file.path, db_file2.path ] + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, db_file.path).and_return(plain_data) + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, db_file2.path).and_return(plain_data) + expect(Chef::DataBagItem).to receive(:new).twice.and_return(new_bag_expects, new_bag_expects) + + knife.run end it "loads all from a folder and saves" do - @knife.name_args = [ 'bag_name', @db_folder ] - @knife.loader.should_receive(:load_from).with("data_bags", 'bag_name', @db_file.path).and_return(@plain_data) - @knife.loader.should_receive(:load_from).with("data_bags", 'bag_name', @db_file2.path).and_return(@plain_data) - dbag = Chef::DataBagItem.new - Chef::DataBagItem.stub(:new).and_return(dbag) - dbag.should_receive(:save).twice - @knife.run + knife.name_args = [ bag_name, db_folder ] + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, db_file.path).and_return(plain_data) + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, db_file2.path).and_return(plain_data) + expect(Chef::DataBagItem).to receive(:new).twice.and_return(new_bag_expects, new_bag_expects) + + knife.run end describe "loading all data bags" do - before do - @pwd = Dir.pwd - Dir.chdir(@tmp_dir) - end - - after do - Dir.chdir(@pwd) - end - it "loads all data bags when -a or --all options is provided" do - @knife.name_args = [] - @knife.stub(:config).and_return({:all => true}) - @knife.loader.should_receive(:load_from).with("data_bags", "bag_name", File.basename(@db_file.path)). - and_return(@plain_data) - @knife.loader.should_receive(:load_from).with("data_bags", "bag_name", File.basename(@db_file2.path)). - and_return(@plain_data) - @knife.loader.should_receive(:load_from).with("data_bags", "bag_name2", File.basename(@db_file3.path)). - and_return(@plain_data) - dbag = Chef::DataBagItem.new - Chef::DataBagItem.stub(:new).and_return(dbag) - dbag.should_receive(:save).exactly(3).times - @knife.run + knife.name_args = [] + config[:all] = true + expect(loader).to receive(:find_all_object_dirs).with("./#{data_bags_path}").and_return([bag_name, bag_name2]) + expect(loader).to receive(:find_all_objects).with("./#{data_bags_path}/#{bag_name}").and_return([File.basename(db_file.path), File.basename(db_file2.path)]) + expect(loader).to receive(:find_all_objects).with("./#{data_bags_path}/#{bag_name2}").and_return([File.basename(db_file3.path)]) + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, File.basename(db_file.path)).and_return(plain_data) + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, File.basename(db_file2.path)).and_return(plain_data) + expect(loader).to receive(:load_from).with(data_bags_path, bag_name2, File.basename(db_file3.path)).and_return(plain_data) + expect(Chef::DataBagItem).to receive(:new).exactly(3).times.and_return(new_bag_expects, new_bag_expects, new_bag_expects(bag_name2)) + + knife.run end it "loads all data bags items when -a or --all options is provided" do - @knife.name_args = ["bag_name2"] - @knife.stub(:config).and_return({:all => true}) - @knife.loader.should_receive(:load_from).with("data_bags", "bag_name2", File.basename(@db_file3.path)). - and_return(@plain_data) - dbag = Chef::DataBagItem.new - Chef::DataBagItem.stub(:new).and_return(dbag) - dbag.should_receive(:save) - @knife.run - dbag.data_bag.should == 'bag_name2' - dbag.raw_data.should == @plain_data + knife.name_args = [bag_name2] + config[:all] = true + expect(loader).to receive(:find_all_objects).with("./#{data_bags_path}/#{bag_name2}").and_return([File.basename(db_file3.path)]) + expect(loader).to receive(:load_from).with(data_bags_path, bag_name2, File.basename(db_file3.path)).and_return(plain_data) + expect(Chef::DataBagItem).to receive(:new).and_return(new_bag_expects(bag_name2)) + + knife.run end end describe "encrypted data bag items" do before(:each) do - @secret = "abc123SECRET" - @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data, - @secret) - - # Random IV is used each time the data bag item is encrypted, so values - # will not be equal if we re-encrypt. - Chef::EncryptedDataBagItem.should_receive(:encrypt_data_bag_item).and_return(@enc_data) - - @secret_file = Tempfile.new("encrypted_data_bag_secret_file_test") - @secret_file.puts(@secret) - @secret_file.flush - end - - after do - @secret_file.close - @secret_file.unlink + expect(knife).to receive(:encryption_secret_provided?).and_return(true) + expect(knife).to receive(:read_secret).and_return(secret) + expect(Chef::EncryptedDataBagItem).to receive(:encrypt_data_bag_item).with(plain_data, secret).and_return(enc_data) end it "encrypts values when given --secret" do - @knife.stub(:config).and_return({:secret => @secret}) - - @knife.loader.should_receive(:load_from).with("data_bags", "bag_name", @db_file.path).and_return(@plain_data) - dbag = Chef::DataBagItem.new - Chef::DataBagItem.stub(:new).and_return(dbag) - dbag.should_receive(:save) - @knife.run - dbag.data_bag.should == 'bag_name' - dbag.raw_data.should == @enc_data - end - - it "encrypts values when given --secret_file" do - @knife.stub(:config).and_return({:secret_file => @secret_file.path}) + knife.name_args = [bag_name, db_file.path] + expect(loader).to receive(:load_from).with(data_bags_path, bag_name, db_file.path).and_return(plain_data) + expect(Chef::DataBagItem).to receive(:new).and_return(new_bag_expects(bag_name, enc_data)) - @knife.loader.stub(:load_from).with("data_bags", 'bag_name', @db_file.path).and_return(@plain_data) - dbag = Chef::DataBagItem.new - Chef::DataBagItem.stub(:new).and_return(dbag) - dbag.should_receive(:save) - @knife.run - dbag.data_bag.should == 'bag_name' - dbag.raw_data.should == @enc_data + knife.run end end describe "command line parsing" do it "prints help if given no arguments" do - @knife.instance_variable_set(:@name_args, []) - lambda { @knife.run }.should raise_error(SystemExit) - help_text = "knife data bag from file BAG FILE|FOLDER [FILE|FOLDER..] (options)" - help_text_regex = Regexp.new("^#{Regexp.escape(help_text)}") - @stdout.string.should match(help_text_regex) + knife.name_args = [bag_name] + expect {knife.run}.to exit_with_code(1) + expect(stdout.string).to start_with("knife data bag from file BAG FILE|FOLDER [FILE|FOLDER..] (options)") end end diff --git a/spec/unit/knife/data_bag_secret_options_spec.rb b/spec/unit/knife/data_bag_secret_options_spec.rb new file mode 100644 index 0000000000..0a2d8ca4bf --- /dev/null +++ b/spec/unit/knife/data_bag_secret_options_spec.rb @@ -0,0 +1,165 @@ +# +# Author:: Tyler Ball (<tball@opscode.com>) +# Copyright:: Copyright (c) 2009-2014 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/knife' +require 'chef/config' +require 'tempfile' + +class ExampleDataBagCommand < Chef::Knife + include Chef::Knife::DataBagSecretOptions +end + +describe Chef::Knife::DataBagSecretOptions do + let(:example_db) do + k = ExampleDataBagCommand.new + allow(k.ui).to receive(:stdout).and_return(stdout) + k + end + + let(:stdout) { StringIO.new } + + let(:secret) { "abc123SECRET" } + let(:secret_file) do + sfile = Tempfile.new("encrypted_data_bag_secret") + sfile.puts(secret) + sfile.flush + sfile + end + + after do + secret_file.close + secret_file.unlink + end + + describe "#validate_secrets" do + + it "throws an error when provided with both --secret and --secret-file on the CL" do + Chef::Config[:knife][:cl_secret_file] = secret_file.path + Chef::Config[:knife][:cl_secret] = secret + expect(example_db).to receive(:exit).with(1) + expect(example_db.ui).to receive(:fatal).with("Please specify only one of --secret, --secret-file") + + example_db.validate_secrets + end + + it "throws an error when provided with `secret` and `secret_file` in knife.rb" do + Chef::Config[:knife][:secret_file] = secret_file.path + Chef::Config[:knife][:secret] = secret + expect(example_db).to receive(:exit).with(1) + expect(example_db.ui).to receive(:fatal).with("Please specify only one of 'secret' or 'secret_file' in your config file") + + example_db.validate_secrets + end + + end + + describe "#read_secret" do + + it "returns the secret first" do + Chef::Config[:knife][:cl_secret] = secret + expect(example_db).to receive(:config).and_return({ :secret => secret }) + expect(example_db.read_secret).to eq(secret) + end + + it "returns the secret_file only if secret does not exist" do + Chef::Config[:knife][:cl_secret_file] = secret_file.path + expect(example_db).to receive(:config).and_return({ :secret_file => secret_file.path }) + expect(Chef::EncryptedDataBagItem).to receive(:load_secret).with(secret_file.path).and_return("secret file contents") + expect(example_db.read_secret).to eq("secret file contents") + end + + it "returns the secret from the knife.rb config" do + Chef::Config[:knife][:secret_file] = secret_file.path + Chef::Config[:knife][:secret] = secret + expect(example_db.read_secret).to eq(secret) + end + + it "returns the secret_file from the knife.rb config only if the secret does not exist" do + Chef::Config[:knife][:secret_file] = secret_file.path + expect(Chef::EncryptedDataBagItem).to receive(:load_secret).with(secret_file.path).and_return("secret file contents") + expect(example_db.read_secret).to eq("secret file contents") + end + + end + + describe "#encryption_secret_provided?" do + + it "returns true if the secret is passed on the CL" do + Chef::Config[:knife][:cl_secret] = secret + expect(example_db.encryption_secret_provided?).to eq(true) + end + + it "returns true if the secret_file is passed on the CL" do + Chef::Config[:knife][:cl_secret_file] = secret_file.path + expect(example_db.encryption_secret_provided?).to eq(true) + end + + it "returns true if --encrypt is passed on the CL and :secret is in config" do + expect(example_db).to receive(:config).and_return({ :encrypt => true }) + Chef::Config[:knife][:secret] = secret + expect(example_db.encryption_secret_provided?).to eq(true) + end + + it "returns true if --encrypt is passed on the CL and :secret_file is in config" do + expect(example_db).to receive(:config).and_return({ :encrypt => true }) + Chef::Config[:knife][:secret_file] = secret_file.path + expect(example_db.encryption_secret_provided?).to eq(true) + end + + it "throws an error if --encrypt is passed and there is not :secret or :secret_file in the config" do + expect(example_db).to receive(:config).and_return({ :encrypt => true }) + expect(example_db).to receive(:exit).with(1) + expect(example_db.ui).to receive(:fatal).with("No secret or secret_file specified in config, unable to encrypt item.") + example_db.encryption_secret_provided? + end + + it "returns false if no secret is passed" do + expect(example_db).to receive(:config).and_return({}) + expect(example_db.encryption_secret_provided?).to eq(false) + end + + it "returns false if --encrypt is not provided and :secret is in the config" do + expect(example_db).to receive(:config).and_return({}) + Chef::Config[:knife][:secret] = secret + expect(example_db.encryption_secret_provided?).to eq(false) + end + + it "returns false if --encrypt is not provided and :secret_file is in the config" do + expect(example_db).to receive(:config).and_return({}) + Chef::Config[:knife][:secret_file] = secret_file.path + expect(example_db.encryption_secret_provided?).to eq(false) + end + + it "returns true if --encrypt is not provided, :secret is in the config and need_encrypt_flag is false" do + Chef::Config[:knife][:secret] = secret + expect(example_db.encryption_secret_provided_ignore_encrypt_flag?).to eq(true) + end + + it "returns true if --encrypt is not provided, :secret_file is in the config and need_encrypt_flag is false" do + Chef::Config[:knife][:secret_file] = secret_file.path + expect(example_db.encryption_secret_provided_ignore_encrypt_flag?).to eq(true) + end + + it "returns false if --encrypt is not provided and need_encrypt_flag is false" do + expect(example_db.encryption_secret_provided_ignore_encrypt_flag?).to eq(false) + end + + end + +end diff --git a/spec/unit/knife/data_bag_show_spec.rb b/spec/unit/knife/data_bag_show_spec.rb index 4aa642fc4b..1125d99c2a 100644 --- a/spec/unit/knife/data_bag_show_spec.rb +++ b/spec/unit/knife/data_bag_show_spec.rb @@ -25,97 +25,99 @@ require 'chef/json_compat' require 'tempfile' describe Chef::Knife::DataBagShow do + before do - Chef::Config[:node_name] = "webmonkey.example.com" - @knife = Chef::Knife::DataBagShow.new - @knife.config[:format] = 'json' - @rest = double("Chef::REST") - allow(@knife).to receive(:rest).and_return(@rest) - @stdout = StringIO.new - allow(@knife.ui).to receive(:stdout).and_return(@stdout) + Chef::Config[:node_name] = "webmonkey.example.com" + knife.name_args = [bag_name, item_name] + allow(knife).to receive(:config).and_return(config) end - - it "prints the ids of the data bag items when given a bag name" do - @knife.instance_variable_set(:@name_args, ['bag_o_data']) - data_bag_contents = { "baz"=>"http://localhost:4000/data/bag_o_data/baz", - "qux"=>"http://localhost:4000/data/bag_o_data/qux"} - expect(Chef::DataBag).to receive(:load).and_return(data_bag_contents) - expected = %q|[ - "baz", - "qux" -]| - @knife.run - expect(@stdout.string.strip).to eq(expected) + let(:knife) do + k = Chef::Knife::DataBagShow.new + allow(k).to receive(:rest).and_return(rest) + allow(k.ui).to receive(:stdout).and_return(stdout) + k end - it "prints the contents of the data bag item when given a bag and item name" do - @knife.instance_variable_set(:@name_args, ['bag_o_data', 'an_item']) - data_item = Chef::DataBagItem.new.tap {|item| item.raw_data = {"id" => "an_item", "zsh" => "victory_through_tabbing"}} + let(:rest) { double("Chef::REST") } + let(:stdout) { StringIO.new } - expect(Chef::DataBagItem).to receive(:load).with('bag_o_data', 'an_item').and_return(data_item) - - @knife.run - expect(Chef::JSONCompat.from_json(@stdout.string)).to eq(data_item.raw_data) - end + let(:bag_name) { "sudoing_admins" } + let(:item_name) { "ME" } - it "should pretty print the data bag contents" do - @knife.instance_variable_set(:@name_args, ['bag_o_data', 'an_item']) - data_item = Chef::DataBagItem.new.tap {|item| item.raw_data = {"id" => "an_item", "zsh" => "victory_through_tabbing"}} + let(:data_bag_contents) { { "id" => "id", "baz"=>"http://localhost:4000/data/bag_o_data/baz", + "qux"=>"http://localhost:4000/data/bag_o_data/qux"} } + let(:enc_hash) {Chef::EncryptedDataBagItem.encrypt_data_bag_item(data_bag_contents, secret)} + let(:data_bag) {Chef::DataBagItem.from_hash(data_bag_contents)} + let(:data_bag_with_encoded_hash) { Chef::DataBagItem.from_hash(enc_hash) } + let(:enc_data_bag) { Chef::EncryptedDataBagItem.new(enc_hash, secret) } - expect(Chef::DataBagItem).to receive(:load).with('bag_o_data', 'an_item').and_return(data_item) + let(:secret) { "abc123SECRET" } + # + # let(:raw_hash) {{ "login_name" => "alphaomega", "id" => item_name }} + # + let(:config) { {format: "json"} } - @knife.run - expect(@stdout.string).to eql("{\n \"id\": \"an_item\",\n \"zsh\": \"victory_through_tabbing\"\n}\n") - end + context "Data bag to show is encrypted" do + before do + allow(knife).to receive(:encrypted?).and_return(true) + end - describe "encrypted data bag items" do - before(:each) do - @secret = "abc123SECRET" - @plain_data = { - "id" => "item_name", - "greeting" => "hello", - "nested" => { "a1" => [1, 2, 3], "a2" => { "b1" => true }} - } - @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data, - @secret) - @knife.instance_variable_set(:@name_args, ['bag_name', 'item_name']) - - @secret_file = Tempfile.new("encrypted_data_bag_secret_file_test") - @secret_file.puts(@secret) - @secret_file.flush + it "decrypts and displays the encrypted data bag when the secret is provided" do + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(true) + expect(knife).to receive(:read_secret).and_return(secret) + expect(Chef::DataBagItem).to receive(:load).with(bag_name, item_name).and_return(data_bag_with_encoded_hash) + expect(knife.ui).to receive(:info).with("Encrypted data bag detected, decrypting with provided secret.") + expect(Chef::EncryptedDataBagItem).to receive(:load).with(bag_name, item_name, secret).and_return(enc_data_bag) + + expected = %q|baz: http://localhost:4000/data/bag_o_data/baz +id: id +qux: http://localhost:4000/data/bag_o_data/qux| + knife.run + expect(stdout.string.strip).to eq(expected) end - after do - @secret_file.close - @secret_file.unlink + it "displays the encrypted data bag when the secret is not provided" do + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(false) + expect(Chef::DataBagItem).to receive(:load).with(bag_name, item_name).and_return(data_bag_with_encoded_hash) + expect(knife.ui).to receive(:warn).with("Encrypted data bag detected, but no secret provided for decoding. Displaying encrypted data.") + + knife.run + expect(stdout.string.strip).to include("baz", "qux", "cipher") end + end - it "prints the decrypted contents of an item when given --secret" do - allow(@knife).to receive(:config).and_return({:secret => @secret}) - expect(Chef::EncryptedDataBagItem).to receive(:load). - with('bag_name', 'item_name', @secret). - and_return(Chef::EncryptedDataBagItem.new(@enc_data, @secret)) - @knife.run - expect(Chef::JSONCompat.from_json(@stdout.string)).to eq(@plain_data) + context "Data bag to show is not encrypted" do + before do + allow(knife).to receive(:encrypted?).and_return(false) + expect(knife).to receive(:read_secret).exactly(0).times end - it "prints the decrypted contents of an item when given --secret_file" do - allow(@knife).to receive(:config).and_return({:secret_file => @secret_file.path}) - expect(Chef::EncryptedDataBagItem).to receive(:load). - with('bag_name', 'item_name', @secret). - and_return(Chef::EncryptedDataBagItem.new(@enc_data, @secret)) - @knife.run - expect(Chef::JSONCompat.from_json(@stdout.string)).to eq(@plain_data) + it "displays the data bag" do + expect(Chef::DataBagItem).to receive(:load).with(bag_name, item_name).and_return(data_bag) + expect(knife.ui).to receive(:info).with("Unencrypted data bag detected, ignoring any provided secret options.") + + expected = %q|baz: http://localhost:4000/data/bag_o_data/baz +id: id +qux: http://localhost:4000/data/bag_o_data/qux| + knife.run + expect(stdout.string.strip).to eq(expected) end end - describe "command line parsing" do - it "prints help if given no arguments" do - @knife.instance_variable_set(:@name_args, []) - expect { @knife.run }.to raise_error(SystemExit) - expect(@stdout.string).to match(/^knife data bag show BAG \[ITEM\] \(options\)/) - end + it "displays the list of items in the data bag when only one @name_arg is provided" do + knife.name_args = [bag_name] + expect(Chef::DataBag).to receive(:load).with(bag_name).and_return({}) + + knife.run + expect(stdout.string.strip).to eq("") + end + + it "raises an error when no @name_args are provided" do + knife.name_args = [] + + expect {knife.run}.to exit_with_code(1) + expect(stdout.string).to start_with("knife data bag show BAG [ITEM] (options)") end end diff --git a/spec/unit/knife/environment_from_file_spec.rb b/spec/unit/knife/environment_from_file_spec.rb index f208320c5d..ed3631fcf5 100644 --- a/spec/unit/knife/environment_from_file_spec.rb +++ b/spec/unit/knife/environment_from_file_spec.rb @@ -57,8 +57,8 @@ describe Chef::Knife::EnvironmentFromFile do end it "loads all environments with -a" do - File.stub(:expand_path).with("./environments/*.{json,rb}").and_return("/tmp/environments") - Dir.stub(:glob).with("/tmp/environments").and_return(["spec.rb", "apple.rb"]) + File.stub(:expand_path).with("./environments/").and_return("/tmp/environments") + Dir.stub(:glob).with("/tmp/environments/*.{json,rb}").and_return(["spec.rb", "apple.rb"]) @knife.name_args = [] @knife.stub(:config).and_return({:all => true}) @environment.should_receive(:save).twice diff --git a/spec/unit/knife/ssl_check_spec.rb b/spec/unit/knife/ssl_check_spec.rb index 32405a5977..bb803ce2ca 100644 --- a/spec/unit/knife/ssl_check_spec.rb +++ b/spec/unit/knife/ssl_check_spec.rb @@ -95,6 +95,49 @@ E end end + describe "verifying trusted certificate X509 properties" do + let(:name_args) { %w{https://foo.example.com:8443} } + + let(:trusted_certs_dir) { File.join(CHEF_SPEC_DATA, "trusted_certs") } + let(:trusted_cert_file) { File.join(trusted_certs_dir, "example.crt") } + + let(:store) { OpenSSL::X509::Store.new } + let(:certificate) { OpenSSL::X509::Certificate.new(IO.read(trusted_cert_file)) } + + before do + Chef::Config[:trusted_certs_dir] = trusted_certs_dir + ssl_check.stub(:trusted_certificates).and_return([trusted_cert_file]) + store.stub(:add_cert).with(certificate) + OpenSSL::X509::Store.stub(:new).and_return(store) + OpenSSL::X509::Certificate.stub(:new).with(IO.read(trusted_cert_file)).and_return(certificate) + ssl_check.stub(:verify_cert).and_return(true) + ssl_check.stub(:verify_cert_host).and_return(true) + end + + context "when the trusted certificates have valid X509 properties" do + before do + store.stub(:verify).with(certificate).and_return(true) + end + + it "does not generate any X509 warnings" do + expect(ssl_check.ui).not_to receive(:warn).with(/There are invalid certificates in your trusted_certs_dir/) + ssl_check.run + end + end + + context "when the trusted certificates have invalid X509 properties" do + before do + store.stub(:verify).with(certificate).and_return(false) + store.stub(:error_string).and_return("unable to get local issuer certificate") + end + + it "generates a warning message with invalid certificate file names" do + expect(ssl_check.ui).to receive(:warn).with(/#{trusted_cert_file}: unable to get local issuer certificate/) + ssl_check.run + end + end + end + describe "verifying the remote certificate" do let(:name_args) { %w{https://foo.example.com:8443} } @@ -117,6 +160,7 @@ E context "when the remote host's certificate is valid" do before do + ssl_check.should_receive(:verify_X509).and_return(true) # X509 valid certs (no warn) ssl_socket.should_receive(:connect) # no error ssl_socket.should_receive(:post_connection_check).with("foo.example.com") # no error end @@ -148,6 +192,7 @@ E context "when the certificate's CN does not match the hostname" do before do + ssl_check.should_receive(:verify_X509).and_return(true) # X509 valid certs ssl_socket.should_receive(:connect) # no error ssl_socket.should_receive(:post_connection_check). with("foo.example.com"). @@ -167,6 +212,7 @@ E context "when the cert is not signed by any trusted authority" do before do + ssl_check.should_receive(:verify_X509).and_return(true) # X509 valid certs ssl_socket.should_receive(:connect). and_raise(OpenSSL::SSL::SSLError) ssl_socket_for_debug.should_receive(:connect) @@ -184,4 +230,3 @@ E end end - diff --git a/spec/unit/mixin/homebrew_owner_spec.rb b/spec/unit/mixin/homebrew_owner_spec.rb new file mode 100644 index 0000000000..428cd827d9 --- /dev/null +++ b/spec/unit/mixin/homebrew_owner_spec.rb @@ -0,0 +1,65 @@ +# +# Author:: Joshua Timberman (<joshua@getchef.com>) +# +# Copyright 2014, Chef Software, Inc <legal@getchef.com> +# +# 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/mixin/homebrew_owner' + +class ExampleHomebrewOwner + include Chef::Mixin::HomebrewOwner +end + +describe Chef::Mixin::HomebrewOwner do + before(:each) do + node.default['homebrew']['owner'] = nil + end + + let(:homebrew_owner) { ExampleHomebrewOwner.new } + let(:node) { Chef::Node.new } + + describe 'when the homebrew owner node attribute is set' do + it 'raises an exception if the owner is root' do + node.default['homebrew']['owner'] = 'root' + expect { homebrew_owner.homebrew_owner(node) }.to raise_exception(Chef::Exceptions::CannotDetermineHomebrewOwner) + end + + it 'returns the owner set by attribute' do + node.default['homebrew']['owner'] = 'siouxsie' + expect(homebrew_owner.homebrew_owner(node)).to eql('siouxsie') + end + end + + describe 'when the owner attribute is not set and we use sudo' do + before(:each) do + ENV.stub(:[]).with('SUDO_USER').and_return('john_lydon') + end + + it 'uses the SUDO_USER environment variable' do + expect(homebrew_owner.homebrew_owner(node)).to eql('john_lydon') + end + end + + describe 'when the owner attribute is not set and we arent using sudo' do + before(:each) do + ENV.stub(:[]).with('USER').and_return('sid_vicious') + ENV.stub(:[]).with('SUDO_USER').and_return(nil) + end + + it 'uses the current user' do + expect(homebrew_owner.homebrew_owner(node)).to eql('sid_vicious') + end + end +end diff --git a/spec/unit/platform/query_helpers_spec.rb b/spec/unit/platform/query_helpers_spec.rb index 2414bdf552..6adea5eecf 100644 --- a/spec/unit/platform/query_helpers_spec.rb +++ b/spec/unit/platform/query_helpers_spec.rb @@ -30,3 +30,26 @@ describe "Chef::Platform#windows_server_2003?" do expect { Thread.fork { Chef::Platform.windows_server_2003? }.join }.not_to raise_error end end + +describe 'Chef::Platform#supports_dsc?' do + it 'returns false if powershell is not present' do + node = Chef::Node.new + Chef::Platform.supports_dsc?(node).should be_false + end + + ['1.0', '2.0', '3.0'].each do |version| + it "returns false for Powershell #{version}" do + node = Chef::Node.new + node.automatic[:languages][:powershell][:version] = version + Chef::Platform.supports_dsc?(node).should be_false + end + end + + ['4.0', '5.0'].each do |version| + it "returns true for Powershell #{version}" do + node = Chef::Node.new + node.automatic[:languages][:powershell][:version] = version + Chef::Platform.supports_dsc?(node).should be_true + end + end +end diff --git a/spec/unit/provider/dsc_script_spec.rb b/spec/unit/provider/dsc_script_spec.rb new file mode 100644 index 0000000000..8a7a7b5c6a --- /dev/null +++ b/spec/unit/provider/dsc_script_spec.rb @@ -0,0 +1,145 @@ +# +# Author:: Jay Mundrawala (<jdm@getchef.com>) +# +# 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' +require 'chef/util/dsc/resource_info' +require 'spec_helper' + +describe Chef::Provider::DscScript do + let (:node) { + node = Chef::Node.new + node.automatic[:languages][:powershell][:version] = '4.0' + node + } + let (:events) { Chef::EventDispatch::Dispatcher.new } + let (:run_context) { Chef::RunContext.new(node, {}, events) } + let (:resource) { Chef::Resource::DscScript.new("script", run_context) } + let (:provider) do + Chef::Provider::DscScript.new(resource, run_context) + end + + describe '#load_current_resource' do + it "describes the resource as converged if there were 0 DSC resources" do + allow(provider).to receive(:run_configuration).with(:test).and_return([]) + provider.load_current_resource + provider.instance_variable_get('@resource_converged').should be_true + end + + it "describes the resource as not converged if there is 1 DSC resources that is converged" do + dsc_resource_info = Chef::Util::DSC::ResourceInfo.new('resource', false, ['nothing will change something']) + allow(provider).to receive(:run_configuration).with(:test).and_return([dsc_resource_info]) + provider.load_current_resource + provider.instance_variable_get('@resource_converged').should be_true + end + + it "describes the resource as not converged if there is 1 DSC resources that is not converged" do + dsc_resource_info = Chef::Util::DSC::ResourceInfo.new('resource', true, ['will change something']) + allow(provider).to receive(:run_configuration).with(:test).and_return([dsc_resource_info]) + provider.load_current_resource + provider.instance_variable_get('@resource_converged').should be_false + end + + it "describes the resource as not converged if there are any DSC resources that are not converged" do + dsc_resource_info1 = Chef::Util::DSC::ResourceInfo.new('resource', true, ['will change something']) + dsc_resource_info2 = Chef::Util::DSC::ResourceInfo.new('resource', false, ['nothing will change something']) + + allow(provider).to receive(:run_configuration).with(:test).and_return([dsc_resource_info1, dsc_resource_info2]) + provider.load_current_resource + provider.instance_variable_get('@resource_converged').should be_false + end + + it "describes the resource as converged if all DSC resources that are converged" do + dsc_resource_info1 = Chef::Util::DSC::ResourceInfo.new('resource', false, ['nothing will change something']) + dsc_resource_info2 = Chef::Util::DSC::ResourceInfo.new('resource', false, ['nothing will change something']) + + allow(provider).to receive(:run_configuration).with(:test).and_return([dsc_resource_info1, dsc_resource_info2]) + provider.load_current_resource + provider.instance_variable_get('@resource_converged').should be_true + end + end + + describe '#generate_configuration_document' do + # I think integration tests should cover these cases + + it 'uses configuration_document_from_script_path when a dsc script file is given' do + allow(provider).to receive(:load_current_resource) + resource.command("path_to_script") + generator = double('Chef::Util::DSC::ConfigurationGenerator') + generator.should_receive(:configuration_document_from_script_path) + allow(Chef::Util::DSC::ConfigurationGenerator).to receive(:new).and_return(generator) + provider.send(:generate_configuration_document, 'tmp', nil) + end + + it 'uses configuration_document_from_script_code when a the dsc resource is given' do + allow(provider).to receive(:load_current_resource) + resource.code("ImADSCResource{}") + generator = double('Chef::Util::DSC::ConfigurationGenerator') + generator.should_receive(:configuration_document_from_script_code) + allow(Chef::Util::DSC::ConfigurationGenerator).to receive(:new).and_return(generator) + provider.send(:generate_configuration_document, 'tmp', nil) + end + + it 'should noop if neither code or command are provided' do + allow(provider).to receive(:load_current_resource) + generator = double('Chef::Util::DSC::ConfigurationGenerator') + generator.should_receive(:configuration_document_from_script_code).with('', anything(), anything()) + allow(Chef::Util::DSC::ConfigurationGenerator).to receive(:new).and_return(generator) + provider.send(:generate_configuration_document, 'tmp', nil) + end + end + + describe 'action_run' do + it 'should converge the script if it is not converged' do + dsc_resource_info = Chef::Util::DSC::ResourceInfo.new('resource', true, ['will change something']) + allow(provider).to receive(:run_configuration).with(:test).and_return([dsc_resource_info]) + allow(provider).to receive(:run_configuration).with(:set) + + provider.run_action(:run) + resource.should be_updated + end + + it 'should not converge if the script is already converged' do + allow(provider).to receive(:run_configuration).with(:test).and_return([]) + + provider.run_action(:run) + resource.should_not be_updated + end + end + + describe '#generate_description' do + it 'removes the resource name from the beginning of any log line from the LCM' do + dsc_resource_info = Chef::Util::DSC::ResourceInfo.new('resourcename', true, ['resourcename doing something', 'lastline']) + provider.instance_variable_set('@dsc_resources_info', [dsc_resource_info]) + provider.send(:generate_description)[1].should match(/converge DSC resource resourcename by doing something/) + end + + it 'ignores the last line' do + dsc_resource_info = Chef::Util::DSC::ResourceInfo.new('resourcename', true, ['resourcename doing something', 'lastline']) + provider.instance_variable_set('@dsc_resources_info', [dsc_resource_info]) + provider.send(:generate_description)[1].should_not match(/lastline/) + end + + it 'reports a dsc resource has not been changed if the LCM reported no change was required' do + dsc_resource_info = Chef::Util::DSC::ResourceInfo.new('resourcename', false, ['resourcename does nothing', 'lastline']) + provider.instance_variable_set('@dsc_resources_info', [dsc_resource_info]) + provider.send(:generate_description)[1].should match(/converge DSC resource resourcename by doing nothing/) + end + end +end + diff --git a/spec/unit/provider/git_spec.rb b/spec/unit/provider/git_spec.rb index 143c22da6e..69a6443c67 100644 --- a/spec/unit/provider/git_spec.rb +++ b/spec/unit/provider/git_spec.rb @@ -244,8 +244,8 @@ SHAS @provider.clone end - it "runs a checkout command with default options and uses -B to reset branches if necessary" do - expected_cmd = 'git checkout -B deploy d35af14d41ae22b19da05d7d03a0bafc321b244c' + it "runs a checkout command with default options" do + expected_cmd = 'git branch -f deploy d35af14d41ae22b19da05d7d03a0bafc321b244c; git checkout deploy' @provider.should_receive(:shell_out!).with(expected_cmd, :cwd => "/my/deploy/dir", :log_tag => "git[web2.0 app]") @provider.checkout diff --git a/spec/unit/provider/ifconfig/debian_spec.rb b/spec/unit/provider/ifconfig/debian_spec.rb index c6a37fdd5b..ebb16e22af 100644 --- a/spec/unit/provider/ifconfig/debian_spec.rb +++ b/spec/unit/provider/ifconfig/debian_spec.rb @@ -56,8 +56,6 @@ describe Chef::Provider::Ifconfig::Debian do describe "generate_config" do context "when writing a file" do - let(:config_file_ifcfg) { StringIO.new } - let(:tempfile) { Tempfile.new("rspec-chef-ifconfig-debian") } let(:tempdir_path) { Dir.mktmpdir("rspec-chef-ifconfig-debian-dir") } @@ -67,14 +65,13 @@ describe Chef::Provider::Ifconfig::Debian do before do stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_FILE", tempfile.path) stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_DOT_D_DIR", tempdir_path) - expect(File).to receive(:new).with(config_filename_ifcfg, "w").and_return(config_file_ifcfg) end it "should write a network-script" do provider.run_action(:add) - expect(config_file_ifcfg.string).to match(/^iface eth0 inet static\s*$/) - expect(config_file_ifcfg.string).to match(/^\s+address 10\.0\.0\.1\s*$/) - expect(config_file_ifcfg.string).to match(/^\s+netmask 255\.255\.254\.0\s*$/) + expect(File.read(config_filename_ifcfg)).to match(/^iface eth0 inet static\s*$/) + expect(File.read(config_filename_ifcfg)).to match(/^\s+address 10\.0\.0\.1\s*$/) + expect(File.read(config_filename_ifcfg)).to match(/^\s+netmask 255\.255\.254\.0\s*$/) end context "when the interface_dot_d directory does not exist" do @@ -123,7 +120,6 @@ iface eth0 inet static netmask 255.255.254.0 EOF ) - expect(File).to receive(:new).with(config_filename_ifcfg, "w").and_return(config_file_ifcfg) expect(File.exists?(tempdir_path)).to be_true # since the file exists, the enclosing dir must also exist end @@ -139,6 +135,8 @@ EOF before do tempfile.write(expected_string) tempfile.close + + expect(provider).not_to receive(:converge_by).with(/modifying #{tempfile.path} to source #{tempdir_path}/) end it "should preserve all the contents" do @@ -165,6 +163,9 @@ EOF before do tempfile.write("a line\nanother line\n") tempfile.close + + allow(provider).to receive(:converge_by).and_call_original + expect(provider).to receive(:converge_by).with(/modifying #{tempfile.path} to source #{tempdir_path}/).and_call_original end it "should preserve the original contents and add the source line" do @@ -316,12 +317,37 @@ source #{tempdir_path}/* describe "delete_config for action_delete" do + let(:tempfile) { Tempfile.new("rspec-chef-ifconfig-debian") } + + let(:tempdir_path) { Dir.mktmpdir("rspec-chef-ifconfig-debian-dir") } + + let(:config_filename_ifcfg) { "#{tempdir_path}/ifcfg-#{new_resource.device}" } + + before do + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_FILE", tempfile.path) + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_DOT_D_DIR", tempdir_path) + File.open(config_filename_ifcfg, "w") do |fh| + fh.write "arbitrary text\n" + fh.close + end + end + + after do + Dir.rmdir(tempdir_path) + end + it "should delete network-script if it exists" do current_resource.device new_resource.device - expect(File).to receive(:exist?).with(config_filename_ifcfg).and_return(true) - expect(FileUtils).to receive(:rm_f).with(config_filename_ifcfg, :verbose => false) + # belt and suspenders testing? + Chef::Util::Backup.any_instance.should_receive(:do_backup).and_call_original + + # internal implementation detail of Ifconfig. + Chef::Provider::File.any_instance.should_receive(:action_delete).and_call_original + + expect(File.exist?(config_filename_ifcfg)).to be_true provider.run_action(:delete) + expect(File.exist?(config_filename_ifcfg)).to be_false end end diff --git a/spec/unit/provider/ifconfig/redhat_spec.rb b/spec/unit/provider/ifconfig/redhat_spec.rb index f4b98dfc32..138c2a389d 100644 --- a/spec/unit/provider/ifconfig/redhat_spec.rb +++ b/spec/unit/provider/ifconfig/redhat_spec.rb @@ -37,22 +37,26 @@ describe Chef::Provider::Ifconfig::Redhat do status = double("Status", :exitstatus => 0) @provider.instance_variable_set("@status", status) @provider.current_resource = @current_resource - end + + config_filename = "/etc/sysconfig/network-scripts/ifcfg-#{@new_resource.device}" + @config = double("chef-resource-file") + @provider.should_receive(:resource_for_config).with(config_filename).and_return(@config) + end describe "generate_config for action_add" do - it "should write network-script for centos" do + it "should write network-script for centos" do @provider.stub(:load_current_resource) @provider.stub(:run_command) - config_filename = "/etc/sysconfig/network-scripts/ifcfg-#{@new_resource.device}" - config_file = StringIO.new - File.should_receive(:new).with(config_filename, "w").and_return(config_file) - + @config.should_receive(:content) do |arg| + arg.should match(/^\s*DEVICE=eth0\s*$/) + arg.should match(/^\s*IPADDR=10\.0\.0\.1\s*$/) + arg.should match(/^\s*NETMASK=255\.255\.254\.0\s*$/) + end + @config.should_receive(:run_action).with(:create) + @config.should_receive(:updated?).and_return(true) @provider.run_action(:add) - config_file.string.should match(/^\s*DEVICE=eth0\s*$/) - config_file.string.should match(/^\s*IPADDR=10\.0\.0\.1\s*$/) - config_file.string.should match(/^\s*NETMASK=255\.255\.254\.0\s*$/) - end + end end describe "delete_config for action_delete" do @@ -61,10 +65,8 @@ describe Chef::Provider::Ifconfig::Redhat do @current_resource.device @new_resource.device @provider.stub(:load_current_resource) @provider.stub(:run_command) - config_filename = "/etc/sysconfig/network-scripts/ifcfg-#{@new_resource.device}" - File.should_receive(:exist?).with(config_filename).and_return(true) - FileUtils.should_receive(:rm_f).with(config_filename, :verbose => false) - + @config.should_receive(:run_action).with(:delete) + @config.should_receive(:updated?).and_return(true) @provider.run_action(:delete) end end diff --git a/spec/unit/provider/ifconfig_spec.rb b/spec/unit/provider/ifconfig_spec.rb index fb8d0956d7..b09e365c65 100644 --- a/spec/unit/provider/ifconfig_spec.rb +++ b/spec/unit/provider/ifconfig_spec.rb @@ -72,7 +72,7 @@ describe Chef::Provider::Ifconfig do @provider.stub(:load_current_resource) @provider.should_not_receive(:run_command) @current_resource.inet_addr "10.0.0.1" - @provider.should_not_receive(:generate_config) + @provider.should_receive(:generate_config) @provider.run_action(:add) @new_resource.should_not be_updated @@ -123,7 +123,7 @@ describe Chef::Provider::Ifconfig do it "should not delete interface if it does not exist" do @provider.stub(:load_current_resource) @provider.should_not_receive(:run_command) - @provider.should_not_receive(:delete_config) + @provider.should_receive(:delete_config) @provider.run_action(:delete) @new_resource.should_not be_updated @@ -171,7 +171,7 @@ describe Chef::Provider::Ifconfig do @provider.stub(:load_current_resource) # This is so that nothing actually runs @provider.should_not_receive(:run_command) - @provider.should_not_receive(:delete_config) + @provider.should_receive(:delete_config) @provider.run_action(:delete) @new_resource.should_not be_updated diff --git a/spec/unit/provider/link_spec.rb b/spec/unit/provider/link_spec.rb index 6052f5dd3b..2f0a5f2020 100644 --- a/spec/unit/provider/link_spec.rb +++ b/spec/unit/provider/link_spec.rb @@ -38,8 +38,8 @@ describe Chef::Resource::Link, :not_supported_on_win2k3 do result end - def canonicalize(path) - Chef::Platform.windows? ? path.gsub('/', '\\') : path + def paths_eql?(path1, path2) + Chef::Util::PathHelper.paths_eql?(path1, path2) end describe "when the target is a symlink" do @@ -68,7 +68,7 @@ describe Chef::Resource::Link, :not_supported_on_win2k3 do provider.current_resource.link_type.should == :symbolic end it "should update the source of the existing link with the links target" do - provider.current_resource.to.should == canonicalize("#{CHEF_SPEC_DATA}/fofile") + paths_eql?(provider.current_resource.to, "#{CHEF_SPEC_DATA}/fofile").should be_true end it "should set the owner" do provider.current_resource.owner.should == 501 @@ -110,7 +110,7 @@ describe Chef::Resource::Link, :not_supported_on_win2k3 do provider.current_resource.link_type.should == :symbolic end it "should update the source of the existing link to the link's target" do - provider.current_resource.to.should == canonicalize("#{CHEF_SPEC_DATA}/fofile") + paths_eql?(provider.current_resource.to, "#{CHEF_SPEC_DATA}/fofile").should be_true end it "should not set the owner" do provider.current_resource.owner.should be_nil @@ -221,7 +221,7 @@ describe Chef::Resource::Link, :not_supported_on_win2k3 do provider.current_resource.link_type.should == :hard end it "should update the source of the existing link to the link's target" do - provider.current_resource.to.should == canonicalize("#{CHEF_SPEC_DATA}/fofile") + paths_eql?(provider.current_resource.to, "#{CHEF_SPEC_DATA}/fofile").should be_true end it "should not set the owner" do provider.current_resource.owner.should == nil diff --git a/spec/unit/provider/package/homebrew_spec.rb b/spec/unit/provider/package/homebrew_spec.rb new file mode 100644 index 0000000000..9f105c13b8 --- /dev/null +++ b/spec/unit/provider/package/homebrew_spec.rb @@ -0,0 +1,251 @@ +# +# Author:: Joshua Timberman (<joshua@getchef.com>) +# Copyright (c) 2014, Chef Software, Inc. <legal@getchef.com> +# +# 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' + +describe Chef::Provider::Package::Homebrew do + let(:node) { Chef::Node.new } + let(:events) { double('Chef::Events').as_null_object } + let(:run_context) { double('Chef::RunContext', node: node, events: events) } + let(:new_resource) { Chef::Resource::HomebrewPackage.new('emacs') } + let(:current_resource) { Chef::Resource::HomebrewPackage.new('emacs')} + + let(:provider) do + Chef::Provider::Package::Homebrew.new(new_resource, run_context) + end + + let(:uninstalled_brew_info) do + { + 'name' => 'emacs', + 'homepage' => 'http://www.gnu.org/software/emacs', + 'versions' => { + 'stable' => '24.3', + 'bottle' => false, + 'devel' => nil, + 'head' => nil + }, + 'revision' => 0, + 'installed' => [], + 'linked_keg' => nil, + 'keg_only' => nil, + 'dependencies' => [], + 'conflicts_with' => [], + 'caveats' => nil, + 'options' => [] + } + end + + let(:installed_brew_info) do + { + 'name' => 'emacs', + 'homepage' => 'http://www.gnu.org/software/emacs/', + 'versions' => { + 'stable' => '24.3', + 'bottle' => false, + 'devel' => nil, + 'head' => 'HEAD' + }, + 'revision' => 0, + 'installed' => [{ 'version' => '24.3' }], + 'linked_keg' => '24.3', + 'keg_only' => nil, + 'dependencies' => [], + 'conflicts_with' => [], + 'caveats' => '', + 'options' => [] + } + end + + let(:keg_only_brew_info) do + { + 'name' => 'emacs-kegger', + 'homepage' => 'http://www.gnu.org/software/emacs/', + 'versions' => { + 'stable' => '24.3-keggy', + 'bottle' => false, + 'devel' => nil, + 'head' => 'HEAD' + }, + 'revision' => 0, + 'installed' => [{ 'version' => '24.3-keggy' }], + 'linked_keg' => nil, + 'keg_only' => true, + 'dependencies' => [], + 'conflicts_with' => [], + 'caveats' => '', + 'options' => [] + } + end + + before(:each) do + node.default['homebrew']['owner'] = 'sid_vicious' + allow(Etc).to receive(:getpwnam).with('sid_vicious').and_return('/Users/sid_vicious') + end + + describe 'load_current_resource' do + before(:each) do + allow(provider).to receive(:current_installed_version).and_return(nil) + allow(provider).to receive(:candidate_version).and_return('24.3') + end + + it 'creates a current resource with the name of the new resource' do + provider.load_current_resource + expect(provider.current_resource).to be_a(Chef::Resource::Package) + expect(provider.current_resource.name).to eql('emacs') + end + + it 'creates a current resource with the version if the package is installed' do + expect(provider).to receive(:current_installed_version).and_return('24.3') + provider.load_current_resource + expect(provider.current_resource.version).to eql('24.3') + end + + it 'creates a current resource with a nil version if the package is not installed' do + provider.load_current_resource + expect(provider.current_resource.version).to be_nil + end + + it 'sets a candidate version if one exists' do + provider.load_current_resource + expect(provider.candidate_version).to eql('24.3') + end + end + + describe 'current_installed_version' do + it 'returns the latest version from brew info if the package is keg only' do + allow(provider).to receive(:brew_info).and_return(keg_only_brew_info) + expect(provider.current_installed_version).to eql('24.3-keggy') + end + + it 'returns the linked keg version if the package is not keg only' do + allow(provider).to receive(:brew_info).and_return(installed_brew_info) + expect(provider.current_installed_version).to eql('24.3') + end + + it 'returns nil if the package is not installed' do + allow(provider).to receive(:brew_info).and_return(uninstalled_brew_info) + expect(provider.current_installed_version).to be_nil + end + end + + describe 'brew' do + it 'passes a single to the brew command and return stdout' do + allow(provider).to receive(:get_response_from_command).and_return('zombo') + expect(provider.brew).to eql('zombo') + end + + it 'takes multiple arguments as an array' do + allow(provider).to receive(:get_response_from_command).and_return('homestarrunner') + expect(provider.brew('info', 'opts', 'bananas')).to eql('homestarrunner') + end + end + + context 'when testing actions' do + before(:each) do + provider.current_resource = current_resource + end + + describe 'install_package' do + before(:each) do + allow(provider).to receive(:candidate_version).and_return('24.3') + end + + it 'installs the named package with brew install' do + allow(provider.new_resource).to receive(:version).and_return('24.3') + allow(provider.current_resource).to receive(:version).and_return(nil) + allow(provider).to receive(:brew_info).and_return(uninstalled_brew_info) + expect(provider).to receive(:get_response_from_command).with('brew install emacs') + provider.install_package('emacs', '24.3') + end + + it 'does not do anything if the package is installed' do + allow(provider.current_resource).to receive(:version).and_return('24.3') + allow(provider).to receive(:brew_info).and_return(installed_brew_info) + expect(provider).not_to receive(:get_response_from_command) + provider.install_package('emacs', '24.3') + end + + it 'uses options to the brew command if specified' do + allow(provider.new_resource).to receive(:options).and_return('--cocoa') + allow(provider.current_resource).to receive(:version).and_return('24.3') + allow(provider).to receive(:get_response_from_command).with('brew install --cocoa emacs') + provider.install_package('emacs', '24.3') + end + end + + describe 'upgrade_package' do + it 'uses brew upgrade to upgrade the package if it is installed' do + allow(provider.current_resource).to receive(:version).and_return('24') + allow(provider).to receive(:brew_info).and_return(installed_brew_info) + expect(provider).to receive(:get_response_from_command).with('brew upgrade emacs') + provider.upgrade_package('emacs', '24.3') + end + + it 'does not do anything if the package version is already installed' do + allow(provider.current_resource).to receive(:version).and_return('24.3') + allow(provider).to receive(:brew_info).and_return(installed_brew_info) + expect(provider).not_to receive(:get_response_from_command) + provider.install_package('emacs', '24.3') + end + + it 'uses brew install to install the package if it is not installed' do + allow(provider.current_resource).to receive(:version).and_return(nil) + allow(provider).to receive(:brew_info).and_return(uninstalled_brew_info) + expect(provider).to receive(:get_response_from_command).with('brew install emacs') + provider.upgrade_package('emacs', '24.3') + end + + it 'uses options to the brew command if specified' do + allow(provider.current_resource).to receive(:version).and_return('24') + allow(provider).to receive(:brew_info).and_return(installed_brew_info) + allow(provider.new_resource).to receive(:options).and_return('--cocoa') + expect(provider).to receive(:get_response_from_command).with('brew upgrade --cocoa emacs') + provider.upgrade_package('emacs', '24.3') + end + end + + describe 'remove_package' do + it 'uninstalls the package with brew uninstall' do + allow(provider.current_resource).to receive(:version).and_return('24.3') + allow(provider).to receive(:brew_info).and_return(installed_brew_info) + expect(provider).to receive(:get_response_from_command).with('brew uninstall emacs') + provider.remove_package('emacs', '24.3') + end + + it 'does not do anything if the package is not installed' do + allow(provider).to receive(:brew_info).and_return(uninstalled_brew_info) + expect(provider).not_to receive(:get_response_from_command) + provider.remove_package('emacs', '24.3') + end + end + + describe 'purge_package' do + it 'uninstalls the package with brew uninstall --force' do + allow(provider.current_resource).to receive(:version).and_return('24.3') + allow(provider).to receive(:brew_info).and_return(installed_brew_info) + expect(provider).to receive(:get_response_from_command).with('brew uninstall --force emacs') + provider.purge_package('emacs', '24.3') + end + + it 'does not do anything if the package is not installed' do + allow(provider).to receive(:brew_info).and_return(uninstalled_brew_info) + expect(provider).not_to receive(:get_response_from_command) + provider.purge_package('emacs', '24.3') + end + end + end +end diff --git a/spec/unit/provider/whyrun_safe_ruby_block_spec.rb b/spec/unit/provider/whyrun_safe_ruby_block_spec.rb index 5a17aacbd9..d5209248b3 100644 --- a/spec/unit/provider/whyrun_safe_ruby_block_spec.rb +++ b/spec/unit/provider/whyrun_safe_ruby_block_spec.rb @@ -30,14 +30,14 @@ describe Chef::Provider::WhyrunSafeRubyBlock, "initialize" do end it "should call the block and flag the resource as updated" do - @provider.run_action(:create) + @provider.run_action(:run) $evil_global_evil_laugh.should == :mwahahaha @new_resource.should be_updated end it "should call the block and flat the resource as updated - even in whyrun" do Chef::Config[:why_run] = true - @provider.run_action(:create) + @provider.run_action(:run) $evil_global_evil_laugh.should == :mwahahaha @new_resource.should be_updated Chef::Config[:why_run] = false diff --git a/spec/unit/resource/dsc_script_spec.rb b/spec/unit/resource/dsc_script_spec.rb new file mode 100644 index 0000000000..cbd502a61c --- /dev/null +++ b/spec/unit/resource/dsc_script_spec.rb @@ -0,0 +1,127 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# 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 'spec_helper' + +describe Chef::Resource::DscScript do + let(:dsc_test_resource_name) { 'DSCTest' } + + context 'when Powershell supports Dsc' do + let(:dsc_test_run_context) { + node = Chef::Node.new + node.automatic[:languages][:powershell][:version] = '4.0' + empty_events = Chef::EventDispatch::Dispatcher.new + Chef::RunContext.new(node, {}, empty_events) + } + let(:dsc_test_resource) { + Chef::Resource::DscScript.new(dsc_test_resource_name, dsc_test_run_context) + } + let(:configuration_code) {'echo "This is supposed to create a configuration document."'} + let(:configuration_path) {'c:/myconfigs/formatc.ps1'} + let(:configuration_name) { 'formatme' } + let(:configuration_data) { '@{AllNodes = @( @{ NodeName = "localhost"; PSDscAllowPlainTextPassword = $true })}' } + let(:configuration_data_script) { 'c:/myconfigs/data/safedata.psd1' } + + it "has a default action of `:run`" do + expect(dsc_test_resource.action).to eq(:run) + end + + it "has an allowed_actions attribute with only the `:run` and `:nothing` attributes" do + expect(dsc_test_resource.allowed_actions.to_set).to eq([:run,:nothing].to_set) + end + + it "allows the code attribute to be set" do + dsc_test_resource.code(configuration_code) + expect(dsc_test_resource.code).to eq(configuration_code) + end + + it "allows the command attribute to be set" do + dsc_test_resource.command(configuration_path) + expect(dsc_test_resource.command).to eq(configuration_path) + end + + it "allows the configuration_name attribute to be set" do + dsc_test_resource.configuration_name(configuration_name) + expect(dsc_test_resource.configuration_name).to eq(configuration_name) + end + + it "allows the configuration_data attribute to be set" do + dsc_test_resource.configuration_data(configuration_data) + expect(dsc_test_resource.configuration_data).to eq(configuration_data) + end + + it "allows the configuration_data_script attribute to be set" do + dsc_test_resource.configuration_data_script(configuration_data_script) + expect(dsc_test_resource.configuration_data_script).to eq(configuration_data_script) + end + + it "raises an ArgumentError exception if an attempt is made to set the code attribute when the command attribute is already set" do + dsc_test_resource.command(configuration_path) + expect { dsc_test_resource.code(configuration_code) }.to raise_error(ArgumentError) + end + + it "raises an ArgumentError exception if an attempt is made to set the command attribute when the code attribute is already set" do + dsc_test_resource.code(configuration_code) + expect { dsc_test_resource.command(configuration_path) }.to raise_error(ArgumentError) + end + + it "raises an ArgumentError exception if an attempt is made to set the configuration_name attribute when the code attribute is already set" do + dsc_test_resource.code(configuration_code) + expect { dsc_test_resource.configuration_name(configuration_name) }.to raise_error(ArgumentError) + end + + it "raises an ArgumentError exception if an attempt is made to set the configuration_data attribute when the configuration_data_script attribute is already set" do + dsc_test_resource.configuration_data_script(configuration_data_script) + expect { dsc_test_resource.configuration_data(configuration_data) }.to raise_error(ArgumentError) + end + + it "raises an ArgumentError exception if an attempt is made to set the configuration_data_script attribute when the configuration_data attribute is already set" do + dsc_test_resource.configuration_data(configuration_data) + expect { dsc_test_resource.configuration_data_script(configuration_data_script) }.to raise_error(ArgumentError) + end + end + + context 'when Powershell does not supported Dsc' do + ['1.0', '2.0', '3.0'].each do |version| + it "raises an exception for powershell version '#{version}'" do + node = Chef::Node.new + node.automatic[:languages][:powershell][:version] = version + empty_events = Chef::EventDispatch::Dispatcher.new + dsc_test_run_context = Chef::RunContext.new(node, {}, empty_events) + + expect { + Chef::Resource::DscScript.new(dsc_test_resource_name, dsc_test_run_context) + }.to raise_error(Chef::Exceptions::NoProviderAvailable) + end + end + end + + context 'when Powershell is not present' do + let (:dsc_test_run_context) { + node = Chef::Node.new + empty_events = Chef::EventDispatch::Dispatcher.new + dsc_test_run_context = Chef::RunContext.new(node, {}, empty_events) + } + + it 'raises an exception if powershell is not present' do + expect { + Chef::Resource::DscScript.new(dsc_test_resource_name, dsc_test_run_context) + }.to raise_error(Chef::Exceptions::NoProviderAvailable) + end + end +end diff --git a/spec/unit/resource/homebrew_package_spec.rb b/spec/unit/resource/homebrew_package_spec.rb new file mode 100644 index 0000000000..4b4f9afe5e --- /dev/null +++ b/spec/unit/resource/homebrew_package_spec.rb @@ -0,0 +1,36 @@ +# +# Author:: Joshua Timberman (<joshua@getchef.com>) +# Copyright (c) 2014, Chef Software, Inc. <legal@getchef.com> +# +# 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' + +describe Chef::Resource::HomebrewPackage, 'initialize' do + + let(:resource) { Chef::Resource::HomebrewPackage.new('emacs') } + + it 'returns a Chef::Resource::HomebrewPackage' do + expect(resource).to be_a_kind_of(Chef::Resource::HomebrewPackage) + end + + it 'sets the resource_name to :homebrew_package' do + expect(resource.resource_name).to eql(:homebrew_package) + end + + it 'sets the provider to Chef::Provider::Package::Homebrew' do + expect(resource.provider).to eql(Chef::Provider::Package::Homebrew) + end + +end diff --git a/spec/unit/run_context_spec.rb b/spec/unit/run_context_spec.rb index 1def10faf5..21ece2abaa 100644 --- a/spec/unit/run_context_spec.rb +++ b/spec/unit/run_context_spec.rb @@ -134,4 +134,19 @@ describe Chef::RunContext do end end + describe "handling reboot requests" do + let(:expected) do + { :reason => "spec tests require a reboot" } + end + + it "stores and deletes the reboot request" do + @run_context.request_reboot(expected) + expect(@run_context.reboot_info).to eq(expected) + expect(@run_context.reboot_requested?).to be_true + + @run_context.cancel_reboot + expect(@run_context.reboot_info).to eq({}) + expect(@run_context.reboot_requested?).to be_false + end + end end diff --git a/spec/unit/util/dsc/configuration_generator_spec.rb b/spec/unit/util/dsc/configuration_generator_spec.rb new file mode 100644 index 0000000000..03f3ffe25c --- /dev/null +++ b/spec/unit/util/dsc/configuration_generator_spec.rb @@ -0,0 +1,171 @@ +# +# Author:: Jay Mundrawala <jmundrawala@getchef.com> +# 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' +require 'chef/util/dsc/configuration_generator' + +describe Chef::Util::DSC::ConfigurationGenerator do + let(:conf_man) do + node = Chef::Node.new + Chef::Util::DSC::ConfigurationGenerator.new(node, 'tmp') + end + + describe '#validate_configuration_name!' do + it 'should not raise an error if a name contains all upper case letters' do + conf_man.send(:validate_configuration_name!, "HELLO") + end + + it 'should not raise an error if the name contains all lower case letters' do + conf_man.send(:validate_configuration_name!, "hello") + end + + it 'should not raise an error if no special characters are used except _' do + conf_man.send(:validate_configuration_name!, "hello_world") + end + + %w{! @ # $ % ^ & * & * ( ) - = + \{ \} . ? < > \\ /}.each do |sym| + it "raises an Argument error if it configuration name contains #{sym}" do + expect { + conf_man.send(:validate_configuration_name!, "Hello#{sym}") + }.to raise_error(ArgumentError) + end + end + end + + describe "#get_merged_configuration_flags" do + context 'when strings are used as switches' do + it 'should merge the hash if there are no restricted switches' do + merged = conf_man.send(:get_merged_configuration_flags!, {'flag' => 'a'}, 'hello') + merged.should include(:flag) + merged[:flag].should eql('a') + merged.should include(:outputpath) + end + + it 'should raise an ArgumentError if you try to override outputpath' do + expect { + conf_man.send(:get_merged_configuration_flags!, {'outputpath' => 'a'}, 'hello') + }.to raise_error(ArgumentError) + end + + it 'should be case insensitive for switches that are not allowed' do + expect { + conf_man.send(:get_merged_configuration_flags!, {'OutputPath' => 'a'}, 'hello') + }.to raise_error(ArgumentError) + end + + it 'should be case insensitive to switches that are allowed' do + merged = conf_man.send(:get_merged_configuration_flags!, {'FLAG' => 'a'}, 'hello') + merged.should include(:flag) + end + end + + context 'when symbols are used as switches' do + it 'should merge the hash if there are no restricted switches' do + merged = conf_man.send(:get_merged_configuration_flags!, {:flag => 'a'}, 'hello') + merged.should include(:flag) + merged[:flag].should eql('a') + merged.should include(:outputpath) + end + + it 'should raise an ArgumentError if you try to override outputpath' do + expect { + conf_man.send(:get_merged_configuration_flags!, {:outputpath => 'a'}, 'hello') + }.to raise_error(ArgumentError) + end + + it 'should be case insensitive for switches that are not allowed' do + expect { + conf_man.send(:get_merged_configuration_flags!, {:OutputPath => 'a'}, 'hello') + }.to raise_error(ArgumentError) + end + + it 'should be case insensitive to switches that are allowed' do + merged = conf_man.send(:get_merged_configuration_flags!, {:FLAG => 'a'}, 'hello') + merged.should include(:flag) + end + end + + context 'when there are no flags' do + it 'should supply an output path if configuration_flags is an empty hash' do + merged = conf_man.send(:get_merged_configuration_flags!, {}, 'hello') + merged.should include(:outputpath) + merged.length.should eql(1) + end + + it 'should supply an output path if configuration_flags is an empty hash' do + merged = conf_man.send(:get_merged_configuration_flags!, nil, 'hello') + merged.should include(:outputpath) + merged.length.should eql(1) + end + end + + # What should happen if configuration flags contains duplicates? + # flagA => 'a', flaga => 'a' + # or + # flagA => 'a', flaga => 'b' + # + end + + describe '#write_document_generation_script' do + let(:file_like_object) { double("file like object") } + + it "should write the input to a file" do + allow(File).to receive(:open).and_yield(file_like_object) + allow(File).to receive(:join) do |a, b| + [a,b].join("++") + end + allow(file_like_object).to receive(:write) + conf_man.send(:write_document_generation_script, 'file', 'hello') + expect(file_like_object).to have_received(:write) + end + end + + describe "#find_configuration_document" do + it "should find the mof file" do + # These tests seem way too implementation specific. Unfortunatly, File and Dir + # need to be mocked because they are OS specific + allow(File).to receive(:join) do |a, b| + [a,b].join("++") + end + + allow(Dir).to receive(:entries).with("tmp++hello") {['f1', 'f2', 'hello.mof', 'f3']} + expect(conf_man.send(:find_configuration_document, 'hello')).to eql('tmp++hello++hello.mof') + end + + it "should return nil if the mof file is not found" do + allow(File).to receive(:join) do |a, b| + [a,b].join("++") + end + allow(Dir).to receive(:entries).with("tmp++hello") {['f1', 'f2', 'f3']} + expect(conf_man.send(:find_configuration_document, 'hello')).to be_nil + end + end + + describe "#configuration_code" do + it "should build dsc" do + dsc = conf_man.send(:configuration_code, 'archive{}', 'hello') + found_configuration = false + dsc.split(';').each do |command| + if command.downcase =~ /\s*configuration\s+'hello'\s*\{\s*node\s+'localhost'\s*\{\s*archive\s*\{\s*\}\s*\}\s*\}\s*/ + found_configuration = true + end + end + expect(found_configuration).to be_true + end + end +end diff --git a/spec/unit/util/dsc/lcm_output_parser_spec.rb b/spec/unit/util/dsc/lcm_output_parser_spec.rb new file mode 100644 index 0000000000..23a3dbd3ec --- /dev/null +++ b/spec/unit/util/dsc/lcm_output_parser_spec.rb @@ -0,0 +1,169 @@ +# +# 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/util/dsc/lcm_output_parser' + +describe Chef::Util::DSC::LocalConfigurationManager::Parser do + context 'empty input parameter' do + it 'returns an empty array for a 0 length string' do + Chef::Util::DSC::LocalConfigurationManager::Parser::parse('').should be_empty + end + + it 'returns an empty array for a nil input' do + Chef::Util::DSC::LocalConfigurationManager::Parser::parse('').should be_empty + end + end + + context 'correctly formatted output from lcm' do + it 'returns an empty array for a log with no resources' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ End Set ] +EOF + Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str).should be_empty + end + + it 'returns a single resource when only 1 logged with the correct name' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) + resources.length.should eq(1) + resources[0].name.should eq('[name]') + end + + it 'identifies when a resource changes the state of the system' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Start Set ] [name] +logtype: [machinename]: LCM: [ End Set ] [name] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) + resources[0].changes_state?.should be_true + end + + it 'preserves the log provided for how the system changed the state' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Start Set ] [name] +logtype: [machinename]: [message] +logtype: [machinename]: LCM: [ End Set ] [name] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) + resources[0].change_log.should match_array(["[name]","[message]","[name]"]) + end + + it 'should return false for changes_state?' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Skip Set ] [name] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) + resources[0].changes_state?.should be_false + end + + it 'should return an empty array for change_log if changes_state? is false' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Skip Set ] [name] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) + resources[0].change_log.should be_empty + end + end + + context 'Incorrectly formatted output from LCM' do + it 'should allow missing a [End Resource] when its the last one and still find all the resource' do + str = <<-EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Start Test ] +logtype: [machinename]: LCM: [ End Test ] +logtype: [machinename]: LCM: [ Skip Set ] +logtype: [machinename]: LCM: [ End Resource ] +logtype: [machinename]: LCM: [ Start Resource ] [name2] +logtype: [machinename]: LCM: [ Start Test ] +logtype: [machinename]: LCM: [ End Test ] +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ End Set ] +logtype: [machinename]: LCM: [ End Set ] +EOF + + resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) + resources[0].changes_state?.should be_false + resources[1].changes_state?.should be_true + end + + it 'should allow missing a [End Resource] when its the first one and still find all the resource' do + str = <<-EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Start Test ] +logtype: [machinename]: LCM: [ End Test ] +logtype: [machinename]: LCM: [ Skip Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name2] +logtype: [machinename]: LCM: [ Start Test ] +logtype: [machinename]: LCM: [ End Test ] +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ End Set ] +logtype: [machinename]: LCM: [ End Resource ] +logtype: [machinename]: LCM: [ End Set ] +EOF + + resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) + resources[0].changes_state?.should be_false + resources[1].changes_state?.should be_true + end + + it 'should allow missing set and end resource and assume an unconverged resource in this case' do + str = <<-EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Start Test ] +logtype: [machinename]: LCM: [ End Test ] +logtype: [machinename]: LCM: [ Start Resource ] [name2] +logtype: [machinename]: LCM: [ Start Test ] +logtype: [machinename]: LCM: [ End Test ] +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ End Set ] +logtype: [machinename]: LCM: [ End Resource ] +logtype: [machinename]: LCM: [ End Set ] +EOF + resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) + resources[0].changes_state?.should be_true + resources[0].name.should eql('[name]') + resources[1].changes_state?.should be_true + resources[1].name.should eql('[name2]') + end + end +end diff --git a/spec/unit/util/dsc/local_configuration_manager_spec.rb b/spec/unit/util/dsc/local_configuration_manager_spec.rb new file mode 100644 index 0000000000..fb6664bd40 --- /dev/null +++ b/spec/unit/util/dsc/local_configuration_manager_spec.rb @@ -0,0 +1,134 @@ +# +# Author:: Adam Edwards <adamed@getchef.com> +# 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' +require 'chef/util/dsc/local_configuration_manager' + +describe Chef::Util::DSC::LocalConfigurationManager do + + let(:lcm) { Chef::Util::DSC::LocalConfigurationManager.new(nil, 'tmp') } + + let(:normal_lcm_output) { <<-EOH +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOH + } + + let(:no_whatif_lcm_output) { <<-EOH +Start-DscConfiguration : A parameter cannot be found that matches parameter name 'whatif'. +At line:1 char:123 ++ run-somecommand -whatif ++ ~~~~~~~~ + + CategoryInfo : InvalidArgument: (:) [Start-DscConfiguration], ParameterBindingException + + FullyQualifiedErrorId : NamedParameterNotFound,SomeCompany.SomeAssembly.Commands.RunSomeCommand +EOH + } + + let(:dsc_resource_import_failure_output) { <<-EOH +PowerShell provider MSFT_xWebsite failed to execute Test-TargetResource functionality with error message: Please ensure that WebAdministration module is installed. + CategoryInfo : InvalidOperation: (:) [], CimException + FullyQualifiedErrorId : ProviderOperationExecutionFailure + PSComputerName : . PowerShell provider MSFT_xWebsite failed to execute Test-TargetResource functionality with error message: Please ensure that WebAdministration module is installed. + CategoryInfo : InvalidOperation: (:) [], CimException + FullyQualifiedErrorId : ProviderOperationExecutionFailure + PSComputerName : . The SendConfigurationApply function did not succeed. + CategoryInfo : NotSpecified: (root/Microsoft/...gurationManager:String) [], CimException + FullyQualifiedErrorId : MI RESULT 1 + PSComputerName : . +EOH + } + + let(:lcm_status) { + double("LCM cmdlet status", :stderr => lcm_standard_error, :return_value => lcm_standard_output, :succeeded? => lcm_cmdlet_success) + } + + describe 'test_configuration method invocation' do + context 'when interacting with the LCM using a PowerShell cmdlet' do + before(:each) do + allow(lcm).to receive(:run_configuration_cmdlet).and_return(lcm_status) + end + context 'that returns successfully' do + before(:each) do + allow(lcm).to receive(:run_configuration_cmdlet).and_return(lcm_status) + end + + let(:lcm_standard_output) { normal_lcm_output } + let(:lcm_standard_error) { nil } + let(:lcm_cmdlet_success) { true } + + it 'should successfully return resource information for normally formatted output when cmdlet the cmdlet succeeds' do + test_configuration_result = lcm.test_configuration('config') + expect(test_configuration_result.class).to be(Array) + expect(test_configuration_result.length).to be > 0 + expect(Chef::Log).not_to receive(:warn) + end + end + + context 'that fails due to missing what-if switch in DSC resource cmdlet implementation' do + let(:lcm_standard_output) { '' } + let(:lcm_standard_error) { no_whatif_lcm_output } + let(:lcm_cmdlet_success) { false } + + it 'should should return a (possibly empty) array of ResourceInfo instances' do + expect(Chef::Log).to receive(:warn) + test_configuration_result = nil + expect {test_configuration_result = lcm.test_configuration('config')}.not_to raise_error + expect(test_configuration_result.class).to be(Array) + end + end + + context 'that fails due to a DSC resource not being imported before StartDSCConfiguration -whatif is executed' do + let(:lcm_standard_output) { '' } + let(:lcm_standard_error) { dsc_resource_import_failure_output } + let(:lcm_cmdlet_success) { false } + + it 'should log a warning if the message is formatted as expected when a resource import failure occurs' do + expect(Chef::Log).to receive(:warn) + expect(lcm).to receive(:output_has_dsc_module_failure?).and_call_original + test_configuration_result = nil + expect {test_configuration_result = lcm.test_configuration('config')}.not_to raise_error + end + + it 'should return a (possibly empty) array of ResourceInfo instances' do + expect(Chef::Log).to receive(:warn) + test_configuration_result = nil + expect {test_configuration_result = lcm.test_configuration('config')}.not_to raise_error + expect(test_configuration_result.class).to be(Array) + end + end + + context 'that fails due to an PowerShell cmdlet error that cannot be handled' do + let(:lcm_standard_output) { 'some output' } + let(:lcm_standard_error) { 'Abort, Retry, Fail?' } + let(:lcm_cmdlet_success) { false } + + it 'should raise a Chef::Exceptions::PowershellCmdletException' do + expect(Chef::Log).not_to receive(:warn) + expect(lcm).to receive(:output_has_dsc_module_failure?).and_call_original + expect {lcm.test_configuration('config')}.to raise_error(Chef::Exceptions::PowershellCmdletException) + end + end + end + + it 'should identify a correctly formatted error message as a resource import failure' do + expect(lcm.send(:output_has_dsc_module_failure?, dsc_resource_import_failure_output)).to be(true) + end + + it 'should not identify an incorrectly formatted error message as a resource import failure' do + expect(lcm.send(:output_has_dsc_module_failure?, dsc_resource_import_failure_output.gsub('module', 'gibberish'))).to be(false) + end + + it 'should not identify a message without a CimException reference as a resource import failure' do + expect(lcm.send(:output_has_dsc_module_failure?, dsc_resource_import_failure_output.gsub('CimException', 'ArgumentException'))).to be(false) + end + end +end + diff --git a/spec/unit/util/path_helper_spec.rb b/spec/unit/util/path_helper_spec.rb index 66ad323c52..1d97efc607 100644 --- a/spec/unit/util/path_helper_spec.rb +++ b/spec/unit/util/path_helper_spec.rb @@ -214,6 +214,28 @@ describe Chef::Util::PathHelper do PathHelper.stub(:canonical_path).with("bandit").and_return("c:/Bo/Bandit") PathHelper.stub(:canonical_path).with("../bandit/bandit").and_return("c:/bandit/bandit") expect(PathHelper.paths_eql?("bandit", "../bandit/bandit")).to be_false - end + 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(PathHelper.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 windows? + "this\\\\\\*path\\\\\\[needs\\]\\\\escaping\\?" + else + "this/\\*path/\\[needs\\]/escaping\\?" + end + expect(PathHelper).to receive(:join).with(*args).and_call_original + expect(PathHelper).to receive(:cleanpath).and_call_original + expect(PathHelper.escape_glob(*args)).to eq(escaped_path) + end + end end end diff --git a/spec/unit/util/powershell/cmdlet_spec.rb b/spec/unit/util/powershell/cmdlet_spec.rb new file mode 100644 index 0000000000..a964f607c8 --- /dev/null +++ b/spec/unit/util/powershell/cmdlet_spec.rb @@ -0,0 +1,106 @@ +# +# Author:: Jay Mundrawala <jdm@getchef.com> +# 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' +require 'chef/util/powershell/cmdlet' + +describe Chef::Util::Powershell::Cmdlet do + before (:all) do + @node = Chef::Node.new + @cmdlet = Chef::Util::Powershell::Cmdlet.new(@node, 'Some-Commandlet') + end + + describe '#validate_switch_name!' do + it 'should not raise an error if a name contains all upper case letters' do + @cmdlet.send(:validate_switch_name!, "HELLO") + end + + it 'should not raise an error if the name contains all lower case letters' do + @cmdlet.send(:validate_switch_name!, "hello") + end + + it 'should not raise an error if no special characters are used except _' do + @cmdlet.send(:validate_switch_name!, "hello_world") + end + + %w{! @ # $ % ^ & * & * ( ) - = + \{ \} . ? < > \\ /}.each do |sym| + it "raises an Argument error if it configuration name contains #{sym}" do + expect { + @cmdlet.send(:validate_switch_name!, "Hello#{sym}") + }.to raise_error(ArgumentError) + end + end + end + + describe '#escape_parameter_value' do + # Is this list really complete? + %w{` " # '}.each do |c| + it "escapse #{c}" do + @cmdlet.send(:escape_parameter_value, "stuff #{c}").should eql("stuff `#{c}") + end + end + + it 'does not do anything to a string without special characters' do + @cmdlet.send(:escape_parameter_value, 'stuff').should eql('stuff') + end + end + + describe '#escape_string_parameter_value' do + it "surrounds a string with ''" do + @cmdlet.send(:escape_string_parameter_value, 'stuff').should eql("'stuff'") + end + end + + describe '#command_switches_string' do + it 'raises an ArgumentError if the key is not a symbol' do + expect { + @cmdlet.send(:command_switches_string, {'foo' => 'bar'}) + }.to raise_error(ArgumentError) + end + + it 'does not allow invalid switch names' do + expect { + @cmdlet.send(:command_switches_string, {:foo! => 'bar'}) + }.to raise_error(ArgumentError) + end + + it 'ignores switches with a false value' do + @cmdlet.send(:command_switches_string, {foo: false}).should eql('') + end + + it 'should correctly handle a value type of string' do + @cmdlet.send(:command_switches_string, {foo: 'bar'}).should eql("-foo 'bar'") + end + + it 'should correctly handle a value type of string even when it is 0 length' do + @cmdlet.send(:command_switches_string, {foo: ''}).should eql("-foo ''") + end + + it 'should not quote integers' do + @cmdlet.send(:command_switches_string, {foo: 1}).should eql("-foo 1") + end + + it 'should not quote floats' do + @cmdlet.send(:command_switches_string, {foo: 1.0}).should eql("-foo 1.0") + end + + it 'has just the switch when the value is true' do + @cmdlet.send(:command_switches_string, {foo: true}).should eql("-foo") + end + end +end diff --git a/spec/unit/workstation_config_loader_spec.rb b/spec/unit/workstation_config_loader_spec.rb index 78313aec37..de108ff6d7 100644 --- a/spec/unit/workstation_config_loader_spec.rb +++ b/spec/unit/workstation_config_loader_spec.rb @@ -88,7 +88,12 @@ describe Chef::WorkstationConfigLoader do let(:env_pwd) { "/path/to/cwd" } before do - env["PWD"] = env_pwd + if Chef::Platform.windows? + env["CD"] = env_pwd + else + env["PWD"] = env_pwd + end + allow(config_loader).to receive(:path_exists?).with("#{env_pwd}/.chef/knife.rb").and_return(true) allow(File).to receive(:exist?).with("#{env_pwd}/.chef").and_return(true) allow(File).to receive(:directory?).with("#{env_pwd}/.chef").and_return(true) @@ -229,13 +234,14 @@ describe Chef::WorkstationConfigLoader do let(:config_content) { "" } let(:explicit_config_location) do - t = Tempfile.new("#{described_class}-rspec-test") + # could use described_class, but remove all ':' from the path if so. + t = Tempfile.new("Chef-WorkstationConfigLoader-rspec-test") t.print(config_content) t.close t.path end - after { File.unlink(explicit_config_location) } + after { File.unlink(explicit_config_location) if File.exists?(explicit_config_location) } context "and is valid" do |