diff options
Diffstat (limited to 'spec/unit/provider/file_spec.rb')
-rw-r--r-- | spec/unit/provider/file_spec.rb | 498 |
1 files changed, 498 insertions, 0 deletions
diff --git a/spec/unit/provider/file_spec.rb b/spec/unit/provider/file_spec.rb new file mode 100644 index 0000000000..13b79e4bd6 --- /dev/null +++ b/spec/unit/provider/file_spec.rb @@ -0,0 +1,498 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +require 'spec_helper' +require '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"))) + + @provider = Chef::Provider::File.new(@resource, @run_context) + end + + it "should return a Chef::Provider::File" do + @provider.should be_a_kind_of(Chef::Provider::File) + end + + it "should store the resource passed to new as new_resource" do + @provider.new_resource.should eql(@resource) + end + + it "should store the node passed to new as node" do + @provider.node.should eql(@node) + 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) + 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(3).with(@resource.path).and_return(stat_struct) + + # test execution + @provider.load_current_resource + + # post-condition checks + @provider.current_resource.mode.should == 0600 + @provider.current_resource.owner.should == 0 + @provider.current_resource.group.should == 0 + 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(3).with(@resource.path).and_return(stat_struct) + + @provider.new_resource.group(1) + @provider.new_resource.owner(1) + @provider.new_resource.mode(0644) + + # test execution + @provider.load_current_resource + + # post-condition checks + @provider.new_resource.group.should == 1 + @provider.new_resource.owner.should == 1 + @provider.new_resource.mode.should == 0644 + end + + it "should update the new_resource state with the current_resource state if the new_resource state is not specified." do + # test setup + stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000) + ::File.should_receive(:stat).exactly(3).with(@resource.path).and_return(stat_struct) + + @provider.new_resource.group(nil) + @provider.new_resource.owner(nil) + @provider.new_resource.mode(nil) + + # test execution + @provider.load_current_resource + + # post-condition checks + @provider.new_resource.group.should eql(@provider.current_resource.group) + @provider.new_resource.owner.should eql(@provider.current_resource.owner) + @provider.new_resource.mode.should eql(@provider.current_resource.mode) + end + + it "should update the new_resource when attempting to set the new state" 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).twice.with(@provider.new_resource.path).and_return(stat_struct) + ::File.should_receive(:directory?).once.with(@provider.new_resource.path).and_return(false) + + @provider.new_resource.group(nil) + @provider.new_resource.owner(nil) + @provider.new_resource.mode(nil) + + # test exectution + @provider.update_new_file_state + + # post-condition checks + @provider.new_resource.group.should == 0 + @provider.new_resource.owner.should == 0 + @provider.new_resource.mode.should == 0600 +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 + + 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 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) + File.should_receive(:open).with(@provider.new_resource.path, "w").and_yield(io) + @provider.set_content + lambda { @provider.send(:converge_actions).converge! }.should_not raise_error + 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) + File.stub!(:open).and_return(1) + 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 + + 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 + + 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 + + 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 + + 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 "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 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 + + 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 "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 + 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 + + describe "when the enclosing directory does not exist" do + before do + @resource.path("/tmp/no-such-path/file.txt") + end + + it "raises a specific error describing the problem" do + lambda {@provider.run_action(:create)}.should raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + 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 + end + + it "should call action create if the does not file exist" do + @resource.path("/tmp/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) + @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 + end + end + + describe "when a diff is requested" do + + before(:each) do + @original_config = Chef::Config.hash_dup + end + + after(:each) do + Chef::Config.configuration = @original_config if @original_config + end + + describe "when identifying files as binary or text" do + + 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 + 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 + end + 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 + end + 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 + 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 + 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 + 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 + 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 + 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 + 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 + end + end +end |