diff options
author | Lamont Granquist <lamont@opscode.com> | 2013-03-15 15:17:07 -0700 |
---|---|---|
committer | Lamont Granquist <lamont@opscode.com> | 2013-03-15 16:39:35 -0700 |
commit | b480aca6eb0167928cabfb47291a91ddcb50e434 (patch) | |
tree | b2375ab1e65ed8535ee4dd9a5fb552a5f5285a72 /spec | |
parent | 6b653284a81195d13c6561371fdcf70bb0fa0ee7 (diff) | |
download | chef-b480aca6eb0167928cabfb47291a91ddcb50e434.tar.gz |
refactoring of file providers
Diffstat (limited to 'spec')
-rw-r--r-- | spec/functional/resource/cookbook_file_spec.rb | 2 | ||||
-rw-r--r-- | spec/functional/resource/directory_spec.rb | 2 | ||||
-rw-r--r-- | spec/functional/resource/file_spec.rb | 16 | ||||
-rw-r--r-- | spec/functional/resource/remote_directory_spec.rb | 2 | ||||
-rw-r--r-- | spec/functional/resource/remote_file_spec.rb | 16 | ||||
-rw-r--r-- | spec/functional/resource/template_spec.rb | 17 | ||||
-rw-r--r-- | spec/unit/provider/file_spec.rb | 867 | ||||
-rw-r--r-- | spec/unit/util/backup_spec.rb | 151 | ||||
-rw-r--r-- | spec/unit/util/diff_spec.rb | 254 |
9 files changed, 898 insertions, 429 deletions
diff --git a/spec/functional/resource/cookbook_file_spec.rb b/spec/functional/resource/cookbook_file_spec.rb index 9977cd6c99..d61668853c 100644 --- a/spec/functional/resource/cookbook_file_spec.rb +++ b/spec/functional/resource/cookbook_file_spec.rb @@ -32,7 +32,7 @@ describe Chef::Resource::CookbookFile do content end - let(:default_mode) { "600" } + let(:default_mode) { ((0100666 - File.umask) & 07777).to_s(8) } it_behaves_like "a securable resource with reporting" diff --git a/spec/functional/resource/directory_spec.rb b/spec/functional/resource/directory_spec.rb index 9ae0503336..0401f506c3 100644 --- a/spec/functional/resource/directory_spec.rb +++ b/spec/functional/resource/directory_spec.rb @@ -23,7 +23,7 @@ describe Chef::Resource::Directory do let(:directory_base) { "directory_spec" } - let(:default_mode) { "755" } + let(:default_mode) { ((0100777 - File.umask) & 07777).to_s(8) } def create_resource events = Chef::EventDispatch::Dispatcher.new diff --git a/spec/functional/resource/file_spec.rb b/spec/functional/resource/file_spec.rb index 2c18d07520..7da15ff2e5 100644 --- a/spec/functional/resource/file_spec.rb +++ b/spec/functional/resource/file_spec.rb @@ -58,6 +58,22 @@ describe Chef::Resource::File do it_behaves_like "a securable resource with reporting" + describe "when running action :create without content" do + before do + resource_without_content.run_action(:create) + end + + context "and the target file does not exist" do + it "creates the file" do + File.should exist(path) + end + + it "is marked updated by last action" do + resource_without_content.should be_updated_by_last_action + 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/remote_directory_spec.rb b/spec/functional/resource/remote_directory_spec.rb index b4e26a59b2..10d4d973e9 100644 --- a/spec/functional/resource/remote_directory_spec.rb +++ b/spec/functional/resource/remote_directory_spec.rb @@ -22,7 +22,7 @@ describe Chef::Resource::RemoteDirectory do include_context Chef::Resource::Directory let(:directory_base) { "directory_spec" } - let(:default_mode) { "755" } + let(:default_mode) { ((0100777 - File.umask) & 07777).to_s(8) } def create_resource cookbook_repo = File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks")) diff --git a/spec/functional/resource/remote_file_spec.rb b/spec/functional/resource/remote_file_spec.rb index fbb921d48c..0354cc1aa3 100644 --- a/spec/functional/resource/remote_file_spec.rb +++ b/spec/functional/resource/remote_file_spec.rb @@ -37,21 +37,7 @@ describe Chef::Resource::RemoteFile do create_resource end - let(:default_mode) do - # TODO: Lots of ugly here :( - # RemoteFile uses FileUtils.cp. FileUtils does a copy by opening the - # destination file and writing to it. Before 1.9.3, it does not preserve - # the mode of the copied file. In 1.9.3 and after, it does. So we have to - # figure out what the default mode ought to be via heuristic. - - t = Tempfile.new("get-the-mode") - path = t.path - path_2 = t.path + "fileutils-mode-test" - FileUtils.cp(path, path_2) - t.close - m = File.stat(path_2).mode - (07777 & m).to_s(8) - end + let(:default_mode) { ((0100666 - File.umask) & 07777).to_s(8) } before(:all) do @server = TinyServer::Manager.new diff --git a/spec/functional/resource/template_spec.rb b/spec/functional/resource/template_spec.rb index 0987aabf05..ae568a496a 100644 --- a/spec/functional/resource/template_spec.rb +++ b/spec/functional/resource/template_spec.rb @@ -49,22 +49,7 @@ describe Chef::Resource::Template do create_resource end - let(:default_mode) do - # TODO: Lots of ugly here :( - # RemoteFile uses FileUtils.cp. FileUtils does a copy by opening the - # destination file and writing to it. Before 1.9.3, it does not preserve - # the mode of the copied file. In 1.9.3 and after, it does. So we have to - # figure out what the default mode ought to be via heuristic. - - t = Tempfile.new("get-the-mode") - path = t.path - path_2 = t.path + "fileutils-mode-test" - FileUtils.cp(path, path_2) - t.close - m = File.stat(path_2).mode - (07777 & m).to_s(8) - end - + let(:default_mode) { ((0100666 - File.umask) & 07777).to_s(8) } it_behaves_like "a file resource" diff --git a/spec/unit/provider/file_spec.rb b/spec/unit/provider/file_spec.rb index 5b1cbbbdb8..7e99e6755b 100644 --- a/spec/unit/provider/file_spec.rb +++ b/spec/unit/provider/file_spec.rb @@ -1,6 +1,7 @@ # # Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Author:: Lamont Granquist (<lamont@opscode.com>) +# Copyright:: Copyright (c) 2008-2013 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,499 +17,575 @@ # limitations under the License. # - require 'spec_helper' require 'tmpdir' describe Chef::Provider::File do - before(:each) do - @node = Chef::Node.new - @node.name "latte" - @events = Chef::EventDispatch::Dispatcher.new - @run_context = Chef::RunContext.new(@node, {}, @events) - @resource = Chef::Resource::File.new("seattle") - @resource.path(File.expand_path(File.join(CHEF_SPEC_DATA, "templates", "seattle.txt"))) + # Mocksplosion + + let(:node) { double('Chef::Node') } + let(:events) { double('Chef::Events').as_null_object } # mock all the methods + let(:run_context) { double('Chef::RunContext', :node => node, :events => events) } + let(:enclosing_directory) { File.expand_path(File.join(CHEF_SPEC_DATA, "templates")) } + let(:resource_path) { File.expand_path(File.join(enclosing_directory, "seattle.txt")) } - @provider = Chef::Provider::File.new(@resource, @run_context) + let(:resource) do + # need to check for/against mutating state within the new_resource, so don't mock + resource = Chef::Resource::File.new("seattle") + resource.path(resource_path) + resource end - it "should return a Chef::Provider::File" do - @provider.should be_a_kind_of(Chef::Provider::File) + # Subject + + let(:provider) do + Chef::Provider::File.new(resource, run_context) end - it "should store the resource passed to new as new_resource" do - @provider.new_resource.should eql(@resource) + # Filesystem stubs + + def setup_normal_file + File.stub!(:exists?).with(resource_path).and_return(true) + File.stub!(:directory?).with(resource_path).and_return(false) + File.stub!(:directory?).with(enclosing_directory).and_return(true) + File.stub!(:writable?).with(resource_path).and_return(true) + File.stub!(:symlink?).with(resource_path).and_return(false) end - it "should store the node passed to new as node" do - @provider.node.should eql(@node) + def setup_missing_file + File.stub!(:exists?).with(resource_path).and_return(false) + File.stub!(:directory?).with(resource_path).and_return(false) + File.stub!(:directory?).with(enclosing_directory).and_return(true) + File.stub!(:writable?).with(resource_path).and_return(false) + File.stub!(:symlink?).with(resource_path).and_return(false) end - it "should load a current resource based on the one specified at construction" do - @provider.load_current_resource - @provider.current_resource.should be_a_kind_of(Chef::Resource::File) - @provider.current_resource.name.should eql(@resource.name) - @provider.current_resource.path.should eql(@resource.path) - @provider.current_resource.content.should eql(nil) + def setup_symlink + File.stub!(:exists?).with(resource_path).and_return(true) + File.stub!(:directory?).with(resource_path).and_return(false) + File.stub!(:directory?).with(enclosing_directory).and_return(true) + File.stub!(:writable?).with(resource_path).and_return(true) + File.stub!(:symlink?).with(resource_path).and_return(true) end - describe "examining file security metadata on Unix" do - before do - Chef::Platform.stub!(:windows?).and_return(false) - end - it "should collect the current state of the file on the filesystem and populate current_resource" do - # test setup - stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000) - ::File.should_receive(:stat).exactly(1).times.with(@resource.path).and_return(stat_struct) + def setup_unwritable_file + File.stub!(:exists?).with(resource_path).and_return(true) + File.stub!(:directory?).with(resource_path).and_return(false) + File.stub!(:directory?).with(enclosing_directory).and_return(true) + File.stub!(:writable?).with(resource_path).and_return(false) + File.stub!(:symlink?).with(resource_path).and_return(false) + end - # test execution + def setup_missing_enclosing_directory + File.stub!(:exists?).with(resource_path).and_return(false) + File.stub!(:directory?).with(resource_path).and_return(false) + File.stub!(:directory?).with(enclosing_directory).and_return(false) + File.stub!(:writable?).with(resource_path).and_return(false) + File.stub!(:symlink?).with(resource_path).and_return(false) + end - Etc.should_receive(:getgrgid).with(0).and_return(mock("Group Ent", :name => "wheel")) - Etc.should_receive(:getpwuid).with(0).and_return(mock("User Ent", :name => "root")) + # Tests - # test execution - @provider.load_current_resource + it "should return a Chef::Provider::File" do + provider.should be_a_kind_of(Chef::Provider::File) + end - # post-condition checks - @provider.current_resource.mode.should == "0600" - @provider.current_resource.owner.should == "root" - @provider.current_resource.group.should == "wheel" - end + it "should store the resource passed to new as new_resource" do + provider.new_resource.should eql(resource) + end - it "should NOT update the new_resource state with the current_resourse state if new_resource state is already specified" do - # test setup - stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000) - ::File.should_receive(:stat).exactly(1).times.with(@resource.path).and_return(stat_struct) + it "should store the node passed to new as node" do + provider.node.should eql(node) + end - @provider.new_resource.group(1) - @provider.new_resource.owner(1) - @provider.new_resource.mode(0644) + context "when loading the current resource" do - # test execution - @provider.load_current_resource + context "when running load_current_resource and the file exists" do + before do + File.should_receive(:exist?).with(resource_path).at_least(:once).and_return(true) + provider.load_current_resource + end - # post-condition checks - @provider.new_resource.group.should == 1 - @provider.new_resource.owner.should == 1 - @provider.new_resource.mode.should == 0644 - end + it "should load a current resource based on the one specified at construction" do + provider.current_resource.should be_a_kind_of(Chef::Resource::File) + end - context "when the new_resource does not specify the desired access control" do - it "records access control information in the new resource after modifying the file" do - # test setup - stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000) - # called once in update_new_file_state and once in checksum - ::File.should_receive(:stat).once.with(@provider.new_resource.path).and_return(stat_struct) - ::File.should_receive(:directory?).once.with(@provider.new_resource.path).and_return(false) + it "the loaded current_resource name should be the same as the resource name" do + provider.current_resource.name.should eql(resource.name) + end - Etc.should_receive(:getpwuid).with(0).and_return(mock("User Ent", :name => "root")) - Etc.should_receive(:getgrgid).with(0).and_return(mock("Group Ent", :name => "wheel")) + it "the loaded current_resource path should be the same as the resoure path" do + provider.current_resource.path.should eql(resource.path) + end - @provider.new_resource.group(nil) - @provider.new_resource.owner(nil) - @provider.new_resource.mode(nil) + it "the loaded current_resource content should be nil" do + provider.current_resource.content.should eql(nil) + end + end - # test exectution - @provider.update_new_file_state + context "when running load_current_resource and the file does not exist" do + before do + File.should_receive(:exist?).with(resource_path).at_least(:once).and_return(false) + provider.load_current_resource + end - # post-condition checks - @provider.new_resource.group.should == "wheel" - @provider.new_resource.owner.should == "root" - @provider.new_resource.mode.should == "0600" + it "the current_resource should be a Chef::Resource::File" do + provider.current_resource.should be_a_kind_of(Chef::Resource::File) end - end - end - describe "when reporting security metadata on windows" do + it "the current_resource name should be the same as the resource name" do + provider.current_resource.name.should eql(resource.name) + end - it "records the file owner" do - pending - end + it "the current_resource path should be the same as the resource path" do + provider.current_resource.path.should eql(resource.path) + end - it "records rights for each user in the ACL" do - pending + it "the loaded current_resource content should be nil" do + provider.current_resource.content.should eql(nil) + end end - it "records deny_rights for each user in the ACL" do - pending - end - end + context "examining file security metadata on Unix with a file that exists" do + before do + # fake that we're on unix even if we're on windows + Chef::Platform.stub!(:windows?).and_return(false) + # mock up the filesystem to behave like unix + stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000) + File.should_receive(:exist?).with(resource_path).at_least(:once).and_return(true) + File.should_receive(:stat).with(resource.path).at_least(:once).and_return(stat_struct) + Etc.stub!(:getgrgid).with(0).and_return(mock("Group Ent", :name => "wheel")) + Etc.stub!(:getpwuid).with(0).and_return(mock("User Ent", :name => "root")) + end - it "should load a mostly blank current resource if the file specified in new_resource doesn't exist/isn't readable" do - resource = Chef::Resource::File.new("seattle") - resource.path(File.expand_path(File.join(CHEF_SPEC_DATA, "templates", "woot.txt"))) - node = Chef::Node.new - node.name "latte" - provider = Chef::Provider::File.new(resource, @run_context) - provider.load_current_resource - provider.current_resource.should be_a_kind_of(Chef::Resource::File) - provider.current_resource.name.should eql(resource.name) - provider.current_resource.path.should eql(resource.path) - end + context "when the new_resource does not specify any state" do + before do + provider.load_current_resource + end - it "should not backup symbolic links on delete" do - path = File.expand_path(File.join(CHEF_SPEC_DATA, "detroit.txt")) - ::File.open(path, "w") do |file| - file.write("Detroit's not so nice, so you should come to Seattle instead and buy me a beer instead.") - end - @resource = Chef::Resource::File.new("detroit") - @resource.path(path) - @node = Chef::Node.new - @node.name "latte" - @provider = Chef::Provider::File.new(@resource, @run_context) - - ::File.stub!(:symlink?).and_return(true) - @provider.should_not_receive(:backup) - @provider.run_action(:delete) - @resource.should be_updated_by_last_action - end + it "should load the permissions into the current_resource" do + provider.current_resource.mode.should == "0600" + provider.current_resource.owner.should == "root" + provider.current_resource.group.should == "wheel" + end - it "should compare the current content with the requested content" do - @provider.load_current_resource + it "should not set the new_resource permissions" do + provider.new_resource.group.should be_nil + provider.new_resource.owner.should be_nil + provider.new_resource.mode.should be_nil + end + end - @provider.new_resource.content "foobar" - @provider.compare_content.should eql(false) + context "when the new_resource explicitly specifies resource state as numbers" do + before do + resource.owner(1) + resource.group(1) + resource.mode(0644) + provider.load_current_resource + end - @provider.new_resource.content IO.read(@resource.path) - @provider.compare_content.should eql(true) - end + it "should load the permissions into the current_resource as numbers (BUT DOESN'T, BUG?)" do + # FIXME: inconsistency, hmmmm.... + provider.current_resource.mode.should == "0600" + provider.current_resource.owner.should == 0 + provider.current_resource.group.should == 0 + end - it "should set the content of the file to the requested content" do - io = StringIO.new - @provider.load_current_resource - @provider.new_resource.content "foobar" - @provider.should_receive(:diff_current_from_content).and_return("") - @provider.should_receive(:backup) - # checksum check - File.should_receive(:open).with(@provider.new_resource.path, "rb").and_yield(io) - File.should_receive(:open).with(@provider.new_resource.path, "w").and_yield(io) - @provider.set_content - io.string.should == "foobar" - end + it "should not set the new_resource permissions" do + provider.new_resource.group.should == 1 + provider.new_resource.owner.should == 1 + provider.new_resource.mode.should == 0644 + end + end - it "should not set the content of the file if it already matches the requested content" do - @provider.load_current_resource - @provider.new_resource.content IO.read(@resource.path) - # Checksum check: - File.should_receive(:open).with(@resource.path, "rb").and_yield(StringIO.new(@resource.content)) - File.should_not_receive(:open).with(@provider.new_resource.path, "w") - lambda { @provider.set_content }.should_not raise_error - @resource.should_not be_updated_by_last_action - end + context "when the new_resource explicitly specifies resource state as symbols" do + before do + resource.owner("macklemore") + resource.group("seattlehiphop") + resource.mode("0321") + provider.load_current_resource + end - it "should create the file if it is missing, then set the attributes on action_create" do - @provider.load_current_resource - @provider.stub!(:update_new_file_state) - @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo")) - @provider.access_controls.should_receive(:set_all) - @provider.should_receive(:diff_current_from_content).and_return("") - File.stub!(:open).and_return(1) - #File.should_receive(:directory?).with("/tmp").and_return(true) - File.should_receive(:open).with(@provider.new_resource.path, "w+") - @provider.run_action(:create) - @resource.should be_updated_by_last_action - end + it "should load the permissions into the current_resource as symbols" do + provider.current_resource.mode.should == "0600" + provider.current_resource.owner.should == "root" + provider.current_resource.group.should == "wheel" + end - it "should create the file with the proper content if it is missing, then set attributes on action_create" do - io = StringIO.new - @provider.load_current_resource - @provider.new_resource.content "foobar" - @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo")) - @provider.should_receive(:diff_current_from_content).and_return("") - @provider.stub!(:update_new_file_state) - File.should_receive(:open).with(@provider.new_resource.path, "w+").and_yield(io) - @provider.access_controls.should_receive(:set_all) - @provider.run_action(:create) - io.string.should == "foobar" - @resource.should be_updated_by_last_action - end + it "should not set the new_resource permissions" do + provider.new_resource.group.should == "seattlehiphop" + provider.new_resource.owner.should == "macklemore" + provider.new_resource.mode.should == "0321" + end + end - it "should delete the file if it exists and is writable on action_delete" do - @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo")) - @provider.stub!(:backup).and_return(true) - File.should_receive("exists?").exactly(2).times.with(@provider.new_resource.path).and_return(true) - File.should_receive("writable?").with(@provider.new_resource.path).and_return(true) - File.should_receive(:delete).with(@provider.new_resource.path).and_return(true) - @provider.run_action(:delete) - @resource.should be_updated_by_last_action - end + end - it "should not raise an error if it cannot delete the file because it does not exist" do - @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo")) - @provider.stub!(:backup).and_return(true) - File.should_receive("exists?").exactly(2).times.with(@provider.new_resource.path).and_return(false) - lambda { @provider.run_action(:delete) }.should_not raise_error() - @resource.should_not be_updated_by_last_action - end + context "examining file security metadata on Unix with a file that does not exist" do + before do + # fake that we're on unix even if we're on windows + Chef::Platform.stub!(:windows?).and_return(false) + File.should_receive(:exist?).with(resource_path).at_least(:once).and_return(false) + end - it "should update the atime/mtime on action_touch" do - @provider.load_current_resource - @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo")) - @provider.should_receive(:diff_current_from_content).and_return("") - @provider.stub!(:update_new_file_state) - File.should_receive(:utime).once.and_return(1) - File.stub!(:open).and_return(1) - @provider.access_controls.should_receive(:set_all).once - @provider.run_action(:touch) - @resource.should be_updated_by_last_action - end + context "when the new_resource does not specify any state" do + before do + provider.load_current_resource + end - it "should keep 1 backup copy if specified" do - @provider.load_current_resource - @provider.new_resource.stub!(:path).and_return("/tmp/s-20080705111233") - @provider.new_resource.stub!(:backup).and_return(1) - Dir.stub!(:[]).and_return([ "/tmp/s-20080705111233", "/tmp/s-20080705111232", "/tmp/s-20080705111223"]) - FileUtils.should_receive(:rm).with("/tmp/s-20080705111223").once.and_return(true) - FileUtils.should_receive(:rm).with("/tmp/s-20080705111232").once.and_return(true) - FileUtils.stub!(:cp).and_return(true) - FileUtils.stub!(:mkdir_p).and_return(true) - File.stub!(:exist?).and_return(true) - @provider.backup - end + it "the current_resource permissions should be nil" do + provider.current_resource.mode.should be_nil + provider.current_resource.owner.should be_nil + provider.current_resource.group.should be_nil + end - it "should backup a file no more than :backup times" do - @provider.load_current_resource - @provider.new_resource.stub!(:path).and_return("/tmp/s-20080705111233") - @provider.new_resource.stub!(:backup).and_return(2) - Dir.stub!(:[]).and_return([ "/tmp/s-20080705111233", "/tmp/s-20080705111232", "/tmp/s-20080705111223"]) - FileUtils.should_receive(:rm).with("/tmp/s-20080705111223").once.and_return(true) - FileUtils.stub!(:cp).and_return(true) - FileUtils.stub!(:mkdir_p).and_return(true) - File.stub!(:exist?).and_return(true) - @provider.backup - end + it "should not set the new_resource permissions" do + provider.new_resource.group.should be_nil + provider.new_resource.owner.should be_nil + provider.new_resource.mode.should be_nil + end + end - it "should not attempt to backup a file if :backup == 0" do - @provider.load_current_resource - @provider.new_resource.stub!(:path).and_return("/tmp/s-20080705111233") - @provider.new_resource.stub!(:backup).and_return(0) - FileUtils.stub!(:cp).and_return(true) - File.stub!(:exist?).and_return(true) - FileUtils.should_not_receive(:cp) - @provider.backup - end + context "when the new_resource explicitly specifies resource state" do + before do + resource.owner(63945) + resource.group(51948) + resource.mode(0123) + provider.load_current_resource + end - it "should put the backup backup file in the directory specified by Chef::Config[:file_backup_path]" do - @provider.load_current_resource - @provider.new_resource.stub!(:path).and_return("/tmp/s-20080705111233") - @provider.new_resource.stub!(:backup).and_return(1) - Chef::Config.stub!(:[]).with(:file_backup_path).and_return("/some_prefix") - Dir.stub!(:[]).and_return([ "/some_prefix/tmp/s-20080705111233", "/some_prefix/tmp/s-20080705111232", "/some_prefix/tmp/s-20080705111223"]) - FileUtils.should_receive(:mkdir_p).with("/some_prefix/tmp").once - FileUtils.should_receive(:rm).with("/some_prefix/tmp/s-20080705111232").once.and_return(true) - FileUtils.should_receive(:rm).with("/some_prefix/tmp/s-20080705111223").once.and_return(true) - FileUtils.stub!(:cp).and_return(true) - File.stub!(:exist?).and_return(true) - @provider.backup - end + it "the current_resource permissions should be nil" do + provider.current_resource.mode.should be_nil + provider.current_resource.owner.should be_nil + provider.current_resource.group.should be_nil + end - it "should strip the drive letter from the backup resource path (for Windows platforms)" do - @provider.load_current_resource - @provider.new_resource.stub!(:path).and_return("C:/tmp/s-20080705111233") - @provider.new_resource.stub!(:backup).and_return(1) - Chef::Config.stub!(:[]).with(:file_backup_path).and_return("C:/some_prefix") - Dir.stub!(:[]).and_return([ "C:/some_prefix/tmp/s-20080705111233", "C:/some_prefix/tmp/s-20080705111232", "C:/some_prefix/tmp/s-20080705111223"]) - FileUtils.should_receive(:mkdir_p).with("C:/some_prefix/tmp").once - FileUtils.should_receive(:rm).with("C:/some_prefix/tmp/s-20080705111232").once.and_return(true) - FileUtils.should_receive(:rm).with("C:/some_prefix/tmp/s-20080705111223").once.and_return(true) - FileUtils.stub!(:cp).and_return(true) - File.stub!(:exist?).and_return(true) - @provider.backup + it "should not set the new_resource permissions" do + provider.new_resource.group.should == 51948 + provider.new_resource.owner.should == 63945 + provider.new_resource.mode.should == 0123 + end + end + end end - it "should keep the same ownership on backed up files" do - @provider.load_current_resource - @provider.new_resource.stub!(:path).and_return("/tmp/s-20080705111233") - @provider.new_resource.stub!(:backup).and_return(1) - Chef::Config.stub!(:[]).with(:file_backup_path).and_return("/some_prefix") - Dir.stub!(:[]).and_return([ "/some_prefix/tmp/s-20080705111233", "/some_prefix/tmp/s-20080705111232", "/some_prefix/tmp/s-20080705111223"]) - FileUtils.stub!(:mkdir_p).and_return(true) - FileUtils.stub!(:rm).and_return(true) - File.stub!(:exist?).and_return(true) - Time.stub!(:now).and_return(Time.at(1272147455).getgm) - FileUtils.should_receive(:cp).with("/tmp/s-20080705111233", "/some_prefix/tmp/s-20080705111233.chef-20100424221735", {:preserve => true}).and_return(true) - @provider.backup - end + context "when loading the new_resource after the run" do - describe "when the enclosing directory does not exist" do before do - @resource.path("/tmp/no-such-path/file.txt") + # fake that we're on unix even if we're on windows + Chef::Platform.stub!(:windows?).and_return(false) + # mock up the filesystem to behave like unix + stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000) + File.stub!(:stat).with(resource.path).and_return(stat_struct) + Etc.stub!(:getgrgid).with(0).and_return(mock("Group Ent", :name => "wheel")) + Etc.stub!(:getpwuid).with(0).and_return(mock("User Ent", :name => "root")) + provider.send(:load_resource_attributes_from_file, resource) end - it "raises a specific error describing the problem" do - lambda {@provider.run_action(:create)}.should raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + it "new_resource should record the new permission information" do + provider.new_resource.group.should == "wheel" + provider.new_resource.owner.should == "root" + provider.new_resource.mode.should == "0600" end end - describe "when creating a file which may be missing" do - it "should not call action create if the file exists" do - @resource.path(File.expand_path(File.join(CHEF_SPEC_DATA, "templates", "seattle.txt"))) - @provider = Chef::Provider::File.new(@resource, @run_context) - File.should_not_receive(:open) - @provider.run_action(:create_if_missing) - @resource.should_not be_updated_by_last_action + context "when reporting security metadata on windows (FIXME: moar tests)" do + + it "records the file owner" do + pending + end + + it "records rights for each user in the ACL" do + pending end - it "should call action create if the does not file exist" do - @resource.path("/tmp/example-dir/non_existant_file") - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.should_receive(:diff_current_from_content).and_return("") - ::File.stub!(:exists?).with(@resource.path).and_return(false) - ::File.stub!(:directory?).with("/tmp/example-dir/non_existant_file").and_return(false) - ::File.stub!(:directory?).with("/tmp/example-dir").and_return(true) - @provider.stub!(:update_new_file_state) - io = StringIO.new - File.should_receive(:open).with(@provider.new_resource.path, "w+").and_yield(io) - #@provider.should_receive(:action_create).and_return(true) - @provider.run_action(:create_if_missing) - @resource.should be_updated_by_last_action + it "records deny_rights for each user in the ACL" do + pending end end - describe "when a diff is requested", :uses_diff => true do - - before(:each) do - @original_config = Chef::Config.hash_dup + context "define_resource_requirements" do + context "when the enclosing directory does not exist" do + before { setup_missing_enclosing_directory } + + [:create, :create_if_missing, :touch].each do |action| + describe "action #{action}" do + it "raises EnclosingDirectoryDoesNotExist" do + lambda {provider.run_action(action)}.should raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + end + + it "does not raise an exception in why-run mode" do + Chef::Config[:why_run] = true + lambda {provider.run_action(action)}.should_not raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + Chef::Config[:why_run] = false + end + end + end end - after(:each) do - Chef::Config.configuration = @original_config if @original_config + context "when the file exists but is not deletable" do + before { setup_unwritable_file } + + it "action delete raises InsufficientPermissions" do + lambda {provider.run_action(:delete)}.should raise_error(Chef::Exceptions::InsufficientPermissions) + end + + it "action delete also raises InsufficientPermissions in why-run mode" do + Chef::Config[:why_run] = true + lambda {provider.run_action(:delete)}.should raise_error(Chef::Exceptions::InsufficientPermissions) + Chef::Config[:why_run] = false + end end + end - describe "when identifying files as binary or text" do + context "action create" do + it "should create the file, update its contents and then set the acls on the file" do + setup_missing_file + provider.should_receive(:do_create_file) + provider.should_receive(:do_contents_changes) + provider.should_receive(:do_acl_changes) + provider.should_receive(:load_resource_attributes_from_file).twice # current_resource + new_resource + provider.run_action(:create) + end - it "should identify zero-length files as text" do - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.is_binary?(file.path).should be_false + context "do_create_file" do + context "when the file exists" do + before { setup_normal_file } + it "should not create the file" do + provider.deployment_strategy.should_not_receive(:create).with(resource_path) + provider.send(:do_create_file) + provider.send(:file_created?).should == false end end - - it "should correctly identify text files as being text" do - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - file.puts("This is a text file.") - file.puts("That has a couple of lines in it.") - file.puts("And lets make sure that other printable chars work too: ~!@\#$%^&*()`:\"<>?{}|_+,./;'[]\\-=") - file.close - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.is_binary?(file.path).should be_false + context "when the file does not exist" do + before { setup_missing_file } + it "should create the file" do + provider.deployment_strategy.should_receive(:create).with(resource_path) + provider.send(:do_create_file) + provider.send(:file_created?).should == true end end + end + + context "do_contents_changes" do + context "when there is content to deploy" do + before do + tempfile = double('Tempfile', :path => "/tmp/foo-bar-baz") + provider.send(:content).should_receive(:tempfile).at_least(:once).and_return(tempfile) + File.should_receive(:exists?).with("/tmp/foo-bar-baz").and_return(true) + tempfile.should_receive(:unlink).once + end - it "should identify a null-terminated string as binary" do - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - file.write("This is a binary file.\0") - file.close - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.is_binary?(file.path).should be_true + context "when the contents have changed" do + let (:tempfile_path) { "/tmp/foo-bar-baz" } + let (:tempfile_md5) { "71f3811d0472fbef15d90a779615b254" } + let (:diff_for_reporting) { "+++\n---\n+foo\n-bar\n" } + before do + provider.stub!(:contents_changed?).and_return(true) + diff = double('Diff', :for_output => ['+++','---','+foo','-bar'], + :for_reporting => diff_for_reporting ) + diff.stub!(:diff).with(resource_path, tempfile_path).and_return(true) + provider.should_receive(:diff).at_least(:once).and_return(diff) + provider.should_receive(:checksum).with(tempfile_path).and_return(tempfile_md5) + provider.deployment_strategy.should_receive(:deploy).with(tempfile_path, resource_path) + end + context "when the file was created" do + before { provider.should_receive(:file_created?).at_least(:once).and_return(true) } + it "does not backup the file and does not produce a diff for reporting" do + provider.should_not_receive(:backup) + provider.send(:do_contents_changes) + resource.diff.should be_nil + end + end + context "when the file was not created" do + before { provider.should_receive(:file_created?).at_least(:once).and_return(false) } + it "backs up the file and produces a diff for reporting" do + provider.should_receive(:backup) + provider.send(:do_contents_changes) + resource.diff.should == diff_for_reporting + end + end end + + it "does nothing when the contents have not changed" do + provider.stub!(:contents_changed?).and_return(false) + provider.should_not_receive(:diff) + provider.send(:do_contents_changes) + end + end + + it "does nothing when there is no content to deploy (tempfile returned from contents is nil)" do + provider.send(:content).should_receive(:tempfile).at_least(:once).and_return(nil) + provider.should_not_receive(:diff) + lambda{ provider.send(:do_contents_changes) }.should_not raise_error end + it "raises an exception when the content object returns a tempfile with a nil path" do + tempfile = double('Tempfile', :path => nil) + provider.send(:content).should_receive(:tempfile).at_least(:once).and_return(tempfile) + lambda{ provider.send(:do_contents_changes) }.should raise_error + end + + it "raises an exception when the content object returns a tempfile that does not exist" do + tempfile = double('Tempfile', :path => "/tmp/foo-bar-baz") + provider.send(:content).should_receive(:tempfile).at_least(:once).and_return(tempfile) + File.should_receive(:exists?).with("/tmp/foo-bar-baz").and_return(false) + lambda{ provider.send(:do_contents_changes) }.should raise_error + end end - it "should not return diff output when chef config has disabled it" do - Chef::Config[:diff_disabled] = true - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.load_current_resource - result = @provider.diff_current_from_content "foo baz" - result.should == [ "(diff output suppressed by config)" ] - @resource.diff.should be_nil + context "do_acl_changes" do + it "needs tests" do + pending end end - it "should not return diff output when there is no new file to compare it to" do - Tempfile.open("some-temp") do |file| - Tempfile.open("other-temp") do |missing_file| - missing_path = missing_file.path - missing_file.close - missing_file.unlink - @resource.path(file.path) - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.load_current_resource - result = @provider.diff_current missing_path - result.should == [ "(no temp file with new content, diff output suppressed)" ] - @resource.diff.should be_nil + # it "should compare the current content with the requested content" do + # @provider.load_current_resource + # + # @provider.new_resource.content "foobar" + # @provider.compare_content.should eql(false) + # + # @provider.new_resource.content IO.read(@resource.path) + # @provider.compare_content.should eql(true) + # end + # + # it "should set the content of the file to the requested content" do + # io = StringIO.new + # @provider.load_current_resource + # @provider.new_resource.content "foobar" + # @provider.should_receive(:diff_current_from_content).and_return("") + # @provider.should_receive(:backup) + # # checksum check + # File.should_receive(:open).with(@provider.new_resource.path, "rb").and_yield(io) + # File.should_receive(:open).with(@provider.new_resource.path, "w").and_yield(io) + # @provider.set_content + # io.string.should == "foobar" + # end + # + # it "should not set the content of the file if it already matches the requested content" do + # @provider.load_current_resource + # @provider.new_resource.content IO.read(@resource.path) + # # Checksum check: + # File.should_receive(:open).with(@resource.path, "rb").and_yield(StringIO.new(@resource.content)) + # File.should_not_receive(:open).with(@provider.new_resource.path, "w") + # lambda { @provider.set_content }.should_not raise_error + # @resource.should_not be_updated_by_last_action + # end + # + # it "should create the file if it is missing, then set the attributes on action_create" do + # @provider.load_current_resource + # @provider.stub!(:update_new_file_state) + # @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo")) + # @provider.access_controls.should_receive(:set_all) + # @provider.should_receive(:diff_current_from_content).and_return("") + # File.stub!(:open).and_return(1) + # #File.should_receive(:directory?).with("/tmp").and_return(true) + # File.should_receive(:open).with(@provider.new_resource.path, "w+") + # @provider.run_action(:create) + # @resource.should be_updated_by_last_action + # end + # + # it "should create the file with the proper content if it is missing, then set attributes on action_create" do + # io = StringIO.new + # @provider.load_current_resource + # @provider.new_resource.content "foobar" + # @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo")) + # @provider.should_receive(:diff_current_from_content).and_return("") + # @provider.stub!(:update_new_file_state) + # File.should_receive(:open).with(@provider.new_resource.path, "w+").and_yield(io) + # @provider.access_controls.should_receive(:set_all) + # @provider.run_action(:create) + # io.string.should == "foobar" + # @resource.should be_updated_by_last_action + # end + end + + context "action delete" do + context "when the file exists" do + context "when the file is writable" do + context "when the file is not a symlink" do + before { setup_normal_file } + it "should backup and delete the file and be updated by the last action" do + provider.should_receive(:backup).at_least(:once).and_return(true) + File.should_receive(:delete).with(resource_path).and_return(true) + provider.run_action(:delete) + resource.should be_updated_by_last_action + end + end + context "when the file is a symlink" do + before { setup_symlink } + it "should not backup the symlink" do + provider.should_not_receive(:backup) + File.should_receive(:delete).with(resource_path).and_return(true) + provider.run_action(:delete) + resource.should be_updated_by_last_action + end + end + end + context "when the file is not writable" do + before { setup_unwritable_file } + it "should not try to backup or delete the file, and should not be updated by last action" do + provider.should_not_receive(:backup) + File.should_not_receive(:delete) + lambda { provider.run_action(:delete) }.should raise_error() + resource.should_not be_updated_by_last_action end end end - it "should produce diff output when the file does not exist yet, but suppress reporting it" do - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - file.close - file.unlink - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.load_current_resource - result = @provider.diff_current_from_content "foo baz" - result.length.should == 4 - @resource.diff.should be_nil + context "when the file does not exist" do + before { setup_missing_file } + + it "should not try to backup or delete the file, and should not be updated by last action" do + provider.should_not_receive(:backup) + File.should_not_receive(:delete) + lambda { provider.run_action(:delete) }.should_not raise_error() + resource.should_not be_updated_by_last_action end end + end - it "should not produce a diff when the current resource file is above the filesize threshold" do - Chef::Config[:diff_filesize_threshold] = 5 - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - file.puts("this is a line which is longer than 5 characters") - file.flush - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.load_current_resource - result = @provider.diff_current_from_content "foo" # not longer than 5 - result.should == [ "(file sizes exceed 5 bytes, diff output suppressed)" ] - @resource.diff.should be_nil + context "action touch" do + context "when the file does not exist" do + before { setup_missing_file } + it "should update the atime/mtime on action_touch" do + File.should_receive(:utime).once + provider.should_receive(:action_create) + provider.run_action(:touch) + resource.should be_updated_by_last_action end end - - it "should not produce a diff when the new content is above the filesize threshold" do - Chef::Config[:diff_filesize_threshold] = 5 - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - file.puts("foo") - file.flush - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.load_current_resource - result = @provider.diff_current_from_content "this is a line that is longer than 5 characters" - result.should == [ "(file sizes exceed 5 bytes, diff output suppressed)" ] - @resource.diff.should be_nil + context "when the file exists" do + before { setup_normal_file } + it "should update the atime/mtime on action_touch" do + File.should_receive(:utime).once + provider.should_receive(:action_create) + provider.run_action(:touch) + resource.should be_updated_by_last_action end end + end - it "should not produce a diff when the generated diff size is above the diff size threshold" do - Chef::Config[:diff_output_threshold] = 5 - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - file.puts("some text to increase the size of the diff") - file.flush - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.load_current_resource - result = @provider.diff_current_from_content "this is a line that is longer than 5 characters" - result.should == [ "(long diff of over 5 characters, diff output suppressed)" ] - @resource.diff.should be_nil + context "action create_if_missing" do + context "when the file does not exist" do + before { setup_missing_file } + it "should call action_create" do + provider.should_receive(:action_create) + provider.run_action(:create_if_missing) end end - it "should return valid diff output when content does not match the string content provided" do - Tempfile.open("some-temp") do |file| - @resource.path file.path - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.load_current_resource - result = @provider.diff_current_from_content "foo baz" - # remove the file name info which varies. - result.shift(2) - # Result appearance seems to vary slightly under solaris diff - # So we'll compare the second line which is common to both. - # Solaris: -1,1 +1,0 @@, "+foo baz" - # Linux/Mac: -1,0, +1 @@, "+foo baz" - result.length.should == 2 - result[1].should == "+foo baz" - @resource.diff.should_not be_nil - end + context "when the file exists" do + before { setup_normal_file } + it "should not call action_create" do + provider.should_not_receive(:action_create) + provider.run_action(:create_if_missing) + end end + end + end + diff --git a/spec/unit/util/backup_spec.rb b/spec/unit/util/backup_spec.rb new file mode 100644 index 0000000000..e08d5c846f --- /dev/null +++ b/spec/unit/util/backup_spec.rb @@ -0,0 +1,151 @@ +# +# Author:: Lamont Granquist (<lamont@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +require 'spec_helper' +require 'tmpdir' + +describe Chef::Util::Backup do + before(:all) do + @original_config = Chef::Config.configuration + end + + after(:all) do + Chef::Config.configuration.replace(@original_config) + end + + let (:tempfile) do + Tempfile.new("chef-util-backup-spec-test") + end + + before(:each) do + @new_resource = mock("new_resource") + @new_resource.should_receive(:path).at_least(:once).and_return(tempfile.path) + @backup = Chef::Util::Backup.new(@new_resource) + end + + it "should store the resource passed to new as new_resource" do + @backup.new_resource.should eql(@new_resource) + end + + describe "for cases when we don't want to back anything up" do + + before(:each) do + @backup.should_not_receive(:do_backup) + end + + it "should not attempt to backup a file if :backup is false" do + @new_resource.should_receive(:backup).at_least(:once).and_return(false) + @backup.backup! + end + + it "should not attempt to backup a file if :backup == 0" do + @new_resource.should_receive(:backup).at_least(:once).and_return(0) + @backup.backup! + end + + it "should not attempt to backup a file if it does not exist" do + @new_resource.should_receive(:backup).at_least(:once).and_return(1) + File.should_receive(:exist?).with(tempfile.path).at_least(:once).and_return(false) + @backup.backup! + end + + end + + describe "for cases when we want to back things up" do + before(:each) do + @backup.should_receive(:do_backup) + end + + describe "when the number of backups is specified as 1" do + before(:each) do + @new_resource.should_receive(:backup).at_least(:once).and_return(1) + end + + it "should not delete anything if this is the only backup" do + @backup.should_receive(:sorted_backup_files).and_return(['a']) + @backup.should_not_receive(:delete_backup) + @backup.backup! + end + + it "should keep only 1 backup copy" do + @backup.should_receive(:sorted_backup_files).and_return(['a', 'b', 'c']) + @backup.should_receive(:delete_backup).with('b') + @backup.should_receive(:delete_backup).with('c') + @backup.backup! + end + end + + describe "when the number of backups is specified as 2" do + before(:each) do + @new_resource.should_receive(:backup).at_least(:once).and_return(2) + end + + it "should not delete anything if we only have one other backup" do + @backup.should_receive(:sorted_backup_files).and_return(['a', 'b']) + @backup.should_not_receive(:delete_backup) + @backup.backup! + end + + it "should keep only 2 backup copies" do + @backup.should_receive(:sorted_backup_files).and_return(['a', 'b', 'c', 'd']) + @backup.should_receive(:delete_backup).with('c') + @backup.should_receive(:delete_backup).with('d') + @backup.backup! + end + end + end + + describe "backup_filename" do + it "should return a timestamped path" do + @backup.should_receive(:path).and_return('/a/b/c.txt') + @backup.send(:backup_filename).should =~ %r|^/a/b/c.txt.chef-\d{14}$| + end + it "should strip the drive letter off for windows" do + @backup.should_receive(:path).and_return('c:\a\b\c.txt') + @backup.send(:backup_filename).should =~ %r|^\\a\\b\\c.txt.chef-\d{14}$| + end + it "should strip the drive letter off for windows (with forwardslashes)" do + @backup.should_receive(:path).and_return('c:/a/b/c.txt') + @backup.send(:backup_filename).should =~ %r|^/a/b/c.txt.chef-\d{14}$| + end + end + + describe "backup_path" do + it "uses the file's directory when Chef::Config[:file_backup_path] is nil" do + @backup.should_receive(:path).and_return('/a/b/c.txt') + Chef::Config[:file_backup_path] = nil + @backup.send(:backup_path).should =~ %r|^/a/b/c.txt.chef-\d{14}$| + end + + it "uses the configured Chef::Config[:file_backup_path]" do + @backup.should_receive(:path).and_return('/a/b/c.txt') + Chef::Config[:file_backup_path] = '/backupdir' + @backup.send(:backup_path).should =~ %r|^/backupdir[\\/]+a/b/c.txt.chef-\d{14}$| + end + + it "uses the configured Chef::Config[:file_backup_path] and strips the drive on windows" do + @backup.should_receive(:path).and_return('c:\\a\\b\\c.txt') + Chef::Config[:file_backup_path] = 'c:\backupdir' + @backup.send(:backup_path).should =~ %r|^c:\\backupdir[\\/]+a\\b\\c.txt.chef-\d{14}$| + end + end + + # it "should keep the same ownership on backed up files" do (FIXME: functional test) + +end diff --git a/spec/unit/util/diff_spec.rb b/spec/unit/util/diff_spec.rb new file mode 100644 index 0000000000..ffb0cd6455 --- /dev/null +++ b/spec/unit/util/diff_spec.rb @@ -0,0 +1,254 @@ +# +# Author:: Lamont Granquist (<lamont@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +require 'spec_helper' +require 'tmpdir' + +describe Chef::Util::Diff, :uses_diff => true do + before(:all) do + @original_config = Chef::Config.hash_dup + end + + after(:all) do + Chef::Config.configuration = @original_config if @original_config + end + + let!(:old_tempfile) { Tempfile.new("chef-util-diff-spec") } + let!(:new_tempfile) { Tempfile.new("chef-util-diff-spec") } + let!(:old_file) { old_tempfile.path } + let!(:new_file) { new_tempfile.path } + + let(:differ) do # subject + differ = Chef::Util::Diff.new + differ.diff(old_file, new_file) + differ + end + + it "should return a Chef::Util::Diff" do + expect(differ).to be_a_kind_of(Chef::Util::Diff) + end + + it "should raise an exception if the old_file does not exist" do + old_tempfile.unlink + expect { differ.diff(old_file, new_file) }.to raise_error + end + + it "should raise an exception if the new_file does not exist" do + new_tempfile.unlink + expect { differ.diff(old_file, new_file) }.to raise_error + end + + describe "when the two files exist with no content" do + it "calling for_output should return the error message" do + expect(differ.for_output).to eql(["(no diff)"]) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + + describe "when diffs are disabled" do + before do + Chef::Config[:diff_disabled] = true + end + + after do + Chef::Config[:diff_disabled] = false + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql( [ "(diff output suppressed by config)" ] ) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + + describe "when the old_file has binary content" do + before do + old_tempfile.write("\x01\xff") + old_tempfile.close + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql( [ "(current file is binary, diff output suppressed)" ] ) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + + describe "when the new_file has binary content" do + before do + new_tempfile.write("\x01\xff") + new_tempfile.close + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql( [ "(new content is binary, diff output suppressed)" ]) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + + describe "when testing the diff_filesize_threshold" do + before do + @diff_filesize_threshold_saved = Chef::Config[:diff_filesize_threshold] + Chef::Config[:diff_filesize_threshold] = 10 + end + + after do + Chef::Config[:diff_filesize_threshold] = @diff_filesize_threshold_saved + end + + describe "when the old_file goes over the threshold" do + before do + old_tempfile.write("But thats what you get when Wu-Tang raised you") + old_tempfile.close + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql( [ "(file sizes exceed 10 bytes, diff output suppressed)" ]) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + + describe "when the new_file goes over the threshold" do + before do + new_tempfile.write("But thats what you get when Wu-Tang raised you") + new_tempfile.close + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql( [ "(file sizes exceed 10 bytes, diff output suppressed)" ]) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + end + + describe "when generating a valid diff" do + before do + old_tempfile.write("foo") + old_tempfile.close + new_tempfile.write("bar") + new_tempfile.close + end + + it "calling for_output should return a unified diff" do + differ.for_output.size.should eql(5) + differ.for_output.join("\\n").should match(/^--- .*\\n\+\+\+ .*\\n@@ .* @@\\n-foo\\n\+bar$/) + end + + it "calling for_reporting should return a unified diff" do + differ.for_reporting.should match(/^--- .*\\n\+\+\+ .*\\n@@ .* @@\\n-foo\\n\+bar$/) + end + + describe "when the diff output is too long" do + + before do + @diff_output_threshold_saved = Chef::Config[:diff_output_threshold] + Chef::Config[:diff_output_threshold] = 10 + end + + after do + Chef::Config[:diff_output_threshold] = @diff_output_threshold_saved + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql(["(long diff of over 10 characters, diff output suppressed)"]) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + end + + describe "when errors are thrown from shell_out" do + before do + differ.stub!(:shell_out).and_raise('boom') + differ.diff(old_file, new_file) + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql(["Could not determine diff. Error: boom"]) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + + describe "when shell_out returns stderr output" do + before do + @result = mock('result', :stdout => "", :stderr => "boom") + differ.stub!(:shell_out).and_return(@result) + differ.diff(old_file, new_file) + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql(["Could not determine diff. Error: boom"]) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + + describe "when checking if files are binary or text" do + + it "should identify zero-length files as text" do + Tempfile.new("chef-util-diff-spec") do |file| + differ.is_binary?(file.path).should be_false + end + end + + it "should identify text files as text" do + Tempfile.new("chef-util-diff-spec") do |file| + file.write("This is a text file.") + file.write("With more than one line.") + file.write("And lets make sure that other printable chars work too: ~!@\#$%^&*()`:\"<>?{}|_+,./;'[]\\-=") + file.close + differ.is_binary?(file.path).should be_false + end + end + + it "should identify a null-terminated string files as binary" do + Tempfile.new("chef-util-diff-spec") do |file| + file.write("This is a binary file.\0") + file.close + differ.is_binary?(file.path).should be_false + end + end + + end + +end + |