diff options
author | Seth Chisamore <schisamo@opscode.com> | 2012-10-30 10:39:35 -0400 |
---|---|---|
committer | Seth Chisamore <schisamo@opscode.com> | 2012-10-30 10:39:35 -0400 |
commit | 24dc69a9a97e82a6e4207de68d6dcc664178249b (patch) | |
tree | 19bb289c9f88b4bbab066bc56b95d6d222fd5c35 /spec/unit/provider | |
parent | 9348c1c9c80ee757354d624b7dc1b78ebc7605c4 (diff) | |
download | chef-24dc69a9a97e82a6e4207de68d6dcc664178249b.tar.gz |
[OC-3564] move core Chef to the repo root \o/ \m/
The opscode/chef repository now only contains the core Chef library code
used by chef-client, knife and chef-solo!
Diffstat (limited to 'spec/unit/provider')
72 files changed, 17944 insertions, 0 deletions
diff --git a/spec/unit/provider/breakpoint_spec.rb b/spec/unit/provider/breakpoint_spec.rb new file mode 100644 index 0000000000..977624597a --- /dev/null +++ b/spec/unit/provider/breakpoint_spec.rb @@ -0,0 +1,54 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.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' +describe Chef::Provider::Breakpoint do + + before do + @resource = Chef::Resource::Breakpoint.new + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @collection = mock("resource collection") + @run_context.stub!(:resource_collection).and_return(@collection) + @provider = Chef::Provider::Breakpoint.new(@resource, @run_context) + end + + it "responds to load_current_resource" do + @provider.should respond_to(:load_current_resource) + end + + it "gets the iterator from @collection and pauses it" do + Shell.stub!(:running?).and_return(true) + @iterator = mock("stepable_iterator") + @collection.stub!(:iterator).and_return(@iterator) + @iterator.should_receive(:pause) + @provider.action_break + @resource.should be_updated + end + + it "doesn't pause the iterator if chef-shell isn't running" do + Shell.stub!(:running?).and_return(false) + @iterator = mock("stepable_iterator") + @collection.stub!(:iterator).and_return(@iterator) + @iterator.should_not_receive(:pause) + @provider.action_break + end + +end diff --git a/spec/unit/provider/cookbook_file_spec.rb b/spec/unit/provider/cookbook_file_spec.rb new file mode 100644 index 0000000000..c70a7d852c --- /dev/null +++ b/spec/unit/provider/cookbook_file_spec.rb @@ -0,0 +1,220 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2010 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 'ostruct' + +describe Chef::Provider::CookbookFile do + before do + Chef::FileAccessControl.any_instance.stub(:set_all) + Chef::FileAccessControl.any_instance.stub(:modified?).and_return(true) + @cookbook_repo = File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks")) + Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest, @cookbook_repo) } + + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + cl = Chef::CookbookLoader.new(@cookbook_repo) + cl.load_cookbooks + @cookbook_collection = Chef::CookbookCollection.new(cl) + @run_context = Chef::RunContext.new(@node, @cookbook_collection, @events) + + @new_resource = Chef::Resource::CookbookFile.new('apache2_module_conf_generate.pl', @run_context) + @new_resource.cookbook_name = 'apache2' + @provider = Chef::Provider::CookbookFile.new(@new_resource, @run_context) + + @file_content=<<-EXPECTED +# apache2_module_conf_generate.pl +# this is just here for show. +EXPECTED + + end + + it "prefers the explicit cookbook name on the resource to the implicit one" do + @new_resource.cookbook('nginx') + @provider.resource_cookbook.should == 'nginx' + end + + it "falls back to the implicit cookbook name on the resource" do + @provider.resource_cookbook.should == 'apache2' + end + + describe "when loading the current file state" do + + it "converts windows-y filenames to unix-y ones" do + @new_resource.path('windows\stuff') + @provider.load_current_resource + @new_resource.path.should == 'windows/stuff' + end + + it "sets the current resources path to the same as the new resource" do + @new_resource.path('/tmp/file') + @provider.load_current_resource + @provider.current_resource.path.should == '/tmp/file' + end + end + + describe "when the enclosing directory of the target file location doesn't exist" do + before do + @new_resource.path("/tmp/no/such/intermediate/path/file.txt") + end + + it "raises a specific error alerting the user to the problem" do + lambda {@provider.run_action(:create)}.should raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + end + end + describe "when the file doesn't yet exist" do + before do + @install_to = Dir.tmpdir + '/apache2_modconf.pl' + + @current_resource = @new_resource.dup + @provider.current_resource = @current_resource + end + + after { ::File.exist?(@install_to) && FileUtils.rm(@install_to) } + + it "loads the current file state" do + @provider.load_current_resource + @provider.current_resource.checksum.should be_nil + end + + it "looks up a file from the cookbook cache" do + expected = CHEF_SPEC_DATA + "/cookbooks/apache2/files/default/apache2_module_conf_generate.pl" + @provider.file_cache_location.should == expected + end + + it "stages the cookbook to a temporary file" do + @new_resource.path(@install_to) + @provider.should_receive(:deploy_tempfile) + @provider.run_action(:create) + end + + it "installs the file from the cookbook cache" do + @new_resource.path(@install_to) + @provider.should_receive(:backup_new_resource) + @provider.stub!(:update_new_file_state) + @provider.run_action(:create) + actual = IO.read(@install_to) + actual.should == @file_content + end + + it "installs the file for create_if_missing --> from Provider::File" do + @new_resource.path(@install_to) + @provider.should_receive(:backup_new_resource) + @provider.stub!(:update_new_file_state) + @provider.run_action(:create_if_missing) + actual = IO.read(@install_to) + actual.should == @file_content + end + + it "marks the resource as updated by the last action --> being tested in the converge framework" do + @new_resource.path(@install_to) + @provider.stub!(:backup_new_resource) + @provider.stub!(:set_file_access_controls) + @provider.stub!(:update_new_file_state) + @provider.run_action(:create) + @new_resource.should be_updated + @new_resource.should be_updated_by_last_action + end + + end + + describe "when the file exists but has incorrect content" do + before do + @tempfile = Tempfile.open('cookbook_file_spec') + @new_resource.path(@target_file = @tempfile.path) + @tempfile.puts "the wrong content" + @tempfile.close + @current_resource = @new_resource.dup + @provider.current_resource = @current_resource + end + + it "stages the cookbook to a temporary file" do + # prevents file backups where we might not have write access + @provider.should_receive(:backup_new_resource) + @new_resource.path(@install_to) + @provider.should_receive(:deploy_tempfile) + @provider.run_action(:create) + end + + it "overwrites it when the create action is called" do + @provider.should_receive(:backup_new_resource) + @provider.run_action(:create) + actual = IO.read(@target_file) + actual.should == @file_content + end + + it "marks the resource as updated by the last action" do + @provider.should_receive(:backup_new_resource) + @provider.run_action(:create) + @new_resource.should be_updated + @new_resource.should be_updated_by_last_action + end + + it "doesn't overwrite when the create if missing action is called" do + @provider.should_not_receive(:set_file_access_controls) + @provider.run_action(:create_if_missing) + actual = IO.read(@target_file) + actual.should == "the wrong content\n" + end + + it "doesn't mark the resource as updated by the action for create_if_missing" do + @provider.run_action(:create_if_missing) + @new_resource.should_not be_updated + @new_resource.should_not be_updated_by_last_action + end + + after { @tempfile && @tempfile.close! } + end + + describe "when the file has the correct content" do + before do + Chef::FileAccessControl.any_instance.stub(:modified?).and_return(false) + @tempfile = Tempfile.open('cookbook_file_spec') + # CHEF-2991: We handle CRLF very poorly and we don't know what line endings + # our source file is going to have, so we use binary mode to preserve CRLF if needed. + source_file = CHEF_SPEC_DATA + "/cookbooks/apache2/files/default/apache2_module_conf_generate.pl" + @tempfile.binmode unless File.open(source_file, "rb") { |f| f.read =~ /\r/ } + @new_resource.path(@target_file = @tempfile.path) + @tempfile.write(@file_content) + @tempfile.close + @current_resource = @new_resource.dup + @provider.current_resource = @current_resource + end + + after { @tempfile && @tempfile.unlink} + + it "checks access control but does not alter content when action is create" do + @provider.should_receive(:set_all_access_controls) + @provider.should_not_receive(:stage_file_to_tmpdir) + @provider.run_action(:create) + end + + it "does not mark the resource as updated by the last action" do + @provider.run_action(:create) + @new_resource.should_not be_updated + @new_resource.should_not be_updated_by_last_action + end + + it "does not alter content or access control when action is create if missing" do + @provider.should_not_receive(:set_all_access_controls) + @provider.should_not_receive(:stage_file_to_tmpdir) + @provider.run_action(:create_if_missing) + end + + end +end diff --git a/spec/unit/provider/cron/solaris_spec.rb b/spec/unit/provider/cron/solaris_spec.rb new file mode 100644 index 0000000000..55516d59e9 --- /dev/null +++ b/spec/unit/provider/cron/solaris_spec.rb @@ -0,0 +1,121 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org) +# Author:: Toomas Pelberg (toomasp@gmx.net) +# Copyright:: Copyright (c) 2009 Bryan McLellan +# Copyright:: Copyright (c) 2010 Toomas Pelberg +# 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::Provider::Cron::Solaris do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Cron.new("cronhole some stuff") + @new_resource.user "root" + @new_resource.minute "30" + @new_resource.command "/bin/true" + + @provider = Chef::Provider::Cron::Solaris.new(@new_resource, @run_context) + end + + it "should inherit from Chef::Provider:Cron" do + @provider.should be_a(Chef::Provider::Cron) + end + + describe "read_crontab" do + before :each do + @status = mock("Status", :exitstatus => 0) + @stdout = StringIO.new(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: something else +* 5 * * * /bin/true + +# Another comment + CRONTAB + @provider.stub!(:popen4).and_yield(1234, StringIO.new, @stdout, StringIO.new).and_return(@status) + end + + it "should call crontab -l with the user" do + @provider.should_receive(:popen4).with("crontab -l #{@new_resource.user}").and_return(@status) + @provider.send(:read_crontab) + end + + it "should return the contents of the crontab" do + crontab = @provider.send(:read_crontab) + crontab.should == <<-CRONTAB +0 2 * * * /some/other/command + +# Chef Name: something else +* 5 * * * /bin/true + +# Another comment +CRONTAB + end + + it "should return nil if the user has no crontab" do + status = mock("Status", :exitstatus => 1) + @provider.stub!(:popen4).and_return(status) + @provider.send(:read_crontab).should == nil + end + + it "should raise an exception if another error occurs" do + status = mock("Status", :exitstatus => 2) + @provider.stub!(:popen4).and_return(status) + lambda do + @provider.send(:read_crontab) + end.should raise_error(Chef::Exceptions::Cron, "Error determining state of #{@new_resource.name}, exit: 2") + end + end + + describe "write_crontab" do + before :each do + @status = mock("Status", :exitstatus => 0) + @provider.stub!(:run_command).and_return(@status) + @tempfile = mock("foo", :path => "/tmp/foo", :close => true, :binmode => nil) + Tempfile.stub!(:new).and_return(@tempfile) + @tempfile.should_receive(:flush) + @tempfile.should_receive(:chmod).with(420) + @tempfile.should_receive(:close!) + end + + it "should call crontab for the user" do + @provider.should_receive(:run_command).with(hash_including(:user => @new_resource.user)) + @tempfile.should_receive(:<<).with("Foo") + @provider.send(:write_crontab, "Foo") + end + + it "should call crontab with a file containing the crontab" do + @provider.should_receive(:run_command) do |args| + (args[:command] =~ %r{\A/usr/bin/crontab (/\S+)\z}).should be_true + $1.should == "/tmp/foo" + @status + end + @tempfile.should_receive(:<<).with("Foo\n# wibble\n wah!!") + @provider.send(:write_crontab, "Foo\n# wibble\n wah!!") + end + + it "should raise an exception if the command returns non-zero" do + @tempfile.should_receive(:<<).with("Foo") + @status.stub!(:exitstatus).and_return(1) + lambda do + @provider.send(:write_crontab, "Foo") + end.should raise_error(Chef::Exceptions::Cron, "Error updating state of #{@new_resource.name}, exit: 1") + end + end +end diff --git a/spec/unit/provider/cron_spec.rb b/spec/unit/provider/cron_spec.rb new file mode 100644 index 0000000000..5a848a30e6 --- /dev/null +++ b/spec/unit/provider/cron_spec.rb @@ -0,0 +1,812 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org) +# Copyright:: Copyright (c) 2009 Bryan McLellan +# 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::Provider::Cron do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Cron.new("cronhole some stuff", @run_context) + @new_resource.user "root" + @new_resource.minute "30" + @new_resource.command "/bin/true" + + @provider = Chef::Provider::Cron.new(@new_resource, @run_context) + end + + describe "when examining the current system state" do + context "with no crontab for the user" do + before :each do + @provider.stub!(:read_crontab).and_return(nil) + end + + it "should set cron_empty" do + @provider.load_current_resource + @provider.cron_empty.should == true + @provider.cron_exists.should == false + end + + it "should report an empty crontab" do + Chef::Log.should_receive(:debug).with("Cron empty for '#{@new_resource.user}'") + @provider.load_current_resource + end + end + + context "with no matching entry in the user's crontab" do + before :each do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: something else +* 5 * * * /bin/true + +# Another comment +CRONTAB + end + + it "should not set cron_exists or cron_empty" do + @provider.load_current_resource + @provider.cron_exists.should == false + @provider.cron_empty.should == false + end + + it "should report no entry found" do + Chef::Log.should_receive(:debug).with("Cron '#{@new_resource.name}' not found") + @provider.load_current_resource + end + + it "should not fail if there's an existing cron with a numerical argument" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +# Chef Name: foo[bar] (baz) +21 */4 * * * some_prog 1234567 +CRONTAB + lambda { + @provider.load_current_resource + }.should_not raise_error + end + end + + context "with a matching entry in the user's crontab" do + before :each do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +* 5 * 1 * /bin/true param1 param2 +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment +CRONTAB + end + + it "should set cron_exists" do + @provider.load_current_resource + @provider.cron_exists.should == true + @provider.cron_empty.should == false + end + + it "should pull the details out of the cron line" do + cron = @provider.load_current_resource + cron.minute.should == '*' + cron.hour.should == '5' + cron.day.should == '*' + cron.month.should == '1' + cron.weekday.should == '*' + cron.command.should == '/bin/true param1 param2' + end + + it "should pull env vars out" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +MAILTO=foo@example.com +SHELL=/bin/foosh +PATH=/bin:/foo +HOME=/home/foo +* 5 * 1 * /bin/true param1 param2 +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment +CRONTAB + cron = @provider.load_current_resource + cron.mailto.should == 'foo@example.com' + cron.shell.should == '/bin/foosh' + cron.path.should == '/bin:/foo' + cron.home.should == '/home/foo' + cron.minute.should == '*' + cron.hour.should == '5' + cron.day.should == '*' + cron.month.should == '1' + cron.weekday.should == '*' + cron.command.should == '/bin/true param1 param2' + end + + it "should parse and load generic and standard environment variables from cron entry" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +# Chef Name: cronhole some stuff +MAILTO=warn@example.com +TEST=lol +FLAG=1 +* 5 * * * /bin/true +CRONTAB + cron = @provider.load_current_resource + + cron.mailto.should == "warn@example.com" + cron.environment.should == {"TEST" => "lol", "FLAG" => "1"} + end + + it "should not break with variabels that match the cron resource internals" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +# Chef Name: cronhole some stuff +MINUTE=40 +HOUR=midnight +TEST=lol +ENVIRONMENT=production +* 5 * * * /bin/true +CRONTAB + cron = @provider.load_current_resource + + cron.minute.should == '*' + cron.hour.should == '5' + cron.environment.should == {"MINUTE" => "40", "HOUR" => "midnight", "TEST" => "lol", "ENVIRONMENT" => "production"} + end + + it "should report the match" do + Chef::Log.should_receive(:debug).with("Found cron '#{@new_resource.name}'") + @provider.load_current_resource + end + end + + context "with a matching entry in the user's crontab using month names and weekday names (#CHEF-3178)" do + before :each do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +* 5 * Jan Mon /bin/true param1 param2 +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment +CRONTAB + end + + it "should set cron_exists" do + @provider.load_current_resource + @provider.cron_exists.should == true + @provider.cron_empty.should == false + end + + it "should pull the details out of the cron line" do + cron = @provider.load_current_resource + cron.minute.should == '*' + cron.hour.should == '5' + cron.day.should == '*' + cron.month.should == 'Jan' + cron.weekday.should == 'Mon' + cron.command.should == '/bin/true param1 param2' + end + + it "should report the match" do + Chef::Log.should_receive(:debug).with("Found cron '#{@new_resource.name}'") + @provider.load_current_resource + end + end + + context "with a matching entry without a crontab line" do + it "should set cron_exists and leave current_resource values at defaults" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +CRONTAB + cron = @provider.load_current_resource + @provider.cron_exists.should == true + cron.minute.should == '*' + cron.hour.should == '*' + cron.day.should == '*' + cron.month.should == '*' + cron.weekday.should == '*' + cron.command.should == nil + end + + it "should not pick up a commented out crontab line" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +#* 5 * 1 * /bin/true param1 param2 +CRONTAB + cron = @provider.load_current_resource + @provider.cron_exists.should == true + cron.minute.should == '*' + cron.hour.should == '*' + cron.day.should == '*' + cron.month.should == '*' + cron.weekday.should == '*' + cron.command.should == nil + end + + it "should not pick up a later crontab entry" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +#* 5 * 1 * /bin/true param1 param2 +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment +CRONTAB + cron = @provider.load_current_resource + @provider.cron_exists.should == true + cron.minute.should == '*' + cron.hour.should == '*' + cron.day.should == '*' + cron.month.should == '*' + cron.weekday.should == '*' + cron.command.should == nil + end + end + end + + describe "cron_different?" do + before :each do + @current_resource = Chef::Resource::Cron.new("cronhole some stuff") + @current_resource.user "root" + @current_resource.minute "30" + @current_resource.command "/bin/true" + @provider.current_resource = @current_resource + end + + [:minute, :hour, :day, :month, :weekday, :command, :mailto, :path, :shell, :home].each do |attribute| + it "should return true if #{attribute} doesn't match" do + @new_resource.send(attribute, "something_else") + @provider.cron_different?.should eql(true) + end + end + + it "should return true if environment doesn't match" do + @new_resource.environment "FOO" => "something_else" + @provider.cron_different?.should eql(true) + end + + it "should return false if the objects are identical" do + @provider.cron_different?.should == false + end + end + + describe "action_create" do + before :each do + @provider.stub!(:write_crontab) + @provider.stub!(:read_crontab).and_return(nil) + end + + context "when there is no existing crontab" do + before :each do + @provider.cron_exists = false + @provider.cron_empty = true + end + + it "should create a crontab with the entry" do + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +# Chef Name: cronhole some stuff +30 * * * * /bin/true + ENDCRON + @provider.run_action(:create) + end + + it "should include env variables that are set" do + @new_resource.mailto 'foo@example.com' + @new_resource.path '/usr/bin:/my/custom/path' + @new_resource.shell '/bin/foosh' + @new_resource.home '/home/foo' + @new_resource.environment "TEST" => "LOL" + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +# Chef Name: cronhole some stuff +MAILTO=foo@example.com +PATH=/usr/bin:/my/custom/path +SHELL=/bin/foosh +HOME=/home/foo +TEST=LOL +30 * * * * /bin/true + ENDCRON + @provider.run_action(:create) + end + + it "should mark the resource as updated" do + @provider.run_action(:create) + @new_resource.should be_updated_by_last_action + end + + it "should log the action" do + Chef::Log.should_receive(:info).with("cron[cronhole some stuff] added crontab entry") + @provider.run_action(:create) + end + end + + context "when there is a crontab with no matching section" do + before :each do + @provider.cron_exists = false + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + CRONTAB + end + + it "should add the entry to the crontab" do + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +0 2 * * * /some/other/command + +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment +# Chef Name: cronhole some stuff +30 * * * * /bin/true + ENDCRON + @provider.run_action(:create) + end + + it "should include env variables that are set" do + @new_resource.mailto 'foo@example.com' + @new_resource.path '/usr/bin:/my/custom/path' + @new_resource.shell '/bin/foosh' + @new_resource.home '/home/foo' + @new_resource.environment "TEST" => "LOL" + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +0 2 * * * /some/other/command + +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment +# Chef Name: cronhole some stuff +MAILTO=foo@example.com +PATH=/usr/bin:/my/custom/path +SHELL=/bin/foosh +HOME=/home/foo +TEST=LOL +30 * * * * /bin/true + ENDCRON + @provider.run_action(:create) + end + + it "should mark the resource as updated" do + @provider.run_action(:create) + @new_resource.should be_updated_by_last_action + end + + it "should log the action" do + Chef::Log.should_receive(:info).with("cron[cronhole some stuff] added crontab entry") + @provider.run_action(:create) + end + end + + context "when there is a crontab with a matching but different section" do + before :each do + @provider.cron_exists = true + @provider.stub!(:cron_different?).and_return(true) + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +30 * * 3 * /bin/true +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + CRONTAB + end + + it "should update the crontab entry" do + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +30 * * * * /bin/true +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + ENDCRON + @provider.run_action(:create) + end + + it "should include env variables that are set" do + @new_resource.mailto 'foo@example.com' + @new_resource.path '/usr/bin:/my/custom/path' + @new_resource.shell '/bin/foosh' + @new_resource.home '/home/foo' + @new_resource.environment "TEST" => "LOL" + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +MAILTO=foo@example.com +PATH=/usr/bin:/my/custom/path +SHELL=/bin/foosh +HOME=/home/foo +TEST=LOL +30 * * * * /bin/true +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + ENDCRON + @provider.run_action(:create) + end + + it "should mark the resource as updated" do + @provider.run_action(:create) + @new_resource.should be_updated_by_last_action + end + + it "should log the action" do + Chef::Log.should_receive(:info).with("cron[cronhole some stuff] updated crontab entry") + @provider.run_action(:create) + end + end + + context "when there is a crontab with a matching section with no crontab line in it" do + before :each do + @provider.cron_exists = true + @provider.stub!(:cron_different?).and_return(true) + end + + it "should add the crontab to the entry" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff + CRONTAB + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +30 * * * * /bin/true + ENDCRON + @provider.run_action(:create) + end + + it "should not blat any following entries" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +#30 * * * * /bin/true +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + CRONTAB + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +30 * * * * /bin/true +#30 * * * * /bin/true +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + ENDCRON + @provider.run_action(:create) + end + + it "should handle env vars with no crontab" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +MAILTO=bar@example.com +PATH=/usr/bin:/my/custom/path +SHELL=/bin/barsh +HOME=/home/foo + +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + CRONTAB + @new_resource.mailto 'foo@example.com' + @new_resource.path '/usr/bin:/my/custom/path' + @new_resource.shell '/bin/foosh' + @new_resource.home '/home/foo' + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +MAILTO=foo@example.com +PATH=/usr/bin:/my/custom/path +SHELL=/bin/foosh +HOME=/home/foo +30 * * * * /bin/true + +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + ENDCRON + @provider.run_action(:create) + end + end + + context "when there is a crontab with a matching and identical section" do + before :each do + @provider.cron_exists = true + @provider.stub!(:cron_different?).and_return(false) + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: something else +* 5 * * * /bin/true + +# Another comment +CRONTAB + end + + it "should not update the crontab" do + @provider.should_not_receive(:write_crontab) + @provider.run_action(:create) + end + + it "should not mark the resource as updated" do + @provider.run_action(:create) + @new_resource.should_not be_updated_by_last_action + end + + it "should log nothing changed" do + Chef::Log.should_receive(:debug).with("Skipping existing cron entry '#{@new_resource.name}'") + @provider.run_action(:create) + end + end + end + + describe "action_delete" do + before :each do + @provider.stub!(:write_crontab) + @provider.stub!(:read_crontab).and_return(nil) + end + + context "when the user's crontab has no matching section" do + before :each do + @provider.cron_exists = false + end + + it "should do nothing" do + @provider.should_not_receive(:write_crontab) + Chef::Log.should_not_receive(:info) + @provider.run_action(:delete) + end + + it "should not mark the resource as updated" do + @provider.run_action(:delete) + @new_resource.should_not be_updated_by_last_action + end + end + + context "when the user has a crontab with a matching section" do + before :each do + @provider.cron_exists = true + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +30 * * 3 * /bin/true +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + CRONTAB + end + + it "should remove the entry" do + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +0 2 * * * /some/other/command + +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + ENDCRON + @provider.run_action(:delete) + end + + it "should remove any env vars with the entry" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +MAILTO=foo@example.com +FOO=test +30 * * 3 * /bin/true +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + CRONTAB + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +0 2 * * * /some/other/command + +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + ENDCRON + @provider.run_action(:delete) + end + + it "should mark the resource as updated" do + @provider.run_action(:delete) + @new_resource.should be_updated_by_last_action + end + + it "should log the action" do + Chef::Log.should_receive(:info).with("#{@new_resource} deleted crontab entry") + @provider.run_action(:delete) + end + end + + context "when the crontab has a matching section with no crontab line" do + before :each do + @provider.cron_exists = true + end + + it "should remove the section" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff + CRONTAB + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +0 2 * * * /some/other/command + + ENDCRON + @provider.run_action(:delete) + end + + it "should not blat following sections" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +#30 * * 3 * /bin/true +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + CRONTAB + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +0 2 * * * /some/other/command + +#30 * * 3 * /bin/true +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + ENDCRON + @provider.run_action(:delete) + end + + it "should remove any envvars with the section" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +MAILTO=foo@example.com +#30 * * 3 * /bin/true +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + CRONTAB + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +0 2 * * * /some/other/command + +#30 * * 3 * /bin/true +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment + ENDCRON + @provider.run_action(:delete) + end + end + end + + describe "read_crontab" do + before :each do + @status = mock("Status", :exitstatus => 0) + @stdout = StringIO.new(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: something else +* 5 * * * /bin/true + +# Another comment + CRONTAB + @provider.stub!(:popen4).and_yield(1234, StringIO.new, @stdout, StringIO.new).and_return(@status) + end + + it "should call crontab -l with the user" do + @provider.should_receive(:popen4).with("crontab -l -u #{@new_resource.user}").and_return(@status) + @provider.send(:read_crontab) + end + + it "should return the contents of the crontab" do + crontab = @provider.send(:read_crontab) + crontab.should == <<-CRONTAB +0 2 * * * /some/other/command + +# Chef Name: something else +* 5 * * * /bin/true + +# Another comment + CRONTAB + end + + it "should return nil if the user has no crontab" do + status = mock("Status", :exitstatus => 1) + @provider.stub!(:popen4).and_return(status) + @provider.send(:read_crontab).should == nil + end + + it "should raise an exception if another error occurs" do + status = mock("Status", :exitstatus => 2) + @provider.stub!(:popen4).and_return(status) + lambda do + @provider.send(:read_crontab) + end.should raise_error(Chef::Exceptions::Cron, "Error determining state of #{@new_resource.name}, exit: 2") + end + end + + describe "write_crontab" do + before :each do + @status = mock("Status", :exitstatus => 0) + @stdin = StringIO.new + @provider.stub!(:popen4).and_yield(1234, @stdin, StringIO.new, StringIO.new).and_return(@status) + end + + it "should call crontab for the user" do + @provider.should_receive(:popen4).with("crontab -u #{@new_resource.user} -", :waitlast => true).and_return(@status) + @provider.send(:write_crontab, "Foo") + end + + it "should write the given string to the crontab command" do + @provider.send(:write_crontab, "Foo\n# wibble\n wah!!") + @stdin.string.should == "Foo\n# wibble\n wah!!" + end + + it "should raise an exception if the command returns non-zero" do + @status.stub!(:exitstatus).and_return(1) + lambda do + @provider.send(:write_crontab, "Foo") + end.should raise_error(Chef::Exceptions::Cron, "Error updating state of #{@new_resource.name}, exit: 1") + end + end +end diff --git a/spec/unit/provider/deploy/revision_spec.rb b/spec/unit/provider/deploy/revision_spec.rb new file mode 100644 index 0000000000..396dd09a8e --- /dev/null +++ b/spec/unit/provider/deploy/revision_spec.rb @@ -0,0 +1,109 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.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' + +describe Chef::Provider::Deploy::Revision do + + before do + @temp_dir = Dir.mktmpdir + Chef::Config[:file_cache_path] = @temp_dir + @resource = Chef::Resource::Deploy.new("/my/deploy/dir") + @resource.revision("8a3195bf3efa246f743c5dfa83683201880f935c") + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @provider = Chef::Provider::Deploy::Revision.new(@resource, @run_context) + @provider.load_current_resource + @runner = mock("runnah") + Chef::Runner.stub!(:new).and_return(@runner) + @expected_release_dir = "/my/deploy/dir/releases/8a3195bf3efa246f743c5dfa83683201880f935c" + end + + after do + # Make sure we don't keep any state in our tests + FileUtils.rspec_reset + FileUtils.rm_rf @temp_dir if File.directory?( @temp_dir ) + end + + + it "uses the resolved revision from the SCM as the release slug" do + @provider.scm_provider.stub!(:revision_slug).and_return("uglySlugly") + @provider.send(:release_slug).should == "uglySlugly" + end + + it "deploys to a dir named after the revision" do + @provider.release_path.should == @expected_release_dir + end + + it "stores the release dir in the file cache when copying the cached repo" do + FileUtils.stub!(:mkdir_p) + @provider.stub!(:run_command).and_return(true) + @provider.copy_cached_repo + @provider.converge + @provider.stub!(:release_slug).and_return("73219b87e977d9c7ba1aa57e9ad1d88fa91a0ec2") + @provider.load_current_resource + @provider.copy_cached_repo + @provider.converge + second_release = "/my/deploy/dir/releases/73219b87e977d9c7ba1aa57e9ad1d88fa91a0ec2" + + @provider.all_releases.should == [@expected_release_dir,second_release] + end + + it "removes a release from the file cache when it's used again in another release and append it to the end" do + FileUtils.stub!(:mkdir_p) + @provider.stub!(:run_command).and_return(true) + @provider.copy_cached_repo + @provider.converge + @provider.stub!(:release_slug).and_return("73219b87e977d9c7ba1aa57e9ad1d88fa91a0ec2") + @provider.load_current_resource + @provider.copy_cached_repo + @provider.converge + second_release = "/my/deploy/dir/releases/73219b87e977d9c7ba1aa57e9ad1d88fa91a0ec2" + @provider.all_releases.should == [@expected_release_dir,second_release] + @provider.copy_cached_repo + @provider.converge + + @provider.stub!(:release_slug).and_return("8a3195bf3efa246f743c5dfa83683201880f935c") + @provider.load_current_resource + @provider.copy_cached_repo + @provider.converge + @provider.all_releases.should == [second_release, @expected_release_dir] + end + + it "removes a release from the file cache when it's deleted by :cleanup!" do + %w{first second third fourth fifth latest}.each do |release_name| + @provider.send(:release_created, release_name) + end + @provider.all_releases.should == %w{first second third fourth fifth latest} + + FileUtils.stub!(:rm_rf) + @provider.cleanup! + @provider.all_releases.should == %w{second third fourth fifth latest} + end + + it "regenerates the file cache if it's not available" do + oldest = "/my/deploy/dir/releases/oldest" + latest = "/my/deploy/dir/releases/latest" + Dir.should_receive(:glob).with("/my/deploy/dir/releases/*").and_return([latest, oldest]) + ::File.should_receive(:ctime).with(oldest).and_return(Time.now - 10) + ::File.should_receive(:ctime).with(latest).and_return(Time.now - 1) + @provider.all_releases.should == [oldest, latest] + end + +end diff --git a/spec/unit/provider/deploy/timestamped_spec.rb b/spec/unit/provider/deploy/timestamped_spec.rb new file mode 100644 index 0000000000..b891a03ce2 --- /dev/null +++ b/spec/unit/provider/deploy/timestamped_spec.rb @@ -0,0 +1,40 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.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' + +describe Chef::Provider::Deploy::Timestamped do + + before do + @release_time = Time.utc( 2004, 8, 15, 16, 23, 42) + Time.stub!(:now).and_return(@release_time) + @expected_release_dir = "/my/deploy/dir/releases/20040815162342" + @resource = Chef::Resource::Deploy.new("/my/deploy/dir") + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @timestamped_deploy = Chef::Provider::Deploy::Timestamped.new(@resource, @run_context) + @runner = mock("runnah") + Chef::Runner.stub!(:new).and_return(@runner) + end + + it "gives a timestamp for release_slug" do + @timestamped_deploy.send(:release_slug).should == "20040815162342" + end + +end diff --git a/spec/unit/provider/deploy_spec.rb b/spec/unit/provider/deploy_spec.rb new file mode 100644 index 0000000000..6bcd64fbfb --- /dev/null +++ b/spec/unit/provider/deploy_spec.rb @@ -0,0 +1,654 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.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' + +describe Chef::Provider::Deploy do + + before do + @release_time = Time.utc( 2004, 8, 15, 16, 23, 42) + Time.stub!(:now).and_return(@release_time) + @expected_release_dir = "/my/deploy/dir/releases/20040815162342" + @resource = Chef::Resource::Deploy.new("/my/deploy/dir") + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @provider = Chef::Provider::Deploy.new(@resource, @run_context) + @provider.stub!(:release_slug) + @provider.stub!(:release_path).and_return(@expected_release_dir) + end + + it "loads scm resource" do + @provider.scm_provider.should_receive(:load_current_resource) + @provider.load_current_resource + end + + it "supports :deploy and :rollback actions" do + @provider.should respond_to(:action_deploy) + @provider.should respond_to(:action_rollback) + end + + context "when the deploy_to dir does not exist yet" do + before do + FileUtils.should_receive(:mkdir_p).with(@resource.deploy_to).ordered + FileUtils.should_receive(:mkdir_p).with(@resource.shared_path).ordered + ::File.stub!(:directory?).and_return(false) + @provider.stub(:symlink) + @provider.stub(:migrate) + @provider.stub(:copy_cached_repo) + end + + it "creates deploy_to dir" do + @provider.stub(:update_cached_repo) + @provider.deploy + end + + it "creates deploy_to dir before calling update_cached_repo (CHEF-3435)" do + @provider.send(:converge_actions).should_receive(:empty?).and_return(false) + @provider.should_receive(:update_cached_repo).ordered + @provider.deploy + end + end + + it "does not create deploy_to dir if it exists" do + ::File.stub!(:directory?).and_return(true) + ::Dir.should_receive(:chdir).with(@expected_release_dir).exactly(4).times + FileUtils.should_not_receive(:mkdir_p).with(@resource.deploy_to) + FileUtils.should_not_receive(:mkdir_p).with(@resource.shared_path) + @provider.stub(:copy_cached_repo) + @provider.stub(:update_cached_repo) + @provider.stub(:symlink) + @provider.stub(:migrate) + @provider.deploy + @provider.converge + end + + it "ensures the deploy_to dir ownership after the verfication that it exists" do + @provider.should_receive(:verify_directories_exist).ordered + @provider.should_receive(:enforce_ownership).ordered + @provider.stub(:copy_cached_repo) + @provider.stub(:update_cached_repo) + @provider.stub(:install_gems) + @provider.stub(:enforce_ownership) + @provider.stub(:symlink) + @provider.stub(:migrate) + @provider.deploy + end + + it "updates and copies the repo, then does a migrate, symlink, restart, restart, cleanup on deploy" do + FileUtils.stub(:mkdir_p).with("/my/deploy/dir") + FileUtils.stub(:mkdir_p).with("/my/deploy/dir/shared") + @provider.should_receive(:enforce_ownership).twice + @provider.should_receive(:update_cached_repo) + @provider.should_receive(:copy_cached_repo) + @provider.should_receive(:install_gems) + @provider.should_receive(:callback).with(:before_migrate, nil) + @provider.should_receive(:migrate) + @provider.should_receive(:callback).with(:before_symlink, nil) + @provider.should_receive(:symlink) + @provider.should_receive(:callback).with(:before_restart, nil) + @provider.should_receive(:restart) + @provider.should_receive(:callback).with(:after_restart, nil) + @provider.should_receive(:cleanup!) + @provider.deploy + @provider.converge + end + + it "should not deploy if there is already a deploy at release_path, and it is the current release" do + @provider.stub!(:all_releases).and_return([@expected_release_dir]) + @provider.stub!(:current_release?).with(@expected_release_dir).and_return(true) + @provider.should_not_receive(:deploy) + @provider.run_action(:deploy) + end + + it "should call action_rollback if there is already a deploy of this revision at release_path, and it is not the current release" do + @provider.stub!(:all_releases).and_return([@expected_release_dir, "102021"]) + @provider.stub!(:current_release?).with(@expected_release_dir).and_return(false) + @provider.should_receive(:rollback_to).with(@expected_release_dir) + @provider.should_receive(:current_release?) + @provider.run_action(:deploy) + end + + it "calls deploy when deploying a new release" do + @provider.stub!(:all_releases).and_return([]) + @provider.should_receive(:deploy) + @provider.run_action(:deploy) + end + + it "runs action svn_force_export when new_resource.svn_force_export is true" do + @resource.svn_force_export true + @provider.scm_provider.should_receive(:run_action).with(:force_export) + @provider.update_cached_repo + @provider.converge + end + + it "Removes the old release before deploying when force deploying over it" do + @provider.stub!(:all_releases).and_return([@expected_release_dir]) + FileUtils.should_receive(:rm_rf).with(@expected_release_dir) + @provider.should_receive(:deploy) + @provider.run_action(:force_deploy) + end + + it "deploys as normal when force deploying and there's no prior release at the same path" do + @provider.stub!(:all_releases).and_return([]) + @provider.should_receive(:deploy) + @provider.run_action(:force_deploy) + end + + it "dont care by default if error happens on deploy" do + @provider.stub!(:all_releases).and_return(['previous_release']) + @provider.stub!(:deploy).and_return{ raise "Unexpected error" } + @provider.stub!(:previous_release_path).and_return('previous_release') + @provider.should_not_receive(:rollback) + lambda { + @provider.run_action(:deploy) + }.should raise_exception(RuntimeError, "Unexpected error") + end + + it "rollbacks to previous release if error happens on deploy" do + @resource.rollback_on_error true + @provider.stub!(:all_releases).and_return(['previous_release']) + @provider.stub!(:deploy).and_return{ raise "Unexpected error" } + @provider.stub!(:previous_release_path).and_return('previous_release') + @provider.should_receive(:rollback) + lambda { + @provider.run_action(:deploy) + }.should raise_exception(RuntimeError, "Unexpected error") + end + + describe "on systems without broken Dir.glob results" do + it "sets the release path to the penultimate release when one is not specified, symlinks, and rm's the last release on rollback" do + @provider.stub!(:release_path).and_return("/my/deploy/dir/releases/3") + all_releases = ["/my/deploy/dir/releases/1", "/my/deploy/dir/releases/2", "/my/deploy/dir/releases/3", "/my/deploy/dir/releases/4", "/my/deploy/dir/releases/5"] + Dir.stub!(:glob).with("/my/deploy/dir/releases/*").and_return(all_releases) + @provider.should_receive(:symlink) + FileUtils.should_receive(:rm_rf).with("/my/deploy/dir/releases/4") + FileUtils.should_receive(:rm_rf).with("/my/deploy/dir/releases/5") + @provider.run_action(:rollback) + @provider.release_path.should eql("/my/deploy/dir/releases/3") + end + + it "sets the release path to the specified release, symlinks, and rm's any newer releases on rollback" do + @provider.unstub!(:release_path) + all_releases = ["/my/deploy/dir/releases/20040815162342", "/my/deploy/dir/releases/20040700000000", + "/my/deploy/dir/releases/20040600000000", "/my/deploy/dir/releases/20040500000000"].sort! + Dir.stub!(:glob).with("/my/deploy/dir/releases/*").and_return(all_releases) + @provider.should_receive(:symlink) + FileUtils.should_receive(:rm_rf).with("/my/deploy/dir/releases/20040815162342") + @provider.run_action(:rollback) + @provider.release_path.should eql("/my/deploy/dir/releases/20040700000000") + end + + it "sets the release path to the penultimate release, symlinks, and rm's the last release on rollback" do + @provider.unstub!(:release_path) + all_releases = [ "/my/deploy/dir/releases/20040815162342", + "/my/deploy/dir/releases/20040700000000", + "/my/deploy/dir/releases/20040600000000", + "/my/deploy/dir/releases/20040500000000"] + Dir.stub!(:glob).with("/my/deploy/dir/releases/*").and_return(all_releases) + @provider.should_receive(:symlink) + FileUtils.should_receive(:rm_rf).with("/my/deploy/dir/releases/20040815162342") + @provider.run_action(:rollback) + @provider.release_path.should eql("/my/deploy/dir/releases/20040700000000") + end + + describe "if there are no releases to fallback to" do + + it "an exception is raised when there is only 1 release" do + #@provider.unstub!(:release_path) -- unstub the release path on top to feed our own release path + all_releases = [ "/my/deploy/dir/releases/20040815162342"] + Dir.stub!(:glob).with("/my/deploy/dir/releases/*").and_return(all_releases) + #@provider.should_receive(:symlink) + #FileUtils.should_receive(:rm_rf).with("/my/deploy/dir/releases/20040815162342") + #@provider.run_action(:rollback) + #@provider.release_path.should eql(NIL) -- no check needed since assertions will fail + lambda { + @provider.run_action(:rollback) + }.should raise_exception(RuntimeError, "There is no release to rollback to!") + end + + it "an exception is raised when there are no releases" do + all_releases = [] + Dir.stub!(:glob).with("/my/deploy/dir/releases/*").and_return(all_releases) + lambda { + @provider.run_action(:rollback) + }.should raise_exception(RuntimeError, "There is no release to rollback to!") + end + end + end + + describe "CHEF-628: on systems with broken Dir.glob results" do + it "sets the release path to the penultimate release, symlinks, and rm's the last release on rollback" do + @provider.unstub!(:release_path) + all_releases = [ "/my/deploy/dir/releases/20040500000000", + "/my/deploy/dir/releases/20040600000000", + "/my/deploy/dir/releases/20040700000000", + "/my/deploy/dir/releases/20040815162342" ] + Dir.stub!(:glob).with("/my/deploy/dir/releases/*").and_return(all_releases) + @provider.should_receive(:symlink) + FileUtils.should_receive(:rm_rf).with("/my/deploy/dir/releases/20040815162342") + @provider.run_action(:rollback) + @provider.release_path.should eql("/my/deploy/dir/releases/20040700000000") + end + end + + it "raises a runtime error when there's no release to rollback to" do + all_releases = [] + Dir.stub!(:glob).with("/my/deploy/dir/releases/*").and_return(all_releases) + lambda {@provider.run_action(:rollback)}.should raise_error(RuntimeError) + end + + it "runs the new resource collection in the runner during a callback" do + @runner = mock("Runner") + Chef::Runner.stub!(:new).and_return(@runner) + @runner.should_receive(:converge) + callback_code = Proc.new { :noop } + @provider.callback(:whatevs, callback_code) + @provider.converge + end + + it "loads callback files from the release/ dir if the file exists" do + foo_callback = @expected_release_dir + "/deploy/foo.rb" + ::File.should_receive(:exist?).with(foo_callback).once.and_return(true) + ::Dir.should_receive(:chdir).with(@expected_release_dir).and_yield + @provider.should_receive(:from_file).with(foo_callback) + @provider.callback(:foo, "deploy/foo.rb") + @provider.converge + end + + it "raises a runtime error if a callback file is explicitly specified but does not exist" do + baz_callback = "/deploy/baz.rb" + ::File.should_receive(:exist?).with("#{@expected_release_dir}/#{baz_callback}").and_return(false) + @resource.before_migrate baz_callback + @provider.define_resource_requirements + @provider.action = :deploy + lambda {@provider.process_resource_requirements}.should raise_error(RuntimeError) + end + + it "runs a default callback if the callback code is nil" do + bar_callback = @expected_release_dir + "/deploy/bar.rb" + ::File.should_receive(:exist?).with(bar_callback).and_return(true) + ::Dir.should_receive(:chdir).with(@expected_release_dir).and_yield + @provider.should_receive(:from_file).with(bar_callback) + @provider.callback(:bar, nil) + @provider.converge + end + + it "skips an eval callback if the file doesn't exist" do + barbaz_callback = @expected_release_dir + "/deploy/barbaz.rb" + ::File.should_receive(:exist?).with(barbaz_callback).and_return(false) + ::Dir.should_receive(:chdir).with(@expected_release_dir).and_yield + @provider.should_not_receive(:from_file) + @provider.callback(:barbaz, nil) + @provider.converge + end + + # CHEF-3449 #converge_by is called in #recipe_eval and must happen in sequence + # with the other calls to #converge_by to keep the train on the tracks + it "evaluates a callback file before the corresponding step" do + @provider.should_receive(:verify_directories_exist) + @provider.should_receive(:update_cached_repo) + @provider.should_receive(:enforce_ownership) + @provider.should_receive(:copy_cached_repo) + @provider.should_receive(:install_gems) + @provider.should_receive(:enforce_ownership) + @provider.should_receive(:converge_by).ordered # before_migrate + @provider.should_receive(:migrate).ordered + @provider.should_receive(:converge_by).ordered # before_symlink + @provider.should_receive(:symlink).ordered + @provider.should_receive(:converge_by).ordered # before_restart + @provider.should_receive(:restart).ordered + @provider.should_receive(:converge_by).ordered # after_restart + @provider.should_receive(:cleanup!) + @provider.deploy + end + + it "gets a SCM provider as specified by its resource" do + @provider.scm_provider.should be_an_instance_of(Chef::Provider::Git) + @provider.scm_provider.new_resource.destination.should eql("/my/deploy/dir/shared/cached-copy") + end + + it "syncs the cached copy of the repo" do + @provider.scm_provider.should_receive(:run_action).with(:sync) + @provider.update_cached_repo + @provider.converge + end + + it "makes a copy of the cached repo in releases dir" do + FileUtils.should_receive(:mkdir_p).with("/my/deploy/dir/releases") + @provider.should_receive(:run_command).with({:command => "cp -RPp /my/deploy/dir/shared/cached-copy/. #{@expected_release_dir}"}) + @provider.copy_cached_repo + @provider.converge + end + + it "calls the internal callback :release_created when copying the cached repo" do + FileUtils.stub!(:mkdir_p) + @provider.stub!(:run_command).and_return(true) + @provider.should_receive(:release_created) + @provider.copy_cached_repo + @provider.converge + end + + it "chowns the whole release dir to user and group specified in the resource" do + @resource.user "foo" + @resource.group "bar" + FileUtils.should_receive(:chown_R).with("foo", "bar", "/my/deploy/dir") + @provider.enforce_ownership + @provider.converge + end + + it "skips the migration when resource.migrate => false but runs symlinks before migration" do + @resource.migrate false + @provider.should_not_receive :run_command + @provider.should_receive :run_symlinks_before_migrate + @provider.migrate + @provider.converge + end + + it "links the database.yml and runs resource.migration_command when resource.migrate #=> true" do + @resource.migrate true + @resource.migration_command "migration_foo" + @resource.user "deployNinja" + @resource.group "deployNinjas" + @resource.environment "RAILS_ENV" => "production" + FileUtils.should_receive(:ln_sf).with("/my/deploy/dir/shared/config/database.yml", @expected_release_dir + "/config/database.yml") + @provider.should_receive(:enforce_ownership) + + STDOUT.stub!(:tty?).and_return(true) + Chef::Log.stub!(:info?).and_return(true) + @provider.should_receive(:run_command).with(:command => "migration_foo", :cwd => @expected_release_dir, + :user => "deployNinja", :group => "deployNinjas", + :log_level => :info, :live_stream => STDOUT, + :log_tag => "deploy[/my/deploy/dir]", + :environment => {"RAILS_ENV"=>"production"}) + @provider.migrate + @provider.converge + end + + it "purges the current release's /log /tmp/pids/ and /public/system directories" do + FileUtils.should_receive(:rm_rf).with(@expected_release_dir + "/log") + FileUtils.should_receive(:rm_rf).with(@expected_release_dir + "/tmp/pids") + FileUtils.should_receive(:rm_rf).with(@expected_release_dir + "/public/system") + @provider.purge_tempfiles_from_current_release + @provider.converge + end + + it "symlinks temporary files and logs from the shared dir into the current release" do + FileUtils.stub(:mkdir_p).with(@resource.shared_path + "/system") + FileUtils.stub(:mkdir_p).with(@resource.shared_path + "/pids") + FileUtils.stub(:mkdir_p).with(@resource.shared_path + "/log") + FileUtils.should_receive(:mkdir_p).with(@expected_release_dir + "/tmp") + FileUtils.should_receive(:mkdir_p).with(@expected_release_dir + "/public") + FileUtils.should_receive(:mkdir_p).with(@expected_release_dir + "/config") + FileUtils.should_receive(:ln_sf).with("/my/deploy/dir/shared/system", @expected_release_dir + "/public/system") + FileUtils.should_receive(:ln_sf).with("/my/deploy/dir/shared/pids", @expected_release_dir + "/tmp/pids") + FileUtils.should_receive(:ln_sf).with("/my/deploy/dir/shared/log", @expected_release_dir + "/log") + FileUtils.should_receive(:ln_sf).with("/my/deploy/dir/shared/config/database.yml", @expected_release_dir + "/config/database.yml") + @provider.should_receive(:enforce_ownership) + @provider.link_tempfiles_to_current_release + @provider.converge + end + + it "symlinks the current release dir into production" do + FileUtils.should_receive(:rm_f).with("/my/deploy/dir/current") + FileUtils.should_receive(:ln_sf).with(@expected_release_dir, "/my/deploy/dir/current") + @provider.should_receive(:enforce_ownership) + @provider.link_current_release_to_production + @provider.converge + end + + context "with a customized app layout" do + + before do + @resource.purge_before_symlink(%w{foo bar}) + @resource.create_dirs_before_symlink(%w{baz qux}) + @resource.symlinks "foo/bar" => "foo/bar", "baz" => "qux/baz" + @resource.symlink_before_migrate "radiohead/in_rainbows.yml" => "awesome" + @provider.converge + end + + it "purges the purge_before_symlink directories" do + FileUtils.should_receive(:rm_rf).with(@expected_release_dir + "/foo") + FileUtils.should_receive(:rm_rf).with(@expected_release_dir + "/bar") + @provider.purge_tempfiles_from_current_release + @provider.converge + end + + it "symlinks files from the shared directory to the current release directory" do + FileUtils.should_receive(:mkdir_p).with(@expected_release_dir + "/baz") + FileUtils.should_receive(:mkdir_p).with(@expected_release_dir + "/qux") + FileUtils.stub(:mkdir_p).with(@resource.shared_path + "/foo/bar") + FileUtils.stub(:mkdir_p).with(@resource.shared_path + "/baz") + FileUtils.should_receive(:ln_sf).with("/my/deploy/dir/shared/foo/bar", @expected_release_dir + "/foo/bar") + FileUtils.should_receive(:ln_sf).with("/my/deploy/dir/shared/baz", @expected_release_dir + "/qux/baz") + FileUtils.should_receive(:ln_sf).with("/my/deploy/dir/shared/radiohead/in_rainbows.yml", @expected_release_dir + "/awesome") + @provider.should_receive(:enforce_ownership) + @provider.link_tempfiles_to_current_release + @provider.converge + end + + end + + it "does nothing for restart if restart_command is empty" do + @provider.should_not_receive(:run_command) + @provider.restart + @provider.converge + end + + it "runs the restart command in the current application dir when the resource has a restart_command" do + @resource.restart_command "restartcmd" + @provider.should_receive(:run_command).with(:command => "restartcmd", :cwd => "/my/deploy/dir/current", :log_tag => "deploy[/my/deploy/dir]", :log_level => :debug) + @provider.restart + @provider.converge + end + + it "lists all available releases" do + all_releases = ["/my/deploy/dir/20040815162342", "/my/deploy/dir/20040700000000", + "/my/deploy/dir/20040600000000", "/my/deploy/dir/20040500000000"].sort! + Dir.should_receive(:glob).with("/my/deploy/dir/releases/*").and_return(all_releases) + @provider.all_releases.should eql(all_releases) + end + + it "removes all but the 5 newest releases" do + all_releases = ["/my/deploy/dir/20040815162342", "/my/deploy/dir/20040700000000", + "/my/deploy/dir/20040600000000", "/my/deploy/dir/20040500000000", + "/my/deploy/dir/20040400000000", "/my/deploy/dir/20040300000000", + "/my/deploy/dir/20040200000000", "/my/deploy/dir/20040100000000"].sort! + @provider.stub!(:all_releases).and_return(all_releases) + FileUtils.should_receive(:rm_rf).with("/my/deploy/dir/20040100000000") + FileUtils.should_receive(:rm_rf).with("/my/deploy/dir/20040200000000") + FileUtils.should_receive(:rm_rf).with("/my/deploy/dir/20040300000000") + @provider.cleanup! + @provider.converge + end + + it "removes all but a certain number of releases when the resource has a keep_releases" do + @resource.keep_releases 7 + all_releases = ["/my/deploy/dir/20040815162342", "/my/deploy/dir/20040700000000", + "/my/deploy/dir/20040600000000", "/my/deploy/dir/20040500000000", + "/my/deploy/dir/20040400000000", "/my/deploy/dir/20040300000000", + "/my/deploy/dir/20040200000000", "/my/deploy/dir/20040100000000"].sort! + @provider.stub!(:all_releases).and_return(all_releases) + FileUtils.should_receive(:rm_rf).with("/my/deploy/dir/20040100000000") + @provider.cleanup! + @provider.converge + end + + it "fires a callback for :release_deleted when deleting an old release" do + all_releases = ["/my/deploy/dir/20040815162342", "/my/deploy/dir/20040700000000", + "/my/deploy/dir/20040600000000", "/my/deploy/dir/20040500000000", + "/my/deploy/dir/20040400000000", "/my/deploy/dir/20040300000000"].sort! + @provider.stub!(:all_releases).and_return(all_releases) + FileUtils.stub!(:rm_rf) + @provider.should_receive(:release_deleted).with("/my/deploy/dir/20040300000000") + @provider.cleanup! + @provider.converge + end + + it "puts resource.to_hash in @configuration for backwards compat with capistano-esque deploy hooks" do + @provider.instance_variable_get(:@configuration).should == @resource.to_hash + end + + it "sets @configuration[:environment] to the value of RAILS_ENV for backwards compat reasons" do + resource = Chef::Resource::Deploy.new("/my/deploy/dir") + resource.environment "production" + provider = Chef::Provider::Deploy.new(resource, @run_context) + provider.instance_variable_get(:@configuration)[:environment].should eql("production") + @provider.converge + end + + it "shouldn't give a no method error on migrate if the environment is nil" do + @provider.stub!(:enforce_ownership) + @provider.stub!(:run_symlinks_before_migrate) + @provider.stub!(:run_command) + @provider.migrate + @provider.converge + end + + context "using inline recipes for callbacks" do + + it "runs an inline recipe with the provided block for :callback_name == {:recipe => &block} " do + snitch = nil + recipe_code = Proc.new {snitch = 42} + #@provider.should_receive(:instance_eval).with(&recipe_code) + @provider.callback(:whateverz, recipe_code) + @provider.converge + snitch.should == 42 + end + + it "loads a recipe file from the specified path and from_file evals it" do + ::File.should_receive(:exist?).with(@expected_release_dir + "/chefz/foobar_callback.rb").once.and_return(true) + ::Dir.should_receive(:chdir).with(@expected_release_dir).and_yield + @provider.should_receive(:from_file).with(@expected_release_dir + "/chefz/foobar_callback.rb") + @provider.callback(:whateverz, "chefz/foobar_callback.rb") + @provider.converge + end + + it "instance_evals a block/proc for restart command" do + snitch = nil + restart_cmd = Proc.new {snitch = 42} + @resource.restart(&restart_cmd) + @provider.restart + @provider.converge + snitch.should == 42 + end + + end + + describe "API bridge to capistrano" do + it "defines sudo as a forwarder to execute" do + @provider.should_receive(:execute).with("the moon, fool") + @provider.sudo("the moon, fool") + @provider.converge + end + + it "defines run as a forwarder to execute, setting the user, group, cwd and environment to new_resource.user" do + mock_execution = mock("Resource::Execute") + @provider.should_receive(:execute).with("iGoToHell4this").and_return(mock_execution) + @resource.user("notCoolMan") + @resource.group("Ggroup") + @resource.environment("APP_ENV" => 'staging') + @resource.deploy_to("/my/app") + mock_execution.should_receive(:user).with("notCoolMan") + mock_execution.should_receive(:group).with("Ggroup") + mock_execution.should_receive(:cwd){|*args| + if args.empty? + nil + else + args.size.should == 1 + args.first.should == @provider.release_path + end + }.twice + mock_execution.should_receive(:environment){ |*args| + if args.empty? + nil + else + args.size.should == 1 + args.first.should == {"APP_ENV" => "staging"} + end + }.twice + @provider.run("iGoToHell4this") + @provider.converge + end + + it "defines run as a forwarder to execute, setting cwd and environment but not override" do + mock_execution = mock("Resource::Execute") + @provider.should_receive(:execute).with("iGoToHell4this").and_return(mock_execution) + @resource.user("notCoolMan") + mock_execution.should_receive(:user).with("notCoolMan") + mock_execution.should_receive(:cwd).with(no_args()).and_return("/some/value") + mock_execution.should_receive(:environment).with(no_args()).and_return({}) + @provider.run("iGoToHell4this") + @provider.converge + end + + + it "converts sudo and run to exec resources in hooks" do + runner = mock("tehRunner") + Chef::Runner.stub!(:new).and_return(runner) + + snitch = nil + @resource.user("tehCat") + + callback_code = Proc.new do + snitch = 42 + temp_collection = self.resource_collection + run("tehMice") + snitch = temp_collection.lookup("execute[tehMice]") + end + + runner.should_receive(:converge) + # + @provider.callback(:phony, callback_code) + @provider.converge + snitch.should be_an_instance_of(Chef::Resource::Execute) + snitch.user.should == "tehCat" + end + end + + describe "installing gems from a gems.yml" do + + before do + ::File.stub!(:exist?).with("#{@expected_release_dir}/gems.yml").and_return(true) + @gem_list = [{:name=>"eventmachine", :version=>"0.12.9"}] + end + + it "reads a gems.yml file, creating gem providers for each with action :upgrade" do + IO.should_receive(:read).with("#{@expected_release_dir}/gems.yml").and_return("cookie") + YAML.should_receive(:load).with("cookie").and_return(@gem_list) + + gems = @provider.send(:gem_packages) + + gems.map { |g| g.action }.should == [[:install]] + gems.map { |g| g.name }.should == %w{eventmachine} + gems.map { |g| g.version }.should == %w{0.12.9} + end + + it "takes a list of gem providers converges them" do + IO.stub!(:read) + YAML.stub!(:load).and_return(@gem_list) + expected_gem_resources = @provider.send(:gem_packages).map { |r| [r.name, r.version] } + gem_runner = @provider.send(:gem_resource_collection_runner) + # no one has heard of defining == to be meaningful so I have use this monstrosity + actual = gem_runner.run_context.resource_collection.all_resources.map { |r| [r.name, r.version] } + actual.should == expected_gem_resources + end + + end + +end diff --git a/spec/unit/provider/directory_spec.rb b/spec/unit/provider/directory_spec.rb new file mode 100644 index 0000000000..4f297e0115 --- /dev/null +++ b/spec/unit/provider/directory_spec.rb @@ -0,0 +1,147 @@ +# +# 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 'ostruct' + +require 'spec_helper' + +describe Chef::Provider::Directory do + before(:each) do + @new_resource = Chef::Resource::Directory.new('/tmp') + @new_resource.owner(500) + @new_resource.group(500) + @new_resource.mode(0644) + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @directory = Chef::Provider::Directory.new(@new_resource, @run_context) + end + + it "should load the current resource based on the new resource" do + File.stub!(:exist?).and_return(true) + cstats = mock("stats") + cstats.stub!(:uid).and_return(500) + cstats.stub!(:gid).and_return(500) + cstats.stub!(:mode).and_return(0755) + File.should_receive(:stat).twice.and_return(cstats) + @directory.load_current_resource + @directory.current_resource.path.should eql(@new_resource.path) + @directory.current_resource.owner.should eql(500) + @directory.current_resource.group.should eql(500) + @directory.current_resource.mode.should == 00755 + end + + it "should create a new directory on create, setting updated to true" do + @new_resource.path "/tmp/foo" + + File.should_receive(:exist?).exactly(3).and_return(false) + Dir.should_receive(:mkdir).with(@new_resource.path).once.and_return(true) + + @directory.should_receive(:set_all_access_controls) + @directory.stub!(:update_new_file_state) + @directory.run_action(:create) + @directory.new_resource.should be_updated + end + + it "should raise an exception if the parent directory does not exist and recursive is false" do + @new_resource.path "/tmp/some/dir" + @new_resource.recursive false + lambda { @directory.run_action(:create) }.should raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + end + + it "should create a new directory when parent directory does not exist if recursive is true and permissions are correct" do + @new_resource.path "/path/to/dir" + @new_resource.recursive true + File.should_receive(:exist?).with(@new_resource.path).ordered.and_return(false) + File.should_receive(:exist?).with(@new_resource.path).ordered.and_return(false) + + File.should_receive(:exist?).with('/path/to').ordered.and_return(false) + File.should_receive(:exist?).with('/path').ordered.and_return(true) + File.should_receive(:writable?).with('/path').ordered.and_return(true) + File.should_receive(:exist?).with(@new_resource.path).ordered.and_return(false) + + FileUtils.should_receive(:mkdir_p).with(@new_resource.path).and_return(true) + @directory.should_receive(:set_all_access_controls) + @directory.stub!(:update_new_file_state) + @directory.run_action(:create) + @new_resource.should be_updated + end + + # it "should raise an error when creating a directory recursively and permissions do not allow creation" do + + # end + + it "should raise an error when creating a directory when parent directory is a file" do + File.should_receive(:directory?).and_return(false) + Dir.should_not_receive(:mkdir).with(@new_resource.path) + lambda { @directory.run_action(:create) }.should raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + @directory.new_resource.should_not be_updated + end + + it "should not create the directory if it already exists" do + stub_file_cstats + @new_resource.path "/tmp/foo" + File.should_receive(:exist?).exactly(3).and_return(true) + Dir.should_not_receive(:mkdir).with(@new_resource.path) + @directory.should_receive(:set_all_access_controls) + @directory.run_action(:create) + end + + it "should delete the directory if it exists, and is writable with action_delete" do + File.should_receive(:directory?).and_return(true) + File.should_receive(:writable?).once.and_return(true) + Dir.should_receive(:delete).with(@new_resource.path).once.and_return(true) + @directory.run_action(:delete) + end + + it "should raise an exception if it cannot delete the directory due to bad permissions" do + File.stub!(:exist?).and_return(true) + File.stub!(:writable?).and_return(false) + lambda { @directory.run_action(:delete) }.should raise_error(RuntimeError) + end + + it "should take no action when deleting a target directory that does not exist" do + @new_resource.path "/an/invalid/path" + File.stub!(:exist?).and_return(false) + Dir.should_not_receive(:delete).with(@new_resource.path) + @directory.run_action(:delete) + @directory.new_resource.should_not be_updated + end + + it "should raise an exception when deleting a directory when target directory is a file" do + stub_file_cstats + @new_resource.path "/an/invalid/path" + File.stub!(:exist?).and_return(true) + File.should_receive(:directory?).and_return(false) + Dir.should_not_receive(:delete).with(@new_resource.path) + lambda { @directory.run_action(:delete) }.should raise_error(RuntimeError) + @directory.new_resource.should_not be_updated + end + + def stub_file_cstats + cstats = mock("stats") + cstats.stub!(:uid).and_return(500) + cstats.stub!(:gid).and_return(500) + cstats.stub!(:mode).and_return(0755) + # File.stat is called in: + # - Chef::Provider::File.load_current_resource_attrs + # - Chef::ScanAccessControl via Chef::Provider::File.setup_acl + File.stub!(:stat).and_return(cstats) + end +end diff --git a/spec/unit/provider/env_spec.rb b/spec/unit/provider/env_spec.rb new file mode 100644 index 0000000000..77aea42b43 --- /dev/null +++ b/spec/unit/provider/env_spec.rb @@ -0,0 +1,232 @@ +# +# Author:: Doug MacEachern (<dougm@vmware.com>) +# Copyright:: Copyright (c) 2010 VMware, 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::Provider::Env do + + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Env.new("FOO") + @new_resource.value("bar") + @provider = Chef::Provider::Env.new(@new_resource, @run_context) + end + + it "assumes the key_name exists by default" do + @provider.key_exists.should be_true + end + + describe "when loading the current status" do + before do + #@current_resource = @new_resource.clone + #Chef::Resource::Env.stub!(:new).and_return(@current_resource) + @provider.current_resource = @current_resource + @provider.stub!(:env_value).with("FOO").and_return("bar") + @provider.stub!(:env_key_exists).and_return(true) + end + + it "should create a current resource with the same name as the new resource" do + @provider.load_current_resource + @provider.new_resource.name.should == "FOO" + end + + it "should set the key_name to the key name of the new resource" do + @provider.load_current_resource + @provider.current_resource.key_name.should == "FOO" + end + + it "should check if the key_name exists" do + @provider.should_receive(:env_key_exists).with("FOO").and_return(true) + @provider.load_current_resource + @provider.key_exists.should be_true + end + + it "should flip the value of exists if the key does not exist" do + @provider.should_receive(:env_key_exists).with("FOO").and_return(false) + @provider.load_current_resource + @provider.key_exists.should be_false + end + + it "should return the current resource" do + @provider.load_current_resource.should be_a_kind_of(Chef::Resource::Env) + end + end + + describe "action_create" do + before do + @provider.key_exists = false + @provider.stub!(:create_env).and_return(true) + @provider.stub!(:modify_env).and_return(true) + end + + it "should call create_env if the key does not exist" do + @provider.should_receive(:create_env).and_return(true) + @provider.action_create + end + + it "should set the the new_resources updated flag when it creates the key" do + @provider.action_create + @new_resource.should be_updated + end + + it "should check to see if the values are the same if the key exists" do + @provider.key_exists = true + @provider.should_receive(:compare_value).and_return(false) + @provider.action_create + end + + it "should call modify_env if the key exists and values are not equal" do + @provider.key_exists = true + @provider.stub!(:compare_value).and_return(true) + @provider.should_receive(:modify_env).and_return(true) + @provider.action_create + end + + it "should set the the new_resources updated flag when it updates an existing value" do + @provider.key_exists = true + @provider.stub!(:compare_value).and_return(true) + @provider.stub!(:modify_env).and_return(true) + @provider.action_create + @new_resource.should be_updated + end + end + + describe "action_delete" do + before(:each) do + @provider.current_resource = @current_resource + @provider.key_exists = false + @provider.stub!(:delete_element).and_return(false) + @provider.stub!(:delete_env).and_return(true) + end + + it "should not call delete_env if the key does not exist" do + @provider.should_not_receive(:delete_env) + @provider.action_delete + end + + it "should not call delete_element if the key does not exist" do + @provider.should_not_receive(:delete_element) + @provider.action_delete + end + + it "should call delete_env if the key exists" do + @provider.key_exists = true + @provider.should_receive(:delete_env) + @provider.action_delete + end + + it "should set the new_resources updated flag to true if the key is deleted" do + @provider.key_exists = true + @provider.action_delete + @new_resource.should be_updated + end + end + + describe "action_modify" do + before(:each) do + @provider.current_resource = @current_resource + @provider.key_exists = true + @provider.stub!(:modify_env).and_return(true) + end + + it "should call modify_group if the key exists and values are not equal" do + @provider.should_receive(:compare_value).and_return(true) + @provider.should_receive(:modify_env).and_return(true) + @provider.action_modify + end + + it "should set the new resources updated flag to true if modify_env is called" do + @provider.stub!(:compare_value).and_return(true) + @provider.stub!(:modify_env).and_return(true) + @provider.action_modify + @new_resource.should be_updated + end + + it "should not call modify_env if the key exists but the values are equal" do + @provider.should_receive(:compare_value).and_return(false) + @provider.should_not_receive(:modify_env) + @provider.action_modify + end + + it "should raise a Chef::Exceptions::Env if the key doesn't exist" do + @provider.key_exists = false + lambda { @provider.action_modify }.should raise_error(Chef::Exceptions::Env) + end + end + + describe "delete_element" do + before(:each) do + @current_resource = Chef::Resource::Env.new("FOO") + + @new_resource.delim ";" + @new_resource.value "C:/bar/bin" + + @current_resource.value "C:/foo/bin;C:/bar/bin" + @provider.current_resource = @current_resource + end + + it "should return true if the element is not found" do + @new_resource.stub!(:value).and_return("C:/baz/bin") + @provider.delete_element.should eql(true) + end + + it "should return false if the delim not defined" do + @new_resource.stub!(:delim).and_return(nil) + @provider.delete_element.should eql(false) + end + + it "should return true if the element is deleted" do + @new_resource.value("C:/foo/bin") + @provider.should_receive(:create_env) + @provider.delete_element.should eql(true) + @new_resource.should be_updated + end + end + + describe "compare_value" do + before(:each) do + @new_resource.value("C:/bar") + @current_resource = @new_resource.clone + @provider.current_resource = @current_resource + end + + it "should return false if the values are equal" do + @provider.compare_value.should be_false + end + + it "should return true if the values not are equal" do + @new_resource.value("C:/elsewhere") + @provider.compare_value.should be_true + end + + it "should return false if the current value contains the element" do + @new_resource.delim(";") + @current_resource.value("C:/bar;C:/foo;C:/baz") + + @provider.compare_value.should be_false + end + + it "should return true if the current value does not contain the element" do + @new_resource.delim(";") + @current_resource.value("C:/biz;C:/foo/bin;C:/baz") + @provider.compare_value.should be_true + end + end +end diff --git a/spec/unit/provider/erl_call_spec.rb b/spec/unit/provider/erl_call_spec.rb new file mode 100644 index 0000000000..df7910424b --- /dev/null +++ b/spec/unit/provider/erl_call_spec.rb @@ -0,0 +1,88 @@ +# +# Author:: Joe Williams (<joe@joetify.com>) +# Copyright:: Copyright (c) 2009 Joe Williams +# 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::Provider::ErlCall do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::ErlCall.new("test", @node) + @new_resource.code("io:format(\"burritos\", []).") + @new_resource.node_name("chef@localhost") + @new_resource.name("test") + + @provider = Chef::Provider::ErlCall.new(@new_resource, @run_context) + + @provider.stub!(:popen4).and_return(@status) + @stdin = StringIO.new + @stdout = StringIO.new('{ok, woohoo}') + @stderr = StringIO.new + @pid = 2342999 + end + + it "should return a Chef::Provider::ErlCall object" do + provider = Chef::Provider::ErlCall.new(@new_resource, @run_context) + provider.should be_a_kind_of(Chef::Provider::ErlCall) + end + + it "should return true" do + @provider.load_current_resource.should eql(true) + end + + describe "when running a distributed erl call resource" do + before do + @new_resource.cookie("nomnomnom") + @new_resource.distributed(true) + @new_resource.name_type("sname") + end + + it "should write to stdin of the erl_call command" do + expected_cmd = "erl_call -e -s -sname chef@localhost -c nomnomnom" + @provider.should_receive(:popen4).with(expected_cmd, :waitlast => true).and_return([@pid, @stdin, @stdout, @stderr]) + Process.should_receive(:wait).with(@pid) + + @provider.action_run + @provider.converge + + @stdin.string.should == "#{@new_resource.code}\n" + end + end + + describe "when running a local erl call resource" do + before do + @new_resource.cookie(nil) + @new_resource.distributed(false) + @new_resource.name_type("name") + end + + it "should write to stdin of the erl_call command" do + @provider.should_receive(:popen4).with("erl_call -e -name chef@localhost ", :waitlast => true).and_return([@pid, @stdin, @stdout, @stderr]) + Process.should_receive(:wait).with(@pid) + + @provider.action_run + @provider.converge + + @stdin.string.should == "#{@new_resource.code}\n" + end + end + +end + diff --git a/spec/unit/provider/execute_spec.rb b/spec/unit/provider/execute_spec.rb new file mode 100644 index 0000000000..a944793a89 --- /dev/null +++ b/spec/unit/provider/execute_spec.rb @@ -0,0 +1,63 @@ +# +# Author:: Prajakta Purohit (<prajakta@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 File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "spec_helper")) +#require 'spec_helper' + +describe Chef::Provider::Execute do + before do + @node = Chef::Node.new + @cookbook_collection = Chef::CookbookCollection.new([]) + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, @cookbook_collection, @events) + @new_resource = Chef::Resource::Execute.new("foo_resource", @run_context) + @new_resource.timeout 3600 + @new_resource.returns 0 + @new_resource.creates "foo_resource" + @provider = Chef::Provider::Execute.new(@new_resource, @run_context) + @current_resource = Chef::Resource::Ifconfig.new("foo_resource", @run_context) + @provider.current_resource = @current_resource + Chef::Log.level = :info + # FIXME: There should be a test for how STDOUT.tty? changes the live_stream option being passed + STDOUT.stub!(:tty?).and_return(true) + end + + + it "should execute foo_resource" do + @provider.stub!(:load_current_resource) + opts = {} + opts[:timeout] = @new_resource.timeout + opts[:returns] = @new_resource.returns + opts[:log_level] = :info + opts[:log_tag] = @new_resource.to_s + opts[:live_stream] = STDOUT + @provider.should_receive(:shell_out!).with(@new_resource.command, opts) + + @provider.run_action(:run) + @new_resource.should be_updated + end + + it "should do nothing if the sentinel file exists" do + @provider.stub!(:load_current_resource) + File.should_receive(:exists?).with(@new_resource.creates).and_return(true) + @provider.should_not_receive(:shell_out!) + + @provider.run_action(:run) + @new_resource.should_not be_updated + end +end + 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 diff --git a/spec/unit/provider/git_spec.rb b/spec/unit/provider/git_spec.rb new file mode 100644 index 0000000000..bb8208dc27 --- /dev/null +++ b/spec/unit/provider/git_spec.rb @@ -0,0 +1,352 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.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' +describe Chef::Provider::Git do + + before(:each) do + STDOUT.stub!(:tty?).and_return(true) + Chef::Log.level = :info + + @current_resource = Chef::Resource::Git.new("web2.0 app") + @current_resource.revision("d35af14d41ae22b19da05d7d03a0bafc321b244c") + + @resource = Chef::Resource::Git.new("web2.0 app") + @resource.repository "git://github.com/opscode/chef.git" + @resource.destination "/my/deploy/dir" + @resource.revision "d35af14d41ae22b19da05d7d03a0bafc321b244c" + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @provider = Chef::Provider::Git.new(@resource, @run_context) + @provider.current_resource = @current_resource + end + + context "determining the revision of the currently deployed checkout" do + + before do + @stdout = mock("standard out") + @stderr = mock("standard error") + @exitstatus = mock("exitstatus") + end + + it "sets the current revision to nil if the deploy dir does not exist" do + ::File.should_receive(:exist?).with("/my/deploy/dir/.git").and_return(false) + @provider.find_current_revision.should be_nil + end + + it "determines the current revision when there is one" do + ::File.should_receive(:exist?).with("/my/deploy/dir/.git").and_return(true) + @stdout = "9b4d8dc38dd471246e7cfb1c3c1ad14b0f2bee13\n" + @provider.should_receive(:shell_out!).with('git rev-parse HEAD', {:cwd => '/my/deploy/dir', :returns => [0,128]}).and_return(mock("ShellOut result", :stdout => @stdout)) + @provider.find_current_revision.should eql("9b4d8dc38dd471246e7cfb1c3c1ad14b0f2bee13") + end + + it "gives the current revision as nil when there is no current revision" do + ::File.should_receive(:exist?).with("/my/deploy/dir/.git").and_return(true) + @stderr = "fatal: Not a git repository (or any of the parent directories): .git" + @stdout = "" + @provider.should_receive(:shell_out!).with('git rev-parse HEAD', :cwd => '/my/deploy/dir', :returns => [0,128]).and_return(mock("ShellOut result", :stdout => "", :stderr => @stderr)) + @provider.find_current_revision.should be_nil + end + end + + it "creates a current_resource with the currently deployed revision when a clone exists in the destination dir" do + @provider.stub!(:find_current_revision).and_return("681c9802d1c62a45b490786c18f0b8216b309440") + @provider.load_current_resource + @provider.current_resource.name.should eql(@resource.name) + @provider.current_resource.revision.should eql("681c9802d1c62a45b490786c18f0b8216b309440") + end + + it "keeps the node and resource passed to it on initialize" do + @provider.node.should equal(@node) + @provider.new_resource.should equal(@resource) + end + + context "resolving revisions to a SHA" do + + before do + @git_ls_remote = "git ls-remote git://github.com/opscode/chef.git " + end + + it "returns resource.revision as is if revision is already a full SHA" do + @provider.target_revision.should eql("d35af14d41ae22b19da05d7d03a0bafc321b244c") + end + + it "converts resource.revision from a tag to a SHA" do + @resource.revision "v1.0" + @stdout = "503c22a5e41f5ae3193460cca044ed1435029f53\trefs/heads/0.8-alpha\n" + @provider.should_receive(:shell_out!).with(@git_ls_remote + "v1.0", {:log_tag=>"git[web2.0 app]", :log_level=>:debug}).and_return(mock("ShellOut result", :stdout => @stdout)) + @provider.target_revision.should eql("503c22a5e41f5ae3193460cca044ed1435029f53") + end + + it "raises an invalid remote reference error if you try to deploy from ``origin'' and assertions are run" do + @resource.revision "origin/" + @provider.action = :checkout + @provider.define_resource_requirements + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + lambda {@provider.process_resource_requirements}.should raise_error(Chef::Exceptions::InvalidRemoteGitReference) + end + + it "raises an unresolvable git reference error if the revision can't be resolved to any revision and assertions are run" do + @resource.revision "FAIL, that's the revision I want" + @provider.action = :checkout + @provider.should_receive(:shell_out!).and_return(mock("ShellOut result", :stdout => "\n")) + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::UnresolvableGitReference) + end + + it "does not raise an error if the revision can't be resolved when assertions are not run" do + @resource.revision "FAIL, that's the revision I want" + @provider.should_receive(:shell_out!).and_return(mock("ShellOut result", :stdout => "\n")) + @provider.target_revision.should == nil + end + + it "does not raise an error when the revision is valid and assertions are run." do + @resource.revision "v1.0" + @stdout = "503c22a5e41f5ae3193460cca044ed1435029f53\trefs/heads/0.8-alpha\n" + @provider.should_receive(:shell_out!).with(@git_ls_remote + "v1.0", {:log_tag=>"git[web2.0 app]", :log_level=>:debug}).and_return(mock("ShellOut result", :stdout => @stdout)) + @provider.action = :checkout + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should_not raise_error(RuntimeError) + end + + it "gives the latest HEAD revision SHA if nothing is specified" do + @stdout =<<-SHAS +28af684d8460ba4793eda3e7ac238c864a5d029a\tHEAD +503c22a5e41f5ae3193460cca044ed1435029f53\trefs/heads/0.8-alpha +28af684d8460ba4793eda3e7ac238c864a5d029a\trefs/heads/master +c44fe79bb5e36941ce799cee6b9de3a2ef89afee\trefs/tags/0.5.2 +14534f0e0bf133dc9ff6dbe74f8a0c863ff3ac6d\trefs/tags/0.5.4 +d36fddb4291341a1ff2ecc3c560494e398881354\trefs/tags/0.5.6 +9e5ce9031cbee81015de680d010b603bce2dd15f\trefs/tags/0.6.0 +9b4d8dc38dd471246e7cfb1c3c1ad14b0f2bee13\trefs/tags/0.6.2 +014a69af1cdce619de82afaf6cdb4e6ac658fede\trefs/tags/0.7.0 +fa8097ff666af3ce64761d8e1f1c2aa292a11378\trefs/tags/0.7.2 +44f9be0b33ba5c10027ddb030a5b2f0faa3eeb8d\trefs/tags/0.7.4 +d7b9957f67236fa54e660cc3ab45ffecd6e0ba38\trefs/tags/0.7.8 +b7d19519a1c15f1c1a324e2683bd728b6198ce5a\trefs/tags/0.7.8^{} +ebc1b392fe7e8f0fbabc305c299b4d365d2b4d9b\trefs/tags/chef-server-package +SHAS + @resource.revision '' + @provider.should_receive(:shell_out!).with(@git_ls_remote, {:log_tag=>"git[web2.0 app]", :log_level=>:debug}).and_return(mock("ShellOut result", :stdout => @stdout)) + @provider.target_revision.should eql("28af684d8460ba4793eda3e7ac238c864a5d029a") + end + end + + it "responds to :revision_slug as an alias for target_revision" do + @provider.should respond_to(:revision_slug) + end + + it "runs a clone command with default git options" do + @resource.user "deployNinja" + @resource.ssh_wrapper "do_it_this_way.sh" + expected_cmd = "git clone git://github.com/opscode/chef.git /my/deploy/dir" + @provider.should_receive(:shell_out!).with(expected_cmd, :user => "deployNinja", + :environment =>{"GIT_SSH"=>"do_it_this_way.sh"}, :log_level => :info, :log_tag => "git[web2.0 app]", :live_stream => STDOUT) + + @provider.clone + @provider.converge + end + + it "runs a clone command with escaped destination" do + @resource.user "deployNinja" + @resource.destination "/Application Support/with/space" + @resource.ssh_wrapper "do_it_this_way.sh" + expected_cmd = "git clone git://github.com/opscode/chef.git /Application\\ Support/with/space" + @provider.should_receive(:shell_out!).with(expected_cmd, :user => "deployNinja", + :environment =>{"GIT_SSH"=>"do_it_this_way.sh"}, :log_level => :info, :log_tag => "git[web2.0 app]", :live_stream => STDOUT) + @provider.clone + @provider.converge + end + + it "compiles a clone command using --depth for shallow cloning" do + @resource.depth 5 + expected_cmd = 'git clone --depth 5 git://github.com/opscode/chef.git /my/deploy/dir' + @provider.should_receive(:shell_out!).with(expected_cmd, {:log_level => :info, :log_tag => "git[web2.0 app]", :live_stream => STDOUT}) + @provider.clone + @provider.converge + end + + it "compiles a clone command with a remote other than ``origin''" do + @resource.remote "opscode" + expected_cmd = 'git clone -o opscode git://github.com/opscode/chef.git /my/deploy/dir' + @provider.should_receive(:shell_out!).with(expected_cmd, {:log_level => :info, :log_tag => "git[web2.0 app]", :live_stream => STDOUT}) + @provider.clone + @provider.converge + end + + it "runs a checkout command with default options" do + expected_cmd = 'git checkout -b deploy d35af14d41ae22b19da05d7d03a0bafc321b244c' + @provider.should_receive(:shell_out!).with(expected_cmd, :cwd => "/my/deploy/dir", :log_level => :debug, :log_tag => "git[web2.0 app]") + @provider.checkout + @provider.converge + end + + it "runs an enable_submodule command" do + @resource.enable_submodules true + expected_cmd = "git submodule update --init --recursive" + @provider.should_receive(:shell_out!).with(expected_cmd, :cwd => "/my/deploy/dir", :log_level => :info, :log_tag => "git[web2.0 app]", :live_stream => STDOUT) + @provider.enable_submodules + @provider.converge + end + + it "does nothing for enable_submodules if resource.enable_submodules #=> false" do + @provider.should_not_receive(:shell_out!) + @provider.enable_submodules + @provider.converge + end + + it "runs a sync command with default options" do + expected_cmd = "git fetch origin && git fetch origin --tags && git reset --hard d35af14d41ae22b19da05d7d03a0bafc321b244c" + @provider.should_receive(:shell_out!).with(expected_cmd, :cwd=> "/my/deploy/dir", :log_level => :debug, :log_tag => "git[web2.0 app]") + @provider.fetch_updates + @provider.converge + end + + it "runs a sync command with the user and group specified in the resource" do + @resource.user("whois") + @resource.group("thisis") + expected_cmd = "git fetch origin && git fetch origin --tags && git reset --hard d35af14d41ae22b19da05d7d03a0bafc321b244c" + @provider.should_receive(:shell_out!).with(expected_cmd, :cwd => "/my/deploy/dir", + :user => "whois", :group => "thisis", :log_level => :debug, :log_tag => "git[web2.0 app]") + @provider.fetch_updates + @provider.converge + end + + it "configures remote tracking branches when remote is not ``origin''" do + @resource.remote "opscode" + conf_tracking_branches = "git config remote.opscode.url git://github.com/opscode/chef.git && " + + "git config remote.opscode.fetch +refs/heads/*:refs/remotes/opscode/*" + @provider.should_receive(:shell_out!).with(conf_tracking_branches, :cwd => "/my/deploy/dir", :log_tag => "git[web2.0 app]", :log_level => :debug) + fetch_command = "git fetch opscode && git fetch opscode --tags && git reset --hard d35af14d41ae22b19da05d7d03a0bafc321b244c" + @provider.should_receive(:shell_out!).with(fetch_command, :cwd => "/my/deploy/dir", :log_level => :debug, :log_tag => "git[web2.0 app]") + @provider.fetch_updates + @provider.converge + end + + it "raises an error if the git clone command would fail because the enclosing directory doesn't exist" do + @provider.stub!(:shell_out!) + lambda {@provider.run_action(:sync)}.should raise_error(Chef::Exceptions::MissingParentDirectory) + end + + it "does a checkout by cloning the repo and then enabling submodules" do + # will be invoked in load_current_resource + ::File.stub!(:exist?).with("/my/deploy/dir/.git").and_return(false) + + ::File.stub!(:exist?).with("/my/deploy/dir").and_return(true) + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + ::Dir.stub!(:entries).with("/my/deploy/dir").and_return(['.','..']) + @provider.should_receive(:clone) + @provider.should_receive(:checkout) + @provider.should_receive(:enable_submodules) + @provider.run_action(:checkout) + # Even though an actual run will cause an update to occur, the fact that we've stubbed out + # the actions above will prevent updates from registering + # @resource.should be_updated + end + + # REGRESSION TEST: on some OSes, the entries from an empty directory will be listed as + # ['..', '.'] but this shouldn't change the behavior + it "does a checkout by cloning the repo and then enabling submodules when the directory entries are listed as %w{.. .}" do + ::File.stub!(:exist?).with("/my/deploy/dir/.git").and_return(false) + ::File.stub!(:exist?).with("/my/deploy/dir").and_return(false) + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + ::Dir.stub!(:entries).with("/my/deploy/dir").and_return(['..','.']) + @provider.should_receive(:clone) + @provider.should_receive(:checkout) + @provider.should_receive(:enable_submodules) + @provider.run_action(:checkout) + # @resource.should be_updated + end + + it "should not checkout if the destination exists or is a non empty directory" do + # will be invoked in load_current_resource + ::File.stub!(:exist?).with("/my/deploy/dir/.git").and_return(false) + + ::File.stub!(:exist?).with("/my/deploy/dir").and_return(true) + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + ::Dir.stub!(:entries).with("/my/deploy/dir").and_return(['.','..','foo','bar']) + @provider.should_not_receive(:clone) + @provider.should_not_receive(:checkout) + @provider.should_not_receive(:enable_submodules) + @provider.run_action(:checkout) + @resource.should_not be_updated + end + + it "syncs the code by updating the source when the repo has already been checked out" do + ::File.should_receive(:exist?).with("/my/deploy/dir/.git").and_return(true) + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + @provider.should_receive(:find_current_revision).exactly(2).and_return('d35af14d41ae22b19da05d7d03a0bafc321b244c') + @provider.should_not_receive(:fetch_updates) + @provider.run_action(:sync) + @resource.should_not be_updated + end + + it "marks the resource as updated when the repo is updated and gets a new version" do + ::File.should_receive(:exist?).with("/my/deploy/dir/.git").and_return(true) + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + # invoked twice - first time from load_current_resource + @provider.should_receive(:find_current_revision).exactly(2).and_return('d35af14d41ae22b19da05d7d03a0bafc321b244c') + @provider.stub!(:target_revision).and_return('28af684d8460ba4793eda3e7ac238c864a5d029a') + @provider.should_receive(:fetch_updates) + @provider.should_receive(:enable_submodules) + @provider.run_action(:sync) + # @resource.should be_updated + end + + it "does not fetch any updates if the remote revision matches the current revision" do + ::File.should_receive(:exist?).with("/my/deploy/dir/.git").and_return(true) + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + @provider.stub!(:find_current_revision).and_return('d35af14d41ae22b19da05d7d03a0bafc321b244c') + @provider.stub!(:target_revision).and_return('d35af14d41ae22b19da05d7d03a0bafc321b244c') + @provider.should_not_receive(:fetch_updates) + @provider.run_action(:sync) + @resource.should_not be_updated + end + + it "clones the repo instead of fetching it if the deploy directory doesn't exist" do + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + ::File.should_receive(:exist?).with("/my/deploy/dir/.git").exactly(2).and_return(false) + @provider.should_receive(:action_checkout) + @provider.should_not_receive(:shell_out!) + @provider.run_action(:sync) + # @resource.should be_updated + end + + it "clones the repo instead of fetching updates if the deploy directory is empty" do + ::File.should_receive(:exist?).with("/my/deploy/dir/.git").exactly(2).and_return(false) + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + ::File.stub!(:directory?).with("/my/deploy/dir").and_return(true) + @provider.stub!(:sync_command).and_return("huzzah!") + @provider.should_receive(:action_checkout) + @provider.should_not_receive(:shell_out!).with("huzzah!", :cwd => "/my/deploy/dir") + @provider.run_action(:sync) + #@resource.should be_updated + end + + it "does an export by cloning the repo then removing the .git directory" do + @provider.should_receive(:action_checkout) + FileUtils.should_receive(:rm_rf).with(@resource.destination + "/.git") + @provider.run_action(:export) + @resource.should be_updated + end + +end diff --git a/spec/unit/provider/group/dscl_spec.rb b/spec/unit/provider/group/dscl_spec.rb new file mode 100644 index 0000000000..b526848dfd --- /dev/null +++ b/spec/unit/provider/group/dscl_spec.rb @@ -0,0 +1,294 @@ +# +# Author:: Dreamcat4 (<dreamcat4@gmail.com>) +# Copyright:: Copyright (c) 2009 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::Provider::Group::Dscl do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Group.new("aj") + @current_resource = Chef::Resource::Group.new("aj") + @provider = Chef::Provider::Group::Dscl.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + @status = mock("Process::Status", :exitstatus => 0) + @pid = 2342 + @stdin = StringIO.new + @stdout = StringIO.new("\n") + @stderr = StringIO.new("") + @provider.stub!(:popen4).and_yield(@pid,@stdin,@stdout,@stderr).and_return(@status) + end + + it "should run popen4 with the supplied array of arguments appended to the dscl command" do + @provider.should_receive(:popen4).with("dscl . -cmd /Path arg1 arg2") + @provider.dscl("cmd", "/Path", "arg1", "arg2") + end + + it "should return an array of four elements - cmd, status, stdout, stderr" do + dscl_retval = @provider.dscl("cmd /Path args") + dscl_retval.should be_a_kind_of(Array) + dscl_retval.should == ["dscl . -cmd /Path args",@status,"\n",""] + end + + describe "safe_dscl" do + before do + @node = Chef::Node.new + @provider = Chef::Provider::Group::Dscl.new(@node, @new_resource) + @provider.stub!(:dscl).and_return(["cmd", @status, "stdout", "stderr"]) + end + + it "should run dscl with the supplied cmd /Path args" do + @provider.should_receive(:dscl).with("cmd /Path args") + @provider.safe_dscl("cmd /Path args") + end + + describe "with the dscl command returning a non zero exit status for a delete" do + before do + @status = mock("Process::Status", :exitstatus => 1) + @provider.stub!(:dscl).and_return(["cmd", @status, "stdout", "stderr"]) + end + + it "should return an empty string of standard output for a delete" do + safe_dscl_retval = @provider.safe_dscl("delete /Path args") + safe_dscl_retval.should be_a_kind_of(String) + safe_dscl_retval.should == "" + end + + it "should raise an exception for any other command" do + lambda { @provider.safe_dscl("cmd /Path arguments") }.should raise_error(Chef::Exceptions::Group) + end + end + + describe "with the dscl command returning no such key" do + before do + @provider.stub!(:dscl).and_return(["cmd", @status, "No such key: ", "stderr"]) + end + + it "should raise an exception" do + lambda { @provider.safe_dscl("cmd /Path arguments") }.should raise_error(Chef::Exceptions::Group) + end + end + + describe "with the dscl command returning a zero exit status" do + it "should return the third array element, the string of standard output" do + safe_dscl_retval = @provider.safe_dscl("cmd /Path args") + safe_dscl_retval.should be_a_kind_of(String) + safe_dscl_retval.should == "stdout" + end + end + end + + describe "get_free_gid" do + before do + @node = Chef::Node.new + @provider = Chef::Provider::Group::Dscl.new(@node, @new_resource) + @provider.stub!(:safe_dscl).and_return("\naj 200\njt 201\n") + end + + it "should run safe_dscl with list /Groups gid" do + @provider.should_receive(:safe_dscl).with("list /Groups gid") + @provider.get_free_gid + end + + it "should return the first unused gid number on or above 200" do + @provider.get_free_gid.should equal(202) + end + + it "should raise an exception when the search limit is exhausted" do + search_limit = 1 + lambda { @provider.get_free_gid(search_limit) }.should raise_error(RuntimeError) + end + end + + describe "gid_used?" do + before do + @node = Chef::Node.new + @provider = Chef::Provider::Group::Dscl.new(@node, @new_resource) + @provider.stub!(:safe_dscl).and_return("\naj 500\n") + end + + it "should run safe_dscl with list /Groups gid" do + @provider.should_receive(:safe_dscl).with("list /Groups gid") + @provider.gid_used?(500) + end + + it "should return true for a used gid number" do + @provider.gid_used?(500).should be_true + end + + it "should return false for an unused gid number" do + @provider.gid_used?(501).should be_false + end + + it "should return false if not given any valid gid number" do + @provider.gid_used?(nil).should be_false + end + end + + describe "set_gid" do + describe "with the new resource and a gid number which is already in use" do + before do + @provider.stub!(:gid_used?).and_return(true) + end + + it "should raise an exception if the new resources gid is already in use" do + lambda { @provider.set_gid }.should raise_error(Chef::Exceptions::Group) + end + end + + describe "with no gid number for the new resources" do + it "should run get_free_gid and return a valid, unused gid number" do + @provider.should_receive(:get_free_gid).and_return(501) + @provider.set_gid + end + end + + describe "with blank gid number for the new resources" do + before do + @new_resource.instance_variable_set(:@gid, nil) + @new_resource.stub!(:safe_dscl) + end + + it "should run get_free_gid and return a valid, unused gid number" do + @provider.should_receive(:get_free_gid).and_return(501) + @provider.set_gid + end + end + + describe "with a valid gid number which is not already in use" do + it "should run safe_dscl with create /Groups/group PrimaryGroupID gid" do + @provider.stub(:get_free_gid).and_return(50) + @provider.should_receive(:safe_dscl).with("list /Groups gid") + @provider.should_receive(:safe_dscl).with("create /Groups/aj PrimaryGroupID 50").and_return(true) + @provider.set_gid + end + end + end + + describe "set_members" do + + describe "with existing members in the current resource and append set to false in the new resource" do + before do + @new_resource.stub!(:members).and_return([]) + @new_resource.stub!(:append).and_return(false) + @current_resource.stub!(:members).and_return(["all", "your", "base"]) + end + + it "should log an appropriate message" do + Chef::Log.should_receive(:debug).with("group[aj] removing group members all your base") + @provider.set_members + end + + it "should run safe_dscl with create /Groups/group GroupMembership to clear the Group's UID list" do + @provider.should_receive(:safe_dscl).with("create /Groups/aj GroupMembers ''").and_return(true) + @provider.should_receive(:safe_dscl).with("create /Groups/aj GroupMembership ''").and_return(true) + @provider.set_members + end + end + + describe "with supplied members in the new resource" do + before do + @new_resource.members(["all", "your", "base"]) + @current_resource.members([]) + end + + it "should log an appropriate debug message" do + Chef::Log.should_receive(:debug).with("group[aj] setting group members all, your, base") + @provider.set_members + end + + it "should run safe_dscl with append /Groups/group GroupMembership and group members all, your, base" do + @provider.should_receive(:safe_dscl).with("create /Groups/aj GroupMembers ''").and_return(true) + @provider.should_receive(:safe_dscl).with("append /Groups/aj GroupMembership all your base").and_return(true) + @provider.should_receive(:safe_dscl).with("create /Groups/aj GroupMembership ''").and_return(true) + @provider.set_members + end + end + + describe "with no members in the new resource" do + before do + @new_resource.append(true) + @new_resource.members([]) + end + + it "should not call safe_dscl" do + @provider.should_not_receive(:safe_dscl) + @provider.set_members + end + end + end + + describe "when loading the current system state" do + before (:each) do + @provider.load_current_resource + @provider.define_resource_requirements + end + it "raises an error if the required binary /usr/bin/dscl doesn't exist" do + File.should_receive(:exists?).with("/usr/bin/dscl").and_return(false) + + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Group) + end + + it "doesn't raise an error if /usr/bin/dscl exists" do + File.stub!(:exists?).and_return(true) + lambda { @provider.process_resource_requirements }.should_not raise_error(Chef::Exceptions::Group) + end + end + + describe "when creating the group" do + it "creates the group, password field, gid, and sets group membership" do + @provider.should_receive(:set_gid).and_return(true) + @provider.should_receive(:set_members).and_return(true) + @provider.should_receive(:safe_dscl).with("create /Groups/aj Password '*'") + @provider.should_receive(:safe_dscl).with("create /Groups/aj") + @provider.create_group + end + end + + describe "managing the group" do + it "should manage the group_name if it changed and the new resources group_name is not null" do + @current_resource.group_name("oldval") + @new_resource.group_name("newname") + @provider.should_receive(:safe_dscl).with("create /Groups/newname") + @provider.should_receive(:safe_dscl).with("create /Groups/newname Password '*'") + @provider.manage_group + end + + it "should manage the gid if it changed and the new resources gid is not null" do + @current_resource.gid(23) + @new_resource.gid(42) + @provider.should_receive(:set_gid) + @provider.manage_group + end + + it "should manage the members if it changed and the new resources members is not null" do + @current_resource.members(%{charlie root}) + @new_resource.members(%{crab revenge}) + @provider.should_receive(:set_members) + @provider.manage_group + end + end + + describe "remove_group" do + it "should run safe_dscl with delete /Groups/group and with the new resources group name" do + @provider.should_receive(:safe_dscl).with("delete /Groups/aj").and_return(true) + @provider.remove_group + end + end +end diff --git a/spec/unit/provider/group/gpasswd_spec.rb b/spec/unit/provider/group/gpasswd_spec.rb new file mode 100644 index 0000000000..59da88e851 --- /dev/null +++ b/spec/unit/provider/group/gpasswd_spec.rb @@ -0,0 +1,108 @@ +# +# Author:: AJ Christensen (<aj@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' + +describe Chef::Provider::Group::Gpasswd, "modify_group_members" do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Group.new("wheel") + @new_resource.members %w{lobster rage fist} + @new_resource.append false + @provider = Chef::Provider::Group::Gpasswd.new(@new_resource, @run_context) + #@provider.stub!(:run_command).and_return(true) + end + + describe "when determining the current group state" do + before (:each) do + @provider.load_current_resource + @provider.define_resource_requirements + end + + # Checking for required binaries is already done in the spec + # for Chef::Provider::Group - no need to repeat it here. We'll + # include only what's specific to this provider. + it "should raise an error if the required binary /usr/bin/gpasswd doesn't exist" do + File.stub!(:exists?).and_return(true) + File.should_receive(:exists?).with("/usr/bin/gpasswd").and_return(false) + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Group) + end + + it "shouldn't raise an error if the required binaries exist" do + File.stub!(:exists?).and_return(true) + lambda { @provider.process_resource_requirements }.should_not raise_error(Chef::Exceptions::Group) + end + end + + describe "after the group's current state is known" do + before do + @current_resource = @new_resource.dup + @provider.current_resource = @new_resource + end + + describe "when no group members are specified and append is not set" do + before do + @new_resource.append(false) + @new_resource.members([]) + end + + it "logs a message and sets group's members to 'none'" do + Chef::Log.should_receive(:debug).with("group[wheel] setting group members to: none") + @provider.should_receive(:shell_out!).with("gpasswd -M \"\" wheel") + @provider.modify_group_members + end + end + + describe "when no group members are specified and append is set" do + before do + @new_resource.append(true) + @new_resource.members([]) + end + + it "logs a message and does not modify group membership" do + Chef::Log.should_receive(:debug).with("group[wheel] not changing group members, the group has no members to add") + @provider.should_not_receive(:shell_out!) + @provider.modify_group_members + end + end + + describe "when the resource specifies group members" do + it "should log an appropriate debug message" do + Chef::Log.should_receive(:debug).with("group[wheel] setting group members to lobster, rage, fist") + @provider.stub!(:shell_out!) + @provider.modify_group_members + end + + it "should run gpasswd with the members joined by ',' followed by the target group" do + @provider.should_receive(:shell_out!).with("gpasswd -M lobster,rage,fist wheel") + @provider.modify_group_members + end + + it "should run gpasswd individually for each user when the append option is set" do + @new_resource.append(true) + @provider.should_receive(:shell_out!).with("gpasswd -a lobster wheel") + @provider.should_receive(:shell_out!).with("gpasswd -a rage wheel") + @provider.should_receive(:shell_out!).with("gpasswd -a fist wheel") + @provider.modify_group_members + end + + end + end +end diff --git a/spec/unit/provider/group/groupadd_spec.rb b/spec/unit/provider/group/groupadd_spec.rb new file mode 100644 index 0000000000..f08e14f99b --- /dev/null +++ b/spec/unit/provider/group/groupadd_spec.rb @@ -0,0 +1,161 @@ +# +# Author:: AJ Christensen (<aj@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' + +describe Chef::Provider::Group::Groupadd, "set_options" do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Group.new("aj") + @new_resource.gid(50) + @new_resource.members(["root", "aj"]) + @new_resource.system false + @current_resource = Chef::Resource::Group.new("aj") + @current_resource.gid(50) + @current_resource.members(["root", "aj"]) + @current_resource.system false + @provider = Chef::Provider::Group::Groupadd.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + end + + field_list = { + :gid => "-g" + } + + field_list.each do |attribute, option| + it "should check for differences in #{attribute.to_s} between the current and new resources" do + @new_resource.should_receive(attribute) + @current_resource.should_receive(attribute) + @provider.set_options + end + it "should set the option for #{attribute} if the new resources #{attribute} is not null" do + @new_resource.stub!(attribute).and_return("wowaweea") + @provider.set_options.should eql(" #{option} '#{@new_resource.send(attribute)}' #{@new_resource.group_name}") + end + end + + it "should combine all the possible options" do + match_string = "" + field_list.sort{ |a,b| a[0] <=> b[0] }.each do |attribute, option| + @new_resource.stub!(attribute).and_return("hola") + match_string << " #{option} 'hola'" + end + match_string << " aj" + @provider.set_options.should eql(match_string) + end + + describe "when we want to create a system group" do + it "should not set groupadd_options '-r' when system is false" do + @new_resource.system(false) + @provider.groupadd_options.should_not =~ /-r/ + end + + it "should set groupadd -r if system is true" do + @new_resource.system(true) + @provider.groupadd_options.should == " -r" + end + end +end + +describe Chef::Provider::Group::Groupadd, "create_group" do + before do + @node = Chef::Node.new + @new_resource = Chef::Resource::Group.new("aj") + @provider = Chef::Provider::Group::Groupadd.new(@node, @new_resource) + @provider.stub!(:run_command).and_return(true) + @provider.stub!(:set_options).and_return(" monkey") + @provider.stub!(:groupadd_options).and_return("") + @provider.stub!(:modify_group_members).and_return(true) + end + + it "should run groupadd with the return of set_options" do + @provider.should_receive(:run_command).with({ :command => "groupadd monkey" }).and_return(true) + @provider.create_group + end + + it "should modify the group members" do + @provider.should_receive(:modify_group_members).and_return(true) + @provider.create_group + end +end + +describe Chef::Provider::Group::Groupadd do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Group.new("aj") + @provider = Chef::Provider::Group::Groupadd.new(@new_resource, @run_context) + @provider.stub!(:run_command).and_return(true) + @provider.stub!(:set_options).and_return(" monkey") + end + + describe "manage group" do + + it "should run groupmod with the return of set_options" do + @provider.stub!(:modify_group_members).and_return(true) + @provider.should_receive(:run_command).with({ :command => "groupmod monkey" }).and_return(true) + @provider.manage_group + end + + it "should modify the group members" do + @provider.should_receive(:modify_group_members).and_return(true) + @provider.manage_group + end + end + + describe "remove_group" do + + it "should run groupdel with the new resources group name" do + @provider.should_receive(:run_command).with({ :command => "groupdel aj" }).and_return(true) + @provider.remove_group + end + end + + describe "modify_group_members" do + + it "should raise an error when calling modify_group_members" do + lambda { @provider.modify_group_members ; @provider.converge }.should raise_error(Chef::Exceptions::Group, "you must override modify_group_members in #{@provider.to_s}") + end + end + + describe "load_current_resource" do + before do + File.stub!(:exists?).and_return(false) + @provider.define_resource_requirements + end + it "should raise an error if the required binary /usr/sbin/groupadd doesn't exist" do + File.should_receive(:exists?).with("/usr/sbin/groupadd").and_return(false) + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Group) + end + it "should raise an error if the required binary /usr/sbin/groupmod doesn't exist" do + File.should_receive(:exists?).with("/usr/sbin/groupadd").and_return(true) + File.should_receive(:exists?).with("/usr/sbin/groupmod").and_return(false) + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Group) + end + it "should raise an error if the required binary /usr/sbin/groupdel doesn't exist" do + File.should_receive(:exists?).with("/usr/sbin/groupadd").and_return(true) + File.should_receive(:exists?).with("/usr/sbin/groupmod").and_return(true) + File.should_receive(:exists?).with("/usr/sbin/groupdel").and_return(false) + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Group) + end + + end +end diff --git a/spec/unit/provider/group/groupmod_spec.rb b/spec/unit/provider/group/groupmod_spec.rb new file mode 100644 index 0000000000..c9c56313b5 --- /dev/null +++ b/spec/unit/provider/group/groupmod_spec.rb @@ -0,0 +1,134 @@ +# +# Author:: Dan Crosta (<dcrosta@late.am>) +# Copyright:: Copyright (c) 2012 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::Provider::Group::Groupmod do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Group.new("wheel") + @new_resource.gid 123 + @new_resource.members %w{lobster rage fist} + @new_resource.append false + @provider = Chef::Provider::Group::Groupmod.new(@new_resource, @run_context) + end + + describe "manage_group" do + describe "when determining the current group state" do + it "should raise an error if the required binary /usr/sbin/group doesn't exist" do + File.should_receive(:exists?).with("/usr/sbin/group").and_return(false) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Group) + end + it "should raise an error if the required binary /usr/sbin/user doesn't exist" do + File.should_receive(:exists?).with("/usr/sbin/group").and_return(true) + File.should_receive(:exists?).with("/usr/sbin/user").and_return(false) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Group) + end + + it "shouldn't raise an error if the required binaries exist" do + File.stub!(:exists?).and_return(true) + lambda { @provider.load_current_resource }.should_not raise_error(Chef::Exceptions::Group) + end + end + + describe "after the group's current state is known" do + before do + @current_resource = @new_resource.dup + @provider.current_resource = @current_resource + end + + describe "when no group members are specified and append is not set" do + before do + @new_resource.append(false) + @new_resource.members([]) + end + + it "logs a message and sets group's members to 'none', then removes existing group members" do + Chef::Log.should_receive(:debug).with("group[wheel] setting group members to: none") + Chef::Log.should_receive(:debug).with("group[wheel] removing members lobster, rage, fist") + @provider.should_receive(:shell_out!).with("group mod -n wheel_bak wheel") + @provider.should_receive(:shell_out!).with("group add -g '123' -o wheel") + @provider.should_receive(:shell_out!).with("group del wheel_bak") + @provider.manage_group + end + end + + describe "when no group members are specified and append is set" do + before do + @new_resource.append(true) + @new_resource.members([]) + end + + it "logs a message and does not modify group membership" do + Chef::Log.should_receive(:debug).with("group[wheel] not changing group members, the group has no members to add") + @provider.should_not_receive(:shell_out!) + @provider.manage_group + end + end + + describe "when removing some group members" do + before do + @new_resource.append(false) + @new_resource.members(%w{ lobster }) + end + + it "updates group membership correctly" do + Chef::Log.stub!(:debug) + @provider.should_receive(:shell_out!).with("group mod -n wheel_bak wheel") + @provider.should_receive(:shell_out!).with("user mod -G wheel lobster") + @provider.should_receive(:shell_out!).with("group add -g '123' -o wheel") + @provider.should_receive(:shell_out!).with("group del wheel_bak") + @provider.manage_group + end + end + end + end + + describe "create_group" do + describe "when creating a new group" do + before do + @current_resource = Chef::Resource::Group.new("wheel") + @provider.current_resource = @current_resource + end + + it "should run a group add command and some user mod commands" do + @provider.should_receive(:shell_out!).with("group add -g '123' wheel") + @provider.should_receive(:shell_out!).with("user mod -G wheel lobster") + @provider.should_receive(:shell_out!).with("user mod -G wheel rage") + @provider.should_receive(:shell_out!).with("user mod -G wheel fist") + @provider.create_group + end + end + end + + describe "remove_group" do + describe "when removing an existing group" do + before do + @current_resource = @new_resource.dup + @provider.current_resource = @current_resource + end + + it "should run a group del command" do + @provider.should_receive(:shell_out!).with("group del wheel") + @provider.remove_group + end + end + end +end diff --git a/spec/unit/provider/group/pw_spec.rb b/spec/unit/provider/group/pw_spec.rb new file mode 100644 index 0000000000..a7dbdb8615 --- /dev/null +++ b/spec/unit/provider/group/pw_spec.rb @@ -0,0 +1,140 @@ +# +# Author:: Stephen Haynes (<sh@nomitor.com>) +# Copyright:: Copyright (c) 2009 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::Provider::Group::Pw do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Group.new("wheel") + @new_resource.gid 50 + @new_resource.members [ "root", "aj"] + + @current_resource = Chef::Resource::Group.new("aj") + @current_resource.gid 50 + @current_resource.members [ "root", "aj"] + @provider = Chef::Provider::Group::Pw.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + end + + describe "when setting options for the pw command" do + it "does not set the gid option if gids match or are unmanaged" do + @provider.set_options.should == " wheel" + end + + it "sets the option for gid if it is not nil" do + @new_resource.gid(42) + @provider.set_options.should eql(" wheel -g '42'") + end + end + + describe "when creating a group" do + it "should run pw groupadd with the return of set_options and set_members_option" do + @new_resource.gid(23) + @provider.should_receive(:run_command).with({ :command => "pw groupadd wheel -g '23' -M root,aj" }).and_return(true) + @provider.create_group + end + end + + describe "when managing the group" do + + it "should run pw groupmod with the return of set_options" do + @new_resource.gid(42) + @provider.should_receive(:run_command).with({ :command => "pw groupmod wheel -g '42' -M root,aj" }).and_return(true) + @provider.manage_group + end + + end + + describe "when removing the group" do + it "should run pw groupdel with the new resources group name" do + @provider.should_receive(:run_command).with({ :command => "pw groupdel wheel" }).and_return(true) + @provider.remove_group + end + end + + describe "when setting group membership" do + + describe "with an empty members array in both the new and current resource" do + before do + @new_resource.stub!(:members).and_return([]) + @current_resource.stub!(:members).and_return([]) + end + + it "should log an appropriate message" do + Chef::Log.should_receive(:debug).with("group[wheel] not changing group members, the group has no members") + @provider.set_members_option + end + + it "should set no options" do + @provider.set_members_option.should eql("") + end + end + + describe "with an empty members array in the new resource and existing members in the current resource" do + before do + @new_resource.stub!(:members).and_return([]) + @current_resource.stub!(:members).and_return(["all", "your", "base"]) + end + + it "should log an appropriate message" do + Chef::Log.should_receive(:debug).with("group[wheel] removing group members all, your, base") + @provider.set_members_option + end + + it "should set the -d option with the members joined by ','" do + @provider.set_members_option.should eql(" -d all,your,base") + end + end + + describe "with supplied members array in the new resource and an empty members array in the current resource" do + before do + @new_resource.stub!(:members).and_return(["all", "your", "base"]) + @current_resource.stub!(:members).and_return([]) + end + + it "should log an appropriate debug message" do + Chef::Log.should_receive(:debug).with("group[wheel] setting group members to all, your, base") + @provider.set_members_option + end + + it "should set the -M option with the members joined by ','" do + @provider.set_members_option.should eql(" -M all,your,base") + end + end + end + + describe"load_current_resource" do + before (:each) do + @provider.load_current_resource + @provider.define_resource_requirements + end + it "should raise an error if the required binary /usr/sbin/pw doesn't exist" do + File.should_receive(:exists?).with("/usr/sbin/pw").and_return(false) + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Group) + end + + it "shouldn't raise an error if /usr/sbin/pw exists" do + File.stub!(:exists?).and_return(true) + lambda { @provider.process_resource_requirements }.should_not raise_error(Chef::Exceptions::Group) + end + end +end diff --git a/spec/unit/provider/group/usermod_spec.rb b/spec/unit/provider/group/usermod_spec.rb new file mode 100644 index 0000000000..6e6e275a7a --- /dev/null +++ b/spec/unit/provider/group/usermod_spec.rb @@ -0,0 +1,95 @@ +# +# Author:: AJ Christensen (<aj@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' + +describe Chef::Provider::Group::Usermod do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Group.new("wheel") + @new_resource.members [ "all", "your", "base" ] + @provider = Chef::Provider::Group::Usermod.new(@new_resource, @run_context) + @provider.stub!(:run_command) + end + + describe "modify_group_members" do + + describe "with an empty members array" do + before do + @new_resource.stub!(:members).and_return([]) + end + + it "should log an appropriate message" do + Chef::Log.should_receive(:debug).with("group[wheel] not changing group members, the group has no members") + @provider.modify_group_members + end + end + + describe "with supplied members" do + platforms = { + "openbsd" => "-G", + "netbsd" => "-G", + "solaris" => "-a -G" + } + + before do + @new_resource.stub!(:members).and_return(["all", "your", "base"]) + File.stub!(:exists?).and_return(true) + end + + it "should raise an error when setting the entire group directly" do + @provider.define_resource_requirements + @provider.load_current_resource + @provider.instance_variable_set("@group_exists", true) + @provider.action = :modify + lambda { @provider.run_action(@provider.process_resource_requirements) }.should raise_error(Chef::Exceptions::Group, "setting group members directly is not supported by #{@provider.to_s}, must set append true in group") + end + + platforms.each do |platform, flags| + it "should usermod each user when the append option is set on #{platform}" do + @node.automatic_attrs[:platform] = platform + @new_resource.stub!(:append).and_return(true) + @provider.should_receive(:run_command).with({:command => "usermod #{flags} wheel all"}) + @provider.should_receive(:run_command).with({:command => "usermod #{flags} wheel your"}) + @provider.should_receive(:run_command).with({:command => "usermod #{flags} wheel base"}) + @provider.modify_group_members + end + end + end + end + + describe "when loading the current resource" do + before(:each) do + File.stub!(:exists?).and_return(false) + @provider.define_resource_requirements + end + + it "should raise an error if the required binary /usr/sbin/usermod doesn't exist" do + File.stub!(:exists?).and_return(true) + File.should_receive(:exists?).with("/usr/sbin/usermod").and_return(false) + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Group) + end + + it "shouldn't raise an error if the required binaries exist" do + File.stub!(:exists?).and_return(true) + lambda { @provider.process_resource_requirements }.should_not raise_error(Chef::Exceptions::Group) + end + end +end diff --git a/spec/unit/provider/group/windows_spec.rb b/spec/unit/provider/group/windows_spec.rb new file mode 100644 index 0000000000..084d1d0acf --- /dev/null +++ b/spec/unit/provider/group/windows_spec.rb @@ -0,0 +1,94 @@ +# +# Author:: Doug MacEachern (<dougm@vmware.com>) +# Copyright:: Copyright (c) 2010 VMware, 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' + +class Chef + class Util + class Windows + class NetGroup + end + end + end +end + +describe Chef::Provider::Group::Windows do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Group.new("staff") + @net_group = mock("Chef::Util::Windows::NetGroup") + Chef::Util::Windows::NetGroup.stub!(:new).and_return(@net_group) + @provider = Chef::Provider::Group::Windows.new(@new_resource, @run_context) + end + + describe "when creating the group" do + it "should call @net_group.local_add" do + @net_group.should_receive(:local_set_members).with([]) + @net_group.should_receive(:local_add) + @provider.create_group + end + end + + describe "manage_group" do + before do + @new_resource.members([ "us" ]) + @current_resource = Chef::Resource::Group.new("staff") + @current_resource.members [ "all", "your", "base" ] + + Chef::Util::Windows::NetGroup.stub!(:new).and_return(@net_group) + @net_group.stub!(:local_add_members) + @net_group.stub!(:local_set_members) + @provider.current_resource = @current_resource + end + + it "should call @net_group.local_set_members" do + @new_resource.stub!(:append).and_return(false) + @net_group.should_receive(:local_set_members).with(@new_resource.members) + @provider.manage_group + end + + it "should call @net_group.local_add_members" do + @new_resource.stub!(:append).and_return(true) + @net_group.should_receive(:local_add_members).with(@new_resource.members) + @provider.manage_group + end + + it "should call @net_group.local_set_members if append fails" do + @new_resource.stub!(:append).and_return(true) + @net_group.stub!(:local_add_members).and_raise(ArgumentError) + @net_group.should_receive(:local_add_members).with(@new_resource.members) + @net_group.should_receive(:local_set_members).with(@new_resource.members + @current_resource.members) + @provider.manage_group + end + + end + + describe "remove_group" do + before do + Chef::Util::Windows::NetGroup.stub!(:new).and_return(@net_group) + @provider.stub!(:run_command).and_return(true) + end + + it "should call @net_group.local_delete" do + @net_group.should_receive(:local_delete) + @provider.remove_group + end + end +end diff --git a/spec/unit/provider/group_spec.rb b/spec/unit/provider/group_spec.rb new file mode 100644 index 0000000000..106a0db14c --- /dev/null +++ b/spec/unit/provider/group_spec.rb @@ -0,0 +1,259 @@ +# +# Author:: AJ Christensen (<aj@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' + +describe Chef::Provider::User do + + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Group.new("wheel", @run_context) + @new_resource.gid 500 + @new_resource.members "aj" + + @provider = Chef::Provider::Group.new(@new_resource, @run_context) + + @current_resource = Chef::Resource::Group.new("aj", @run_context) + @current_resource.gid 500 + @current_resource.members "aj" + + @provider.current_resource = @current_resource + + @pw_group = mock("Struct::Group", + :name => "wheel", + :gid => 20, + :mem => [ "root", "aj" ] + ) + Etc.stub!(:getgrnam).with('wheel').and_return(@pw_group) + end + + it "assumes the group exists by default" do + @provider.group_exists.should be_true + end + + describe "when establishing the current state of the group" do + + it "sets the group name of the current resource to the group name of the new resource" do + @provider.load_current_resource + @provider.current_resource.group_name.should == 'wheel' + end + + it "does not modify the desired gid if set" do + @provider.load_current_resource + @new_resource.gid.should == 500 + end + + it "sets the desired gid to the current gid if none is set" do + @new_resource.instance_variable_set(:@gid, nil) + @provider.load_current_resource + @new_resource.gid.should == 20 + end + + it "looks up the group in /etc/group with getgrnam" do + Etc.should_receive(:getgrnam).with(@new_resource.group_name).and_return(@pw_group) + @provider.load_current_resource + @provider.current_resource.gid.should == 20 + @provider.current_resource.members.should == %w{root aj} + end + + it "should flip the value of exists if it cannot be found in /etc/group" do + Etc.stub!(:getgrnam).and_raise(ArgumentError) + @provider.load_current_resource + @provider.group_exists.should be_false + end + + it "should return the current resource" do + @provider.load_current_resource.should equal(@provider.current_resource) + end + end + + describe "when determining if the system is already in the target state" do + [ :gid, :members ].each do |attribute| + it "should return true if #{attribute} doesn't match" do + @current_resource.stub!(attribute).and_return("looooooooooooooooooool") + @provider.compare_group.should be_true + end + end + + it "should return false if gid and members are equal" do + @provider.compare_group.should be_false + end + + it "should return false if append is true and the group member(s) already exists" do + @current_resource.members << "extra_user" + @new_resource.stub!(:append).and_return(true) + @provider.compare_group.should be_false + end + + it "should return true if append is true and the group member(s) do not already exist" do + @new_resource.members << "extra_user" + @new_resource.stub!(:append).and_return(true) + @provider.compare_group.should be_true + end + + end + + describe "when creating a group" do + it "should call create_group if the group does not exist" do + @provider.group_exists = false + @provider.should_receive(:create_group).and_return(true) + @provider.run_action(:create) + end + + it "should set the the new_resources updated flag when it creates the group" do + @provider.group_exists = false + @provider.stub!(:create_group) + @provider.run_action(:create) + @provider.new_resource.should be_updated + end + + it "should check to see if the group has mismatched attributes if the group exists" do + @provider.group_exists = true + @provider.stub!(:compare_group).and_return(false) + @provider.run_action(:create) + @provider.new_resource.should_not be_updated + end + + it "should call manage_group if the group exists and has mismatched attributes" do + @provider.group_exists = true + @provider.stub!(:compare_group).and_return(true) + @provider.should_receive(:manage_group).and_return(true) + @provider.run_action(:create) + end + + it "should set the the new_resources updated flag when it creates the group if we call manage_group" do + @provider.group_exists = true + @provider.stub!(:compare_group).and_return(true) + @provider.stub!(:manage_group).and_return(true) + @provider.run_action(:create) + @new_resource.should be_updated + end + end + + describe "when removing a group" do + + it "should not call remove_group if the group does not exist" do + @provider.group_exists = false + @provider.should_not_receive(:remove_group) + @provider.run_action(:remove) + @provider.new_resource.should_not be_updated + end + + it "should call remove_group if the group exists" do + @provider.group_exists = true + @provider.should_receive(:remove_group) + @provider.run_action(:remove) + @provider.new_resource.should be_updated + end + end + + describe "when updating a group" do + before(:each) do + @provider.group_exists = true + @provider.stub!(:manage_group).and_return(true) + end + + it "should run manage_group if the group exists and has mismatched attributes" do + @provider.should_receive(:compare_group).and_return(true) + @provider.should_receive(:manage_group).and_return(true) + @provider.run_action(:manage) + end + + it "should set the new resources updated flag to true if manage_group is called" do + @provider.stub!(:compare_group).and_return(true) + @provider.stub!(:manage_group).and_return(true) + @provider.run_action(:manage) + @new_resource.should be_updated + end + + it "should not run manage_group if the group does not exist" do + @provider.group_exists = false + @provider.should_not_receive(:manage_group) + @provider.run_action(:manage) + end + + it "should not run manage_group if the group exists but has no differing attributes" do + @provider.should_receive(:compare_group).and_return(false) + @provider.should_not_receive(:manage_group) + @provider.run_action(:manage) + end + end + + describe "when modifying the group" do + before(:each) do + @provider.group_exists = true + @provider.stub!(:manage_group).and_return(true) + end + + it "should run manage_group if the group exists and has mismatched attributes" do + @provider.should_receive(:compare_group).and_return(true) + @provider.should_receive(:manage_group).and_return(true) + @provider.run_action(:modify) + end + + it "should set the new resources updated flag to true if manage_group is called" do + @provider.stub!(:compare_group).and_return(true) + @provider.stub!(:manage_group).and_return(true) + @provider.run_action(:modify) + @new_resource.should be_updated + end + + it "should not run manage_group if the group exists but has no differing attributes" do + @provider.should_receive(:compare_group).and_return(false) + @provider.should_not_receive(:manage_group) + @provider.run_action(:modify) + end + + it "should raise a Chef::Exceptions::Group if the group doesn't exist" do + @provider.group_exists = false + lambda { @provider.run_action(:modify) }.should raise_error(Chef::Exceptions::Group) + end + end + + describe "when determining the reason for a change" do + it "should report which group members are missing if members are missing and appending to the group" do + @new_resource.members << "user1" + @new_resource.members << "user2" + @new_resource.stub!(:append).and_return true + @provider.compare_group.should be_true + @provider.change_desc.should == "add missing member(s): user1, user2" + end + + it "should report that the group members will be overwritten if not appending" do + @new_resource.members << "user1" + @new_resource.stub!(:append).and_return false + @provider.compare_group.should be_true + @provider.change_desc.should == "replace group members with new list of members" + end + + it "should report the gid will be changed when it does not match" do + @current_resource.stub!(:gid).and_return("BADF00D") + @provider.compare_group.should be_true + @provider.change_desc.should == "change gid #{@current_resource.gid} to #{@new_resource.gid}" + + end + + it "should report no change reason when no change is required" do + @provider.compare_group.should be_false + @provider.change_desc.should == nil + end + end + +end diff --git a/spec/unit/provider/http_request_spec.rb b/spec/unit/provider/http_request_spec.rb new file mode 100644 index 0000000000..26d73cebb4 --- /dev/null +++ b/spec/unit/provider/http_request_spec.rb @@ -0,0 +1,178 @@ +# +# 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' + +describe Chef::Provider::HttpRequest do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::HttpRequest.new('adam') + @new_resource.name "adam" + @new_resource.url "http://www.opscode.com" + @new_resource.message "is cool" + + @provider = Chef::Provider::HttpRequest.new(@new_resource, @run_context) + end + + describe "load_current_resource" do + + it "should set up a Chef::REST client, with no authentication" do + Chef::REST.should_receive(:new).with(@new_resource.url, nil, nil) + @provider.load_current_resource + end + end + + describe "when making REST calls" do + before(:each) do + # run_action(x) forces load_current_resource to run; + # that would overwrite our supplied mock Chef::Rest # object + @provider.stub!(:load_current_resource).and_return(true) + @rest = mock("Chef::REST", :create_url => "http://www.opscode.com", :run_request => "you made it!" ) + @provider.rest = @rest + end + + describe "action_get" do + it "should create the url with a message argument" do + @rest.should_receive(:create_url).with("#{@new_resource.url}?message=#{@new_resource.message}") + @provider.run_action(:get) + end + + it "should inflate a message block at runtime" do + @new_resource.stub!(:message).and_return(lambda { "return" }) + @rest.should_receive(:create_url).with("#{@new_resource.url}?message=return") + @provider.run_action(:get) + end + + it "should run a GET request" do + @rest.should_receive(:run_request).with(:GET, @rest.create_url, {}, false, 10, false) + @provider.run_action(:get) + end + + it "should update the resource" do + @provider.run_action(:get) + @new_resource.should be_updated + end + end + + describe "action_put" do + it "should create the url" do + @rest.should_receive(:create_url).with("#{@new_resource.url}") + @provider.run_action(:put) + end + + it "should run a PUT request with the message as the payload" do + @rest.should_receive(:run_request).with(:PUT, @rest.create_url, {}, @new_resource.message, 10, false) + @provider.run_action(:put) + end + + it "should inflate a message block at runtime" do + @new_resource.stub!(:message).and_return(lambda { "return" }) + @rest.should_receive(:run_request).with(:PUT, @rest.create_url, {}, "return", 10, false) + @provider.run_action(:put) + end + + it "should update the resource" do + @provider.run_action(:put) + @new_resource.should be_updated + end + end + + describe "action_post" do + it "should create the url" do + @rest.should_receive(:create_url).with("#{@new_resource.url}") + @provider.run_action(:post) + end + + it "should run a PUT request with the message as the payload" do + @rest.should_receive(:run_request).with(:POST, @rest.create_url, {}, @new_resource.message, 10, false) + @provider.run_action(:post) + end + + it "should inflate a message block at runtime" do + @new_resource.stub!(:message).and_return(lambda { "return" }) + @rest.should_receive(:run_request).with(:POST, @rest.create_url, {}, "return", 10, false) + @provider.run_action(:post) + end + + it "should update the resource" do + @provider.run_action(:post) + @new_resource.should be_updated + end + end + + describe "action_delete" do + it "should create the url" do + @rest.should_receive(:create_url).with("#{@new_resource.url}") + @provider.run_action(:delete) + end + + it "should run a DELETE request" do + @rest.should_receive(:run_request).with(:DELETE, @rest.create_url, {}, false, 10, false) + @provider.run_action(:delete) + end + + it "should update the resource" do + @provider.run_action(:delete) + @new_resource.should be_updated + end + end + + describe "action_head" do + before do + @rest = mock("Chef::REST", :create_url => "http://www.opscode.com", :run_request => true) + @provider.rest = @rest + end + + it "should create the url with a message argument" do + @rest.should_receive(:create_url).with("#{@new_resource.url}?message=#{@new_resource.message}") + @provider.run_action(:head) + end + + it "should inflate a message block at runtime" do + @new_resource.stub!(:message).and_return(lambda { "return" }) + @rest.should_receive(:create_url).with("#{@new_resource.url}?message=return") + @provider.run_action(:head) + end + + it "should run a HEAD request" do + @rest.should_receive(:run_request).with(:HEAD, @rest.create_url, {}, false, 10, false) + @provider.run_action(:head) + end + + it "should update the resource" do + @provider.run_action(:head) + @new_resource.should be_updated + end + + it "should run a HEAD request with If-Modified-Since header" do + @new_resource.headers "If-Modified-Since" => File.mtime(__FILE__).httpdate + @rest.should_receive(:run_request).with(:HEAD, @rest.create_url, @new_resource.headers, false, 10, false) + @provider.run_action(:head) + end + + it "doesn't call converge_by if HEAD does not return modified" do + @rest.should_receive(:run_request).and_return(false) + @provider.should_not_receive(:converge_by) + @provider.run_action(:head) + end + end + end +end diff --git a/spec/unit/provider/ifconfig_spec.rb b/spec/unit/provider/ifconfig_spec.rb new file mode 100644 index 0000000000..1bf702cf6e --- /dev/null +++ b/spec/unit/provider/ifconfig_spec.rb @@ -0,0 +1,213 @@ +# +# Author:: Prajakta Purohit (prajakta@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 File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "spec_helper")) +require 'spec_helper' +require 'chef/exceptions' + +describe Chef::Provider::Ifconfig do + before do + @node = Chef::Node.new + @cookbook_collection = Chef::CookbookCollection.new([]) + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, @cookbook_collection, @events) + #This new_resource can be called anything --> it is not the same as in ifconfig.rb + @new_resource = Chef::Resource::Ifconfig.new("10.0.0.1", @run_context) + @new_resource.mask "255.255.254.0" + @new_resource.metric "1" + @new_resource.mtu "1500" + @new_resource.device "eth0" + @provider = Chef::Provider::Ifconfig.new(@new_resource, @run_context) + @current_resource = Chef::Resource::Ifconfig.new("10.0.0.1", @run_context) + + status = mock("Status", :exitstatus => 0) + @provider.instance_variable_set("@status", status) + @provider.current_resource = @current_resource + + end + describe Chef::Provider::Ifconfig, "load_current_resource" do + before do + status = mock("Status", :exitstatus => 1) + @provider.should_receive(:popen4).and_return status + @provider.load_current_resource + end + it "should track state of ifconfig failure." do + @provider.instance_variable_get("@status").exitstatus.should_not == 0 + end + it "should thrown an exception when ifconfig fails" do + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error Chef::Exceptions::Ifconfig + end + end + describe Chef::Provider::Ifconfig, "action_add" do + + it "should add an interface if it does not exist" do + #@provider.stub!(:run_command).and_return(true) + @provider.stub!(:load_current_resource) + @current_resource.inet_addr nil + command = "ifconfig eth0 10.0.0.1 netmask 255.255.254.0 metric 1 mtu 1500" + @provider.should_receive(:run_command).with(:command => command) + @provider.should_receive(:generate_config) + + @provider.run_action(:add) + @new_resource.should be_updated + end + + it "should not add an interface if it already exists" do + @provider.stub!(:load_current_resource) + @provider.should_not_receive(:run_command) + @current_resource.inet_addr "10.0.0.1" + @provider.should_receive(:generate_config) + + @provider.run_action(:add) + @new_resource.should_not be_updated + end + + #We are not testing this case with the assumption that anyone writing the cookbook would not make a typo == lo + #it "should add a blank command if the #{@new_resource.device} == lo" do + #end + end + + describe Chef::Provider::Ifconfig, "action_enable" do + + it "should enable interface if does not exist" do + @provider.stub!(:load_current_resource) + @current_resource.inet_addr nil + command = "ifconfig eth0 10.0.0.1 netmask 255.255.254.0 metric 1 mtu 1500" + @provider.should_receive(:run_command).with(:command => command) + @provider.should_not_receive(:generate_config) + + @provider.run_action(:enable) + @new_resource.should be_updated + end + + it "should not enable interface if it already exists" 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.run_action(:enable) + @new_resource.should_not be_updated + end + end + + describe Chef::Provider::Ifconfig, "action_delete" do + + it "should delete interface if it exists" do + @provider.stub!(:load_current_resource) + @current_resource.device "eth0" + command = "ifconfig #{@new_resource.device} down" + @provider.should_receive(:run_command).with(:command => command) + @provider.should_receive(:delete_config) + + @provider.run_action(:delete) + @new_resource.should be_updated + end + + 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.run_action(:delete) + @new_resource.should_not be_updated + end + end + + describe Chef::Provider::Ifconfig, "action_disable" do + + it "should disable interface if it exists" do + @provider.stub!(:load_current_resource) + @current_resource.device "eth0" + command = "ifconfig #{@new_resource.device} down" + @provider.should_receive(:run_command).with(:command => command) + @provider.should_not_receive(:delete_config) + + @provider.run_action(:disable) + @new_resource.should be_updated + end + + 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.run_action(:disable) + @new_resource.should_not be_updated + end + end + + describe Chef::Provider::Ifconfig, "action_delete" do + + it "should delete interface of it exists" do + @provider.stub!(:load_current_resource) + @current_resource.device "eth0" + command = "ifconfig #{@new_resource.device} down" + @provider.should_receive(:run_command).with(:command => command) + @provider.should_receive(:delete_config) + + @provider.run_action(:delete) + @new_resource.should be_updated + end + + it "should not delete interface if it does not exist" do + # This is so that our fake values do not get overwritten + @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.run_action(:delete) + @new_resource.should_not be_updated + end + end + + describe Chef::Provider::Ifconfig, "generate_config for action_add" do + #%w[ centos redhat fedora ].each do |platform| + + it "should write network-script for centos" do + @provider.stub!(:load_current_resource) + @node.automatic_attrs[:platform] = "centos" + @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) + + @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 + + describe Chef::Provider::Ifconfig, "delete_config for action_delete" do + + it "should delete network-script if it exists for centos" do + @node.automatic_attrs[:platform] = "centos" + @current_resource.device "eth0" + @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) + + @provider.run_action(:delete) + end + end +end diff --git a/spec/unit/provider/link_spec.rb b/spec/unit/provider/link_spec.rb new file mode 100644 index 0000000000..bb3f01abbe --- /dev/null +++ b/spec/unit/provider/link_spec.rb @@ -0,0 +1,252 @@ +# +# Author:: AJ Christensen (<aj@junglist.gen.nz>) +# Author:: John Keiser (<jkeiser@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 'ostruct' + +require 'spec_helper' + +if Chef::Platform.windows? + require 'chef/win32/file' #probably need this in spec_helper +end + +describe Chef::Resource::Link do + let(:provider) do + node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, @events) + Chef::Provider::Link.new(new_resource, run_context) + end + let(:new_resource) do + result = Chef::Resource::Link.new("#{CHEF_SPEC_DATA}/fofile-link") + result.to "#{CHEF_SPEC_DATA}/fofile" + result + end + + def canonicalize(path) + Chef::Platform.windows? ? path.gsub('/', '\\') : path + end + + describe "when the target is a symlink" do + before(:each) do + lstat = mock("stats", :ino => 5) + lstat.stub!(:uid).and_return(501) + lstat.stub!(:gid).and_return(501) + lstat.stub!(:mode).and_return(0777) + File.stub!(:lstat).with("#{CHEF_SPEC_DATA}/fofile-link").and_return(lstat) + provider.file_class.stub!(:symlink?).with("#{CHEF_SPEC_DATA}/fofile-link").and_return(true) + provider.file_class.stub!(:readlink).with("#{CHEF_SPEC_DATA}/fofile-link").and_return("#{CHEF_SPEC_DATA}/fofile") + end + + describe "to a file that exists" do + before do + File.stub(:exist?).with("#{CHEF_SPEC_DATA}/fofile-link").and_return(true) + new_resource.owner 501 # only loaded in current_resource if present in new + new_resource.group 501 + provider.load_current_resource + end + + it "should set the symlink target" do + provider.current_resource.target_file.should == "#{CHEF_SPEC_DATA}/fofile-link" + end + it "should set the link type" 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") + end + it "should set the owner" do + provider.current_resource.owner.should == 501 + end + it "should set the group" do + provider.current_resource.group.should == 501 + end + + # We test create in unit tests because there is no other way to ensure + # it does no work. Other create and delete scenarios are covered in + # the functional tests for links. + context 'when the desired state is identical' do + let(:new_resource) do + result = Chef::Resource::Link.new("#{CHEF_SPEC_DATA}/fofile-link") + result.to "#{CHEF_SPEC_DATA}/fofile" + result + end + it 'create does no work' do + provider.access_controls.should_not_receive(:set_all) + provider.run_action(:create) + end + end + end + + describe "to a file that doesn't exist" do + before do + File.stub!(:exist?).with("#{CHEF_SPEC_DATA}/fofile-link").and_return(false) + provider.file_class.stub!(:symlink?).with("#{CHEF_SPEC_DATA}/fofile-link").and_return(true) + provider.file_class.stub!(:readlink).with("#{CHEF_SPEC_DATA}/fofile-link").and_return("#{CHEF_SPEC_DATA}/fofile") + new_resource.owner "501" # only loaded in current_resource if present in new + new_resource.group "501" + provider.load_current_resource + end + + it "should set the symlink target" do + provider.current_resource.target_file.should == "#{CHEF_SPEC_DATA}/fofile-link" + end + it "should set the link type" 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") + end + it "should not set the owner" do + provider.current_resource.owner.should be_nil + end + it "should not set the group" do + provider.current_resource.group.should be_nil + end + end + end + + describe "when the target doesn't exist" do + before do + File.stub!(:exists?).with("#{CHEF_SPEC_DATA}/fofile-link").and_return(false) + provider.file_class.stub!(:symlink?).with("#{CHEF_SPEC_DATA}/fofile-link").and_return(false) + provider.load_current_resource + end + + it "should set the symlink target" do + provider.current_resource.target_file.should == "#{CHEF_SPEC_DATA}/fofile-link" + end + it "should update the source of the existing link to nil" do + provider.current_resource.to.should be_nil + end + it "should not set the owner" do + provider.current_resource.owner.should == nil + end + it "should not set the group" do + provider.current_resource.group.should == nil + end + end + + describe "when the target is a regular old file" do + before do + stat = mock("stats", :ino => 5) + stat.stub!(:uid).and_return(501) + stat.stub!(:gid).and_return(501) + stat.stub!(:mode).and_return(0755) + provider.file_class.stub!(:stat).with("#{CHEF_SPEC_DATA}/fofile-link").and_return(stat) + + File.stub!(:exists?).with("#{CHEF_SPEC_DATA}/fofile-link").and_return(true) + provider.file_class.stub!(:symlink?).with("#{CHEF_SPEC_DATA}/fofile-link").and_return(false) + end + + describe "and the source does not exist" do + before do + File.stub!(:exists?).with("#{CHEF_SPEC_DATA}/fofile").and_return(false) + provider.load_current_resource + end + + it "should set the symlink target" do + provider.current_resource.target_file.should == "#{CHEF_SPEC_DATA}/fofile-link" + end + it "should update the current source of the existing link with an empty string" do + provider.current_resource.to.should == '' + end + it "should not set the owner" do + provider.current_resource.owner.should == nil + end + it "should not set the group" do + provider.current_resource.group.should == nil + end + end + + describe "and the source exists" do + before do + stat = mock("stats", :ino => 6) + stat.stub!(:uid).and_return(502) + stat.stub!(:gid).and_return(502) + stat.stub!(:mode).and_return(0644) + + provider.file_class.stub!(:stat).with("#{CHEF_SPEC_DATA}/fofile").and_return(stat) + + File.stub!(:exists?).with("#{CHEF_SPEC_DATA}/fofile").and_return(true) + provider.load_current_resource + end + + it "should set the symlink target" do + provider.current_resource.target_file.should == "#{CHEF_SPEC_DATA}/fofile-link" + end + it "should update the current source of the existing link with an empty string" do + provider.current_resource.to.should == '' + end + it "should not set the owner" do + provider.current_resource.owner.should == nil + end + it "should not set the group" do + provider.current_resource.group.should == nil + end + end + + describe "and is hardlinked to the source" do + before do + stat = mock("stats", :ino => 5) + stat.stub!(:uid).and_return(502) + stat.stub!(:gid).and_return(502) + stat.stub!(:mode).and_return(0644) + + provider.file_class.stub!(:stat).with("#{CHEF_SPEC_DATA}/fofile").and_return(stat) + + File.stub!(:exists?).with("#{CHEF_SPEC_DATA}/fofile").and_return(true) + provider.load_current_resource + end + + it "should set the symlink target" do + provider.current_resource.target_file.should == "#{CHEF_SPEC_DATA}/fofile-link" + end + it "should set the link type" 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") + end + it "should not set the owner" do + provider.current_resource.owner.should == nil + end + it "should not set the group" do + provider.current_resource.group.should == nil + end + + # We test create in unit tests because there is no other way to ensure + # it does no work. Other create and delete scenarios are covered in + # the functional tests for links. + context 'when the desired state is identical' do + let(:new_resource) do + result = Chef::Resource::Link.new("#{CHEF_SPEC_DATA}/fofile-link") + result.to "#{CHEF_SPEC_DATA}/fofile" + result.link_type :hard + result + end + it 'create does no work' do + provider.file_class.should_not_receive(:symlink) + provider.file_class.should_not_receive(:link) + provider.access_controls.should_not_receive(:set_all) + provider.run_action(:create) + end + end + end + end +end diff --git a/spec/unit/provider/log_spec.rb b/spec/unit/provider/log_spec.rb new file mode 100644 index 0000000000..fe3733240a --- /dev/null +++ b/spec/unit/provider/log_spec.rb @@ -0,0 +1,81 @@ +# +# Author:: Cary Penniman (<cary@rightscale.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' + +describe Chef::Provider::Log::ChefLog do + + before(:each) do + @log_str = "this is my test string to log" + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + end + + it "should be registered with the default platform hash" do + Chef::Platform.platforms[:default][:log].should_not be_nil + end + + it "should write the string to the Chef::Log object at default level (info)" do + @new_resource = Chef::Resource::Log.new(@log_str) + @provider = Chef::Provider::Log::ChefLog.new(@new_resource, @run_context) + Chef::Log.should_receive(:info).with(@log_str).and_return(true) + @provider.action_write + end + + it "should write the string to the Chef::Log object at debug level" do + @new_resource = Chef::Resource::Log.new(@log_str) + @new_resource.level :debug + @provider = Chef::Provider::Log::ChefLog.new(@new_resource, @run_context) + Chef::Log.should_receive(:debug).with(@log_str).and_return(true) + @provider.action_write + end + + it "should write the string to the Chef::Log object at info level" do + @new_resource = Chef::Resource::Log.new(@log_str) + @new_resource.level :info + @provider = Chef::Provider::Log::ChefLog.new(@new_resource, @run_context) + Chef::Log.should_receive(:info).with(@log_str).and_return(true) + @provider.action_write + end + + it "should write the string to the Chef::Log object at warn level" do + @new_resource = Chef::Resource::Log.new(@log_str) + @new_resource.level :warn + @provider = Chef::Provider::Log::ChefLog.new(@new_resource, @run_context) + Chef::Log.should_receive(:warn).with(@log_str).and_return(true) + @provider.action_write + end + + it "should write the string to the Chef::Log object at error level" do + @new_resource = Chef::Resource::Log.new(@log_str) + @new_resource.level :error + @provider = Chef::Provider::Log::ChefLog.new(@new_resource, @run_context) + Chef::Log.should_receive(:error).with(@log_str).and_return(true) + @provider.action_write + end + + it "should write the string to the Chef::Log object at fatal level" do + @new_resource = Chef::Resource::Log.new(@log_str) + @new_resource.level :fatal + @provider = Chef::Provider::Log::ChefLog.new(@new_resource, @run_context) + Chef::Log.should_receive(:fatal).with(@log_str).and_return(true) + @provider.action_write + end + +end diff --git a/spec/unit/provider/mdadm_spec.rb b/spec/unit/provider/mdadm_spec.rb new file mode 100644 index 0000000000..a818c37101 --- /dev/null +++ b/spec/unit/provider/mdadm_spec.rb @@ -0,0 +1,128 @@ +# +# Author:: Joe Williams (<joe@joetify.com>) +# Copyright:: Copyright (c) 2009 Joe Williams +# 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 'ostruct' + +describe Chef::Provider::Mdadm do + + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Mdadm.new('/dev/md1') + @new_resource.devices ["/dev/sdz1","/dev/sdz2"] + @new_resource.level 1 + @new_resource.chunk 256 + + @provider = Chef::Provider::Mdadm.new(@new_resource, @run_context) + end + + describe "when determining the current metadevice status" do + it "should set the current resources mount point to the new resources mount point" do + @provider.stub!(:shell_out!).and_return(OpenStruct.new(:status => 0)) + @provider.load_current_resource + @provider.current_resource.name.should == '/dev/md1' + @provider.current_resource.raid_device.should == '/dev/md1' + end + + it "determines that the metadevice exists when mdadm exit code is zero" do + @provider.stub!(:shell_out!).with("mdadm --detail --test /dev/md1", :returns => [0,4]).and_return(OpenStruct.new(:status => 0)) + @provider.load_current_resource + @provider.current_resource.exists.should be_true + end + + it "determines that the metadevice does not exist when mdadm exit code is 4" do + @provider.stub!(:shell_out!).with("mdadm --detail --test /dev/md1", :returns => [0,4]).and_return(OpenStruct.new(:status => 4)) + @provider.load_current_resource + @provider.current_resource.exists.should be_false + end + end + + describe "after the metadevice status is known" do + before(:each) do + @current_resource = Chef::Resource::Mdadm.new('/dev/md1') + @current_resource.devices ["/dev/sdz1","/dev/sdz2"] + @current_resource.level 1 + @current_resource.chunk 256 + @provider.stub!(:load_current_resource).and_return(true) + @provider.current_resource = @current_resource + end + + describe "when creating the metadevice" do + it "should create the raid device if it doesnt exist" do + @current_resource.exists(false) + expected_command = "yes | mdadm --create /dev/md1 --chunk=256 --level 1 --metadata=0.90 --raid-devices 2 /dev/sdz1 /dev/sdz2" + @provider.should_receive(:shell_out!).with(expected_command) + @provider.run_action(:create) + end + + it "should specify a bitmap only if set" do + @current_resource.exists(false) + @new_resource.bitmap('grow') + expected_command = "yes | mdadm --create /dev/md1 --chunk=256 --level 1 --metadata=0.90 --bitmap=grow --raid-devices 2 /dev/sdz1 /dev/sdz2" + @provider.should_receive(:shell_out!).with(expected_command) + @provider.run_action(:create) + @new_resource.should be_updated_by_last_action + end + + it "should not create the raid device if it does exist" do + @current_resource.exists(true) + @provider.should_not_receive(:shell_out!) + @provider.run_action(:create) + @new_resource.should_not be_updated_by_last_action + end + end + + describe "when asembling the metadevice" do + it "should assemble the raid device if it doesnt exist" do + @current_resource.exists(false) + expected_mdadm_cmd = "yes | mdadm --assemble /dev/md1 /dev/sdz1 /dev/sdz2" + @provider.should_receive(:shell_out!).with(expected_mdadm_cmd) + @provider.run_action(:assemble) + @new_resource.should be_updated_by_last_action + end + + it "should not assemble the raid device if it doesnt exist" do + @current_resource.exists(true) + @provider.should_not_receive(:shell_out!) + @provider.run_action(:assemble) + @new_resource.should_not be_updated_by_last_action + end + end + + describe "when stopping the metadevice" do + + it "should stop the raid device if it exists" do + @current_resource.exists(true) + expected_mdadm_cmd = "yes | mdadm --stop /dev/md1" + @provider.should_receive(:shell_out!).with(expected_mdadm_cmd) + @provider.run_action(:stop) + @new_resource.should be_updated_by_last_action + end + + it "should not attempt to stop the raid device if it does not exist" do + @current_resource.exists(false) + @provider.should_not_receive(:shell_out!) + @provider.run_action(:stop) + @new_resource.should_not be_updated_by_last_action + end + end + end +end diff --git a/spec/unit/provider/mount/mount_spec.rb b/spec/unit/provider/mount/mount_spec.rb new file mode 100644 index 0000000000..c497a08e40 --- /dev/null +++ b/spec/unit/provider/mount/mount_spec.rb @@ -0,0 +1,398 @@ +# +# Author:: Joshua Timberman (<joshua@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 'ostruct' + +describe Chef::Provider::Mount::Mount do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Mount.new("/tmp/foo") + @new_resource.device "/dev/sdz1" + @new_resource.device_type :device + @new_resource.fstype "ext3" + + @new_resource.supports :remount => false + + @provider = Chef::Provider::Mount::Mount.new(@new_resource, @run_context) + + ::File.stub!(:exists?).with("/dev/sdz1").and_return true + ::File.stub!(:exists?).with("/tmp/foo").and_return true + end + + describe "when discovering the current fs state" do + before do + @provider.stub!(:shell_out!).and_return(OpenStruct.new(:stdout => '')) + ::File.stub!(:foreach).with("/etc/fstab") + end + + it "should create a current resource with the same mount point and device" do + @provider.load_current_resource + @provider.current_resource.name.should == '/tmp/foo' + @provider.current_resource.mount_point.should == '/tmp/foo' + @provider.current_resource.device.should == '/dev/sdz1' + end + + it "should accecpt device_type :uuid" do + @new_resource.device_type :uuid + @new_resource.device "d21afe51-a0fe-4dc6-9152-ac733763ae0a" + @stdout_findfs = mock("STDOUT", :first => "/dev/sdz1") + @provider.should_receive(:popen4).with("/sbin/findfs UUID=d21afe51-a0fe-4dc6-9152-ac733763ae0a").and_yield(@pid,@stdin,@stdout_findfs,@stderr).and_return(@status) + @provider.load_current_resource() + @provider.mountable? + end + + describe "when dealing with network mounts" do + { "nfs" => "nfsserver:/vol/path", + "cifs" => "//cifsserver/share" }.each do |type, fs_spec| + it "should detect network fs_spec (#{type})" do + @new_resource.device fs_spec + @provider.network_device?.should be_true + end + + it "should ignore trailing slash and set mounted to true for network mount (#{type})" do + @new_resource.device fs_spec + @provider.stub!(:shell_out!).and_return(OpenStruct.new(:stdout => "#{fs_spec}/ on /tmp/foo type #{type} (rw)\n")) + @provider.load_current_resource + @provider.current_resource.mounted.should be_true + end + end + end + + it "should raise an error if the mount device does not exist" do + ::File.stub!(:exists?).with("/dev/sdz1").and_return false + lambda { @provider.load_current_resource();@provider.mountable? }.should raise_error(Chef::Exceptions::Mount) + end + + it "should not call mountable? with load_current_resource - CHEF-1565" do + ::File.stub!(:exists?).with("/dev/sdz1").and_return false + @provider.should_receive(:mounted?).and_return(true) + @provider.should_receive(:enabled?).and_return(true) + @provider.should_not_receive(:mountable?) + @provider.load_current_resource + end + + it "should raise an error if the mount device (uuid) does not exist" do + @new_resource.device_type :uuid + @new_resource.device "d21afe51-a0fe-4dc6-9152-ac733763ae0a" + status_findfs = mock("Status", :exitstatus => 1) + stdout_findfs = mock("STDOUT", :first => nil) + @provider.should_receive(:popen4).with("/sbin/findfs UUID=d21afe51-a0fe-4dc6-9152-ac733763ae0a").and_yield(@pid,@stdin,stdout_findfs,@stderr).and_return(status_findfs) + ::File.should_receive(:exists?).with("").and_return(false) + lambda { @provider.load_current_resource();@provider.mountable? }.should raise_error(Chef::Exceptions::Mount) + end + + it "should raise an error if the mount point does not exist" do + ::File.stub!(:exists?).with("/tmp/foo").and_return false + lambda { @provider.load_current_resource();@provider.mountable? }.should raise_error(Chef::Exceptions::Mount) + end + + it "does not expect the device to exist for tmpfs" do + @new_resource.fstype("tmpfs") + @new_resource.device("whatever") + lambda { @provider.load_current_resource() }.should_not raise_error + end + + it "does not expect the device to exist for Fuse filesystems" do + @new_resource.fstype("fuse") + @new_resource.device("nilfs#xxx") + lambda { @provider.load_current_resource() }.should_not raise_error + end + + it "should set mounted true if the mount point is found in the mounts list" do + @provider.stub!(:shell_out!).and_return(OpenStruct.new(:stdout => '/dev/sdz1 on /tmp/foo')) + @provider.load_current_resource() + @provider.current_resource.mounted.should be_true + end + + it "should set mounted true if the symlink target of the device is found in the mounts list" do + target = "/dev/mapper/target" + + ::File.stub!(:symlink?).with("#{@new_resource.device}").and_return(true) + ::File.stub!(:readlink).with("#{@new_resource.device}").and_return(target) + + @provider.stub!(:shell_out!).and_return(OpenStruct.new(:stdout => "/dev/mapper/target on /tmp/foo type ext3 (rw)\n")) + @provider.load_current_resource() + @provider.current_resource.mounted.should be_true + end + + it "should set mounted true if the mount point is found last in the mounts list" do + mount = "/dev/sdy1 on #{@new_resource.mount_point} type ext3 (rw)\n" + mount << "#{@new_resource.device} on #{@new_resource.mount_point} type ext3 (rw)\n" + + @provider.stub!(:shell_out!).and_return(OpenStruct.new(:stdout => mount)) + @provider.load_current_resource() + @provider.current_resource.mounted.should be_true + end + + it "should set mounted false if the mount point is not last in the mounts list" do + mount = "#{@new_resource.device} on #{@new_resource.mount_point} type ext3 (rw)\n" + mount << "/dev/sdy1 on #{@new_resource.mount_point} type ext3 (rw)\n" + + @provider.stub!(:shell_out!).and_return(OpenStruct.new(:stdout => mount)) + @provider.load_current_resource() + @provider.current_resource.mounted.should be_false + end + + it "mounted should be false if the mount point is not found in the mounts list" do + @provider.stub!(:shell_out!).and_return(OpenStruct.new(:stdout => "/dev/sdy1 on /tmp/foo type ext3 (rw)\n")) + @provider.load_current_resource() + @provider.current_resource.mounted.should be_false + end + + it "should set enabled to true if the mount point is last in fstab" do + fstab1 = "/dev/sdy1 /tmp/foo ext3 defaults 1 2\n" + fstab2 = "#{@new_resource.device} #{@new_resource.mount_point} ext3 defaults 1 2\n" + + ::File.stub!(:foreach).with("/etc/fstab").and_yield(fstab1).and_yield(fstab2) + + @provider.load_current_resource + @provider.current_resource.enabled.should be_true + end + + it "should set enabled to true if the mount point is not last in fstab and mount_point is a substring of another mount" do + fstab1 = "#{@new_resource.device} #{@new_resource.mount_point} ext3 defaults 1 2\n" + fstab2 = "/dev/sdy1 /tmp/foo/bar ext3 defaults 1 2\n" + + ::File.stub!(:foreach).with("/etc/fstab").and_yield(fstab1).and_yield(fstab2) + + @provider.load_current_resource + @provider.current_resource.enabled.should be_true + end + + it "should set enabled to true if the symlink target is in fstab" do + target = "/dev/mapper/target" + + ::File.stub!(:symlink?).with("#{@new_resource.device}").and_return(true) + ::File.stub!(:readlink).with("#{@new_resource.device}").and_return(target) + + fstab = "/dev/sdz1 /tmp/foo ext3 defaults 1 2\n" + + ::File.stub!(:foreach).with("/etc/fstab").and_yield fstab + + @provider.load_current_resource + @provider.current_resource.enabled.should be_true + end + + it "should set enabled to false if the mount point is not in fstab" do + fstab = "/dev/sdy1 #{@new_resource.mount_point} ext3 defaults 1 2\n" + ::File.stub!(:foreach).with("/etc/fstab").and_yield fstab + + @provider.load_current_resource + @provider.current_resource.enabled.should be_false + end + + it "should ignore commented lines in fstab " do + fstab = "\# #{@new_resource.device} #{@new_resource.mount_point} ext3 defaults 1 2\n" + ::File.stub!(:foreach).with("/etc/fstab").and_yield fstab + + @provider.load_current_resource + @provider.current_resource.enabled.should be_false + end + + it "should set enabled to false if the mount point is not last in fstab" do + line_1 = "#{@new_resource.device} #{@new_resource.mount_point} ext3 defaults 1 2\n" + line_2 = "/dev/sdy1 #{@new_resource.mount_point} ext3 defaults 1 2\n" + ::File.stub!(:foreach).with("/etc/fstab").and_yield(line_1).and_yield(line_2) + + @provider.load_current_resource + @provider.current_resource.enabled.should be_false + end + end + + context "after the mount's state has been discovered" do + before do + @current_resource = Chef::Resource::Mount.new("/tmp/foo") + @current_resource.device "/dev/sdz1" + @current_resource.device_type :device + @current_resource.fstype "ext3" + + @provider.current_resource = @current_resource + end + + describe "mount_fs" do + it "should mount the filesystem if it is not mounted" do + @provider.rspec_reset + @provider.should_receive(:shell_out!).with("mount -t ext3 -o defaults /dev/sdz1 /tmp/foo") + @provider.mount_fs() + end + + it "should mount the filesystem with options if options were passed" do + options = "rw,noexec,noauto" + @new_resource.options(%w{rw noexec noauto}) + @provider.should_receive(:shell_out!).with("mount -t ext3 -o rw,noexec,noauto /dev/sdz1 /tmp/foo") + @provider.mount_fs() + end + + it "should mount the filesystem specified by uuid" do + @new_resource.device "d21afe51-a0fe-4dc6-9152-ac733763ae0a" + @new_resource.device_type :uuid + @stdout_findfs = mock("STDOUT", :first => "/dev/sdz1") + @provider.stub!(:popen4).with("/sbin/findfs UUID=d21afe51-a0fe-4dc6-9152-ac733763ae0a").and_yield(@pid,@stdin,@stdout_findfs,@stderr).and_return(@status) + @stdout_mock = mock('stdout mock') + @stdout_mock.stub!(:each).and_yield("#{@new_resource.device} on #{@new_resource.mount_point}") + @provider.should_receive(:shell_out!).with("mount -t #{@new_resource.fstype} -o defaults -U #{@new_resource.device} #{@new_resource.mount_point}").and_return(@stdout_mock) + @provider.mount_fs() + end + + it "should not mount the filesystem if it is mounted" do + @current_resource.stub!(:mounted).and_return(true) + @provider.should_not_receive(:shell_out!) + @provider.mount_fs() + end + + end + + describe "umount_fs" do + it "should umount the filesystem if it is mounted" do + @current_resource.mounted(true) + @provider.should_receive(:shell_out!).with("umount /tmp/foo") + @provider.umount_fs() + end + + it "should not umount the filesystem if it is not mounted" do + @current_resource.mounted(false) + @provider.should_not_receive(:shell_out!) + @provider.umount_fs() + end + end + + describe "remount_fs" do + it "should use mount -o remount if remount is supported" do + @new_resource.supports({:remount => true}) + @current_resource.mounted(true) + @provider.should_receive(:shell_out!).with("mount -o remount #{@new_resource.mount_point}") + @provider.remount_fs + end + + it "should umount and mount if remount is not supported" do + @new_resource.supports({:remount => false}) + @current_resource.mounted(true) + @provider.should_receive(:umount_fs) + @provider.should_receive(:sleep).with(1) + @provider.should_receive(:mount_fs) + @provider.remount_fs() + end + + it "should not try to remount at all if mounted is false" do + @current_resource.mounted(false) + @provider.should_not_receive(:shell_out!) + @provider.should_not_receive(:umount_fs) + @provider.should_not_receive(:mount_fs) + @provider.remount_fs() + end + end + + describe "when enabling the fs" do + it "should enable if enabled isn't true" do + @current_resource.enabled(false) + + @fstab = StringIO.new + ::File.stub!(:open).with("/etc/fstab", "a").and_yield(@fstab) + @provider.enable_fs + @fstab.string.should match(%r{^/dev/sdz1\s+/tmp/foo\s+ext3\s+defaults\s+0\s+2\s*$}) + end + + it "should not enable if enabled is true and resources match" do + @current_resource.enabled(true) + @current_resource.fstype("ext3") + @current_resource.options(["defaults"]) + @current_resource.dump(0) + @current_resource.pass(2) + ::File.should_not_receive(:open).with("/etc/fstab", "a") + + @provider.enable_fs + end + + it "should enable if enabled is true and resources do not match" do + @current_resource.enabled(true) + @current_resource.fstype("auto") + @current_resource.options(["defaults"]) + @current_resource.dump(0) + @current_resource.pass(2) + @fstab = StringIO.new + ::File.stub(:readlines).and_return([]) + ::File.should_receive(:open).once.with("/etc/fstab", "w").and_yield(@fstab) + ::File.should_receive(:open).once.with("/etc/fstab", "a").and_yield(@fstab) + + @provider.enable_fs + end + end + + describe "when disabling the fs" do + it "should disable if enabled is true" do + @current_resource.enabled(true) + + other_mount = "/dev/sdy1 /tmp/foo ext3 defaults 1 2\n" + this_mount = "/dev/sdz1 /tmp/foo ext3 defaults 1 2\n" + + @fstab_read = [this_mount, other_mount] + ::File.stub!(:readlines).with("/etc/fstab").and_return(@fstab_read) + @fstab_write = StringIO.new + ::File.stub!(:open).with("/etc/fstab", "w").and_yield(@fstab_write) + + @provider.disable_fs + @fstab_write.string.should match(Regexp.escape(other_mount)) + @fstab_write.string.should_not match(Regexp.escape(this_mount)) + end + + it "should disable if enabled is true and ignore commented lines" do + @current_resource.enabled(true) + + fstab_read = [%q{/dev/sdy1 /tmp/foo ext3 defaults 1 2}, + %q{/dev/sdz1 /tmp/foo ext3 defaults 1 2}, + %q{#/dev/sdz1 /tmp/foo ext3 defaults 1 2}] + fstab_write = StringIO.new + + ::File.stub!(:readlines).with("/etc/fstab").and_return(fstab_read) + ::File.stub!(:open).with("/etc/fstab", "w").and_yield(fstab_write) + + @provider.disable_fs + fstab_write.string.should match(%r{^/dev/sdy1 /tmp/foo ext3 defaults 1 2$}) + fstab_write.string.should match(%r{^#/dev/sdz1 /tmp/foo ext3 defaults 1 2$}) + fstab_write.string.should_not match(%r{^/dev/sdz1 /tmp/foo ext3 defaults 1 2$}) + end + + it "should disable only the last entry if enabled is true" do + @current_resource.stub!(:enabled).and_return(true) + fstab_read = ["/dev/sdz1 /tmp/foo ext3 defaults 1 2\n", + "/dev/sdy1 /tmp/foo ext3 defaults 1 2\n", + "/dev/sdz1 /tmp/foo ext3 defaults 1 2\n"] + + fstab_write = StringIO.new + ::File.stub!(:readlines).with("/etc/fstab").and_return(fstab_read) + ::File.stub!(:open).with("/etc/fstab", "w").and_yield(fstab_write) + + @provider.disable_fs + fstab_write.string.should == "/dev/sdz1 /tmp/foo ext3 defaults 1 2\n/dev/sdy1 /tmp/foo ext3 defaults 1 2\n" + end + + it "should not disable if enabled is false" do + @current_resource.stub!(:enabled).and_return(false) + + ::File.stub!(:readlines).with("/etc/fstab").and_return([]) + ::File.should_not_receive(:open).and_yield(@fstab) + + @provider.disable_fs + end + end + end +end diff --git a/spec/unit/provider/mount/windows_spec.rb b/spec/unit/provider/mount/windows_spec.rb new file mode 100644 index 0000000000..f173ebba22 --- /dev/null +++ b/spec/unit/provider/mount/windows_spec.rb @@ -0,0 +1,134 @@ +# +# Author:: Doug MacEachern (<dougm@vmware.com>) +# Copyright:: Copyright (c) 2010 VMware, 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' + +class Chef + class Util + class Windows + class NetUse + end + class Volume + end + end + end +end + +GUID = "\\\\?\\Volume{578e72b5-6e70-11df-b5c5-000c29d4a7d9}\\" +REMOTE = "\\\\server-name\\path" + +describe Chef::Provider::Mount::Windows do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Mount.new("X:") + @new_resource.device GUID + @current_resource = Chef::Resource::Mount.new("X:") + Chef::Resource::Mount.stub!(:new).and_return(@current_resource) + + @net_use = mock("Chef::Util::Windows::NetUse") + Chef::Util::Windows::NetUse.stub!(:new).and_return(@net_use) + @vol = mock("Chef::Util::Windows::Volume") + Chef::Util::Windows::Volume.stub!(:new).and_return(@vol) + + @provider = Chef::Provider::Mount::Windows.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + end + + describe "when loading the current resource" do + it "should set mounted true if the mount point is found" do + @vol.stub!(:device).and_return(@new_resource.device) + @current_resource.should_receive(:mounted).with(true) + @provider.load_current_resource + end + + it "should set mounted false if the mount point is not found" do + @vol.stub!(:device).and_raise(ArgumentError) + @current_resource.should_receive(:mounted).with(false) + @provider.load_current_resource + end + + describe "with a local device" do + before do + @new_resource.device GUID + @vol.stub!(:device).and_return(@new_resource.device) + @net_use.stub!(:device).and_raise(ArgumentError) + end + + it "should determine the device is a volume GUID" do + @provider.should_receive(:is_volume).with(@new_resource.device).and_return(true) + @provider.load_current_resource + end + end + + describe "with a remote device" do + before do + @new_resource.device REMOTE + @net_use.stub!(:device).and_return(@new_resource.device) + @vol.stub!(:device).and_raise(ArgumentError) + end + + it "should determine the device is remote" do + @provider.should_receive(:is_volume).with(@new_resource.device).and_return(false) + @provider.load_current_resource + end + end + + describe "when mounting a file system" do + before do + @new_resource.device GUID + @vol.stub!(:add) + @vol.stub!(:device).and_raise(ArgumentError) + @provider.load_current_resource + end + + it "should mount the filesystem if it is not mounted" do + @vol.should_receive(:add).with(@new_resource.device) + @provider.mount_fs + end + + it "should not mount the filesystem if it is mounted" do + @vol.should_not_receive(:add) + @current_resource.stub!(:mounted).and_return(true) + @provider.mount_fs + end + end + + describe "when unmounting a file system" do + before do + @new_resource.device GUID + @vol.stub!(:delete) + @vol.stub!(:device).and_raise(ArgumentError) + @provider.load_current_resource + end + + it "should umount the filesystem if it is mounted" do + @current_resource.stub!(:mounted).and_return(true) + @vol.should_receive(:delete) + @provider.umount_fs + end + + it "should not umount the filesystem if it is not mounted" do + @current_resource.stub!(:mounted).and_return(false) + @vol.should_not_receive(:delete) + @provider.umount_fs + end + end + end +end diff --git a/spec/unit/provider/mount_spec.rb b/spec/unit/provider/mount_spec.rb new file mode 100644 index 0000000000..921bde4cc9 --- /dev/null +++ b/spec/unit/provider/mount_spec.rb @@ -0,0 +1,160 @@ +# +# Author:: Joshua Timberman (<joshua@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' + +describe Chef::Provider::Mount do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Mount.new('/tmp/foo') + @new_resource.device "/dev/sdz1" + @new_resource.name "/tmp/foo" + @new_resource.mount_point "/tmp/foo" + @new_resource.fstype "ext3" + + @current_resource = Chef::Resource::Mount.new('/tmp/foo') + @current_resource.device "/dev/sdz1" + @current_resource.name "/tmp/foo" + @current_resource.mount_point "/tmp/foo" + @current_resource.fstype "ext3" + + @provider = Chef::Provider::Mount.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + end + + describe "when the target state is a mounted filesystem" do + + it "should mount the filesystem if it isn't mounted" do + @current_resource.stub!(:mounted).and_return(false) + @provider.should_receive(:mount_fs).with.and_return(true) + @provider.run_action(:mount) + @new_resource.should be_updated_by_last_action + end + + it "should not mount the filesystem if it is mounted" do + @current_resource.stub!(:mounted).and_return(true) + @provider.should_not_receive(:mount_fs).and_return(true) + @provider.run_action(:mount) + @new_resource.should_not be_updated_by_last_action + end + + end + + describe "when the target state is an unmounted filesystem" do + it "should umount the filesystem if it is mounted" do + @current_resource.stub!(:mounted).and_return(true) + @provider.should_receive(:umount_fs).with.and_return(true) + @provider.run_action(:umount) + @new_resource.should be_updated_by_last_action + end + + it "should not umount the filesystem if it is not mounted" do + @current_resource.stub!(:mounted).and_return(false) + @provider.should_not_receive(:umount_fs).and_return(true) + @provider.run_action(:umount) + @new_resource.should_not be_updated_by_last_action + end + end + + describe "when the filesystem should be remounted and the resource supports remounting" do + before do + @new_resource.supports[:remount] = true + end + + it "should remount the filesystem if it is mounted" do + @current_resource.stub!(:mounted).and_return(true) + @provider.should_receive(:remount_fs).and_return(true) + @provider.run_action(:remount) + @new_resource.should be_updated_by_last_action + end + + it "should not remount the filesystem if it is not mounted" do + @current_resource.stub!(:mounted).and_return(false) + @provider.should_not_receive(:remount_fs) + @provider.run_action(:remount) + @new_resource.should_not be_updated_by_last_action + end + end + describe "when the filesystem should be remounted and the resource does not support remounting" do + before do + @new_resource.supports[:remount] = false + end + + it "should fail to remount the filesystem" do + @provider.should_not_receive(:remount_fs) + lambda {@provider.run_action(:remount)}.should raise_error(Chef::Exceptions::UnsupportedAction) + @new_resource.should_not be_updated_by_last_action + end + + end + describe "when enabling the filesystem to be mounted" do + it "should enable the mount if it isn't enable" do + @current_resource.stub!(:enabled).and_return(false) + @provider.should_receive(:enable_fs).with.and_return(true) + @provider.run_action(:enable) + @new_resource.should be_updated_by_last_action + end + + it "should not enable the mount if it is enabled" do + @current_resource.stub!(:enabled).and_return(true) + @provider.should_not_receive(:enable_fs).with.and_return(true) + @provider.run_action(:enable) + @new_resource.should_not be_updated_by_last_action + end + end + + describe "when the target state is to disable the mount" do + it "should disable the mount if it is enabled" do + @current_resource.stub!(:enabled).and_return(true) + @provider.should_receive(:disable_fs).with.and_return(true) + @provider.run_action(:disable) + @new_resource.should be_updated_by_last_action + end + + it "should not disable the mount if it isn't enabled" do + @current_resource.stub!(:enabled).and_return(false) + @provider.should_not_receive(:disable_fs).with.and_return(true) + @provider.run_action(:disable) + @new_resource.should_not be_updated_by_last_action + end + end + + + it "should delegates the mount implementation to subclasses" do + lambda { @provider.mount_fs }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "should delegates the umount implementation to subclasses" do + lambda { @provider.umount_fs }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "should delegates the remount implementation to subclasses" do + lambda { @provider.remount_fs }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "should delegates the enable implementation to subclasses" do + lambda { @provider.enable_fs }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "should delegates the disable implementation to subclasses" do + lambda { @provider.disable_fs }.should raise_error(Chef::Exceptions::UnsupportedAction) + end +end diff --git a/spec/unit/provider/ohai_spec.rb b/spec/unit/provider/ohai_spec.rb new file mode 100644 index 0000000000..8402c92e97 --- /dev/null +++ b/spec/unit/provider/ohai_spec.rb @@ -0,0 +1,85 @@ +# +# Author:: Michael Leinartas (<mleinartas@gmail.com>) +# Copyright:: Copyright (c) 2010 Michael Leinartas +# 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/run_context' + +describe Chef::Provider::Ohai do + before(:each) do + # Copied from client_spec + @fqdn = "hostname.domainname" + @hostname = "hostname" + @platform = "example-platform" + @platform_version = "example-platform" + Chef::Config[:node_name] = @fqdn + mock_ohai = { + :fqdn => @fqdn, + :hostname => @hostname, + :platform => @platform, + :platform_version => @platform_version, + :data => { + :origdata => "somevalue" + }, + :data2 => { + :origdata => "somevalue", + :newdata => "somevalue" + } + } + mock_ohai.stub!(:all_plugins).and_return(true) + mock_ohai.stub!(:require_plugin).and_return(true) + mock_ohai.stub!(:data).and_return(mock_ohai[:data], + mock_ohai[:data2]) + Ohai::System.stub!(:new).and_return(mock_ohai) + Chef::Platform.stub!(:find_platform_and_version).and_return({ "platform" => @platform, + "platform_version" => @platform_version}) + # Fake node with a dummy save + @node = Chef::Node.new + @node.name(@fqdn) + @node.stub!(:save).and_return(@node) + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Ohai.new("ohai_reload") + ohai = Ohai::System.new + ohai.all_plugins + @node.consume_external_attrs(ohai.data,{}) + + @provider = Chef::Provider::Ohai.new(@new_resource, @run_context) + end + + describe "when reloading ohai" do + before do + @node.automatic_attrs[:origdata] = 'somevalue' + end + + it "applies updated ohai data to the node" do + @node[:origdata].should == 'somevalue' + @node[:newdata].should be_nil + @provider.run_action(:reload) + @node[:origdata].should == 'somevalue' + @node[:newdata].should == 'somevalue' + end + + it "should reload a specific plugin and cause node to pick up new values" do + @new_resource.plugin "someplugin" + @provider.run_action(:reload) + @node[:origdata].should == 'somevalue' + @node[:newdata].should == 'somevalue' + end + end +end diff --git a/spec/unit/provider/package/apt_spec.rb b/spec/unit/provider/package/apt_spec.rb new file mode 100644 index 0000000000..06ada5189e --- /dev/null +++ b/spec/unit/provider/package/apt_spec.rb @@ -0,0 +1,351 @@ +# +# 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 'ostruct' + +describe Chef::Provider::Package::Apt do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Package.new("irssi", @run_context) + @current_resource = Chef::Resource::Package.new("irssi", @run_context) + + @status = mock("Status", :exitstatus => 0) + @provider = Chef::Provider::Package::Apt.new(@new_resource, @run_context) + Chef::Resource::Package.stub!(:new).and_return(@current_resource) + @stdin = StringIO.new + @stdout =<<-PKG_STATUS +irssi: + Installed: (none) + Candidate: 0.8.14-1ubuntu4 + Version table: + 0.8.14-1ubuntu4 0 + 500 http://us.archive.ubuntu.com/ubuntu/ lucid/main Packages +PKG_STATUS + @stderr = StringIO.new + @pid = 12345 + @shell_out = OpenStruct.new(:stdout => @stdout,:stdin => @stdin,:stderr => @stderr,:status => @status,:exitstatus => 0) + end + + describe "when loading current resource" do + + it "should create a current resource with the name of the new_resource" do + @provider.should_receive(:shell_out!).and_return(@shell_out) + Chef::Resource::Package.should_receive(:new).and_return(@current_resource) + @provider.load_current_resource + end + + it "should set the current resources package name to the new resources package name" do + @provider.should_receive(:shell_out!).and_return(@shell_out) + @current_resource.should_receive(:package_name).with(@new_resource.package_name) + @provider.load_current_resource + end + + it "should run apt-cache policy with the package name" do + @provider.should_receive(:shell_out!).with("apt-cache policy #{@new_resource.package_name}").and_return(@shell_out) + @provider.load_current_resource + end + + it "should set the installed version to nil on the current resource if package state is not installed" do + @provider.should_receive(:shell_out!).and_return(@shell_out) + @current_resource.should_receive(:version).with(nil).and_return(true) + @provider.load_current_resource + end + + it "should set the installed version if package has one" do + @stdout.replace(<<-INSTALLED) +sudo: + Installed: 1.7.2p1-1ubuntu5.3 + Candidate: 1.7.2p1-1ubuntu5.3 + Version table: + *** 1.7.2p1-1ubuntu5.3 0 + 500 http://us.archive.ubuntu.com/ubuntu/ lucid-updates/main Packages + 500 http://security.ubuntu.com/ubuntu/ lucid-security/main Packages + 100 /var/lib/dpkg/status + 1.7.2p1-1ubuntu5 0 + 500 http://us.archive.ubuntu.com/ubuntu/ lucid/main Packages +INSTALLED + @provider.should_receive(:shell_out!).and_return(@shell_out) + @provider.load_current_resource + @current_resource.version.should == "1.7.2p1-1ubuntu5.3" + @provider.candidate_version.should eql("1.7.2p1-1ubuntu5.3") + end + + it "should return the current resouce" do + @provider.should_receive(:shell_out!).and_return(@shell_out) + @provider.load_current_resource.should eql(@current_resource) + end + + # libmysqlclient-dev is a real package in newer versions of debian + ubuntu + # list of virtual packages: http://www.debian.org/doc/packaging-manuals/virtual-package-names-list.txt + it "should not install the virtual package there is a single provider package and it is installed" do + @new_resource.package_name("libmysqlclient15-dev") + virtual_package_out=<<-VPKG_STDOUT +libmysqlclient15-dev: + Installed: (none) + Candidate: (none) + Version table: +VPKG_STDOUT + virtual_package = mock(:stdout => virtual_package_out,:exitstatus => 0) + @provider.should_receive(:shell_out!).with("apt-cache policy libmysqlclient15-dev").and_return(virtual_package) + showpkg_out =<<-SHOWPKG_STDOUT +Package: libmysqlclient15-dev +Versions: + +Reverse Depends: + libmysqlclient-dev,libmysqlclient15-dev + libmysqlclient-dev,libmysqlclient15-dev + libmysqlclient-dev,libmysqlclient15-dev + libmysqlclient-dev,libmysqlclient15-dev + libmysqlclient-dev,libmysqlclient15-dev + libmysqlclient-dev,libmysqlclient15-dev +Dependencies: +Provides: +Reverse Provides: +libmysqlclient-dev 5.1.41-3ubuntu12.7 +libmysqlclient-dev 5.1.41-3ubuntu12.10 +libmysqlclient-dev 5.1.41-3ubuntu12 +SHOWPKG_STDOUT + showpkg = mock(:stdout => showpkg_out,:exitstatus => 0) + @provider.should_receive(:shell_out!).with("apt-cache showpkg libmysqlclient15-dev").and_return(showpkg) + real_package_out=<<-RPKG_STDOUT +libmysqlclient-dev: + Installed: 5.1.41-3ubuntu12.10 + Candidate: 5.1.41-3ubuntu12.10 + Version table: + *** 5.1.41-3ubuntu12.10 0 + 500 http://us.archive.ubuntu.com/ubuntu/ lucid-updates/main Packages + 100 /var/lib/dpkg/status + 5.1.41-3ubuntu12.7 0 + 500 http://security.ubuntu.com/ubuntu/ lucid-security/main Packages + 5.1.41-3ubuntu12 0 + 500 http://us.archive.ubuntu.com/ubuntu/ lucid/main Packages +RPKG_STDOUT + real_package = mock(:stdout => real_package_out,:exitstatus => 0) + @provider.should_receive(:shell_out!).with("apt-cache policy libmysqlclient-dev").and_return(real_package) + @provider.should_not_receive(:run_command_with_systems_locale) + @provider.load_current_resource + end + + it "should raise an exception if you specify a virtual package with multiple provider packages" do + @new_resource.package_name("mp3-decoder") + virtual_package_out=<<-VPKG_STDOUT +mp3-decoder: + Installed: (none) + Candidate: (none) + Version table: +VPKG_STDOUT + virtual_package = mock(:stdout => virtual_package_out,:exitstatus => 0) + @provider.should_receive(:shell_out!).with("apt-cache policy mp3-decoder").and_return(virtual_package) + showpkg_out=<<-SHOWPKG_STDOUT +Package: mp3-decoder +Versions: + +Reverse Depends: + nautilus,mp3-decoder + vux,mp3-decoder + plait,mp3-decoder + ecasound,mp3-decoder + nautilus,mp3-decoder +Dependencies: +Provides: +Reverse Provides: +vlc-nox 1.0.6-1ubuntu1.8 +vlc 1.0.6-1ubuntu1.8 +vlc-nox 1.0.6-1ubuntu1 +vlc 1.0.6-1ubuntu1 +opencubicplayer 1:0.1.17-2 +mpg321 0.2.10.6 +mpg123 1.12.1-0ubuntu1 +SHOWPKG_STDOUT + showpkg = mock(:stdout => showpkg_out,:exitstatus => 0) + @provider.should_receive(:shell_out!).with("apt-cache showpkg mp3-decoder").and_return(showpkg) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Package) + end + + it "should run apt-cache policy with the default_release option, if there is one and provider is explicitly defined" do + @new_resource = Chef::Resource::AptPackage.new("irssi", @run_context) + @provider = Chef::Provider::Package::Apt.new(@new_resource, @run_context) + + @new_resource.stub!(:default_release).and_return("lenny-backports") + @new_resource.stub!(:provider).and_return("Chef::Provider::Package::Apt") + @provider.should_receive(:shell_out!).with("apt-cache -o APT::Default-Release=lenny-backports policy irssi").and_return(@shell_out) + @provider.load_current_resource + end + + end + + describe "install_package" do + it "should run apt-get install with the package name and version" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "apt-get -q -y install irssi=0.8.12-7", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @provider.install_package("irssi", "0.8.12-7") + end + + it "should run apt-get install with the package name and version and options if specified" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "apt-get -q -y --force-yes install irssi=0.8.12-7", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @new_resource.stub!(:options).and_return("--force-yes") + + @provider.install_package("irssi", "0.8.12-7") + end + + it "should run apt-get install with the package name and version and default_release if there is one and provider is explicitly defined" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "apt-get -q -y -o APT::Default-Release=lenny-backports install irssi=0.8.12-7", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @new_resource.stub!(:default_release).and_return("lenny-backports") + @new_resource.stub!(:provider).and_return("Chef::Provider::Package::Apt") + + @provider.install_package("irssi", "0.8.12-7") + end + end + + describe Chef::Provider::Package::Apt, "upgrade_package" do + + it "should run install_package with the name and version" do + @provider.should_receive(:install_package).with("irssi", "0.8.12-7") + @provider.upgrade_package("irssi", "0.8.12-7") + end + end + + describe Chef::Provider::Package::Apt, "remove_package" do + + it "should run apt-get remove with the package name" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "apt-get -q -y remove irssi", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @provider.remove_package("irssi", "0.8.12-7") + end + + it "should run apt-get remove with the package name and options if specified" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "apt-get -q -y --force-yes remove irssi", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @new_resource.stub!(:options).and_return("--force-yes") + + @provider.remove_package("irssi", "0.8.12-7") + end + end + + describe "when purging a package" do + + it "should run apt-get purge with the package name" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "apt-get -q -y purge irssi", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @provider.purge_package("irssi", "0.8.12-7") + end + + it "should run apt-get purge with the package name and options if specified" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "apt-get -q -y --force-yes purge irssi", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @new_resource.stub!(:options).and_return("--force-yes") + + @provider.purge_package("irssi", "0.8.12-7") + end + end + + describe "when preseeding a package" do + before(:each) do + @provider.stub!(:get_preseed_file).and_return("/tmp/irssi-0.8.12-7.seed") + @provider.stub!(:run_command_with_systems_locale).and_return(true) + end + + it "should get the full path to the preseed response file" do + @provider.should_receive(:get_preseed_file).with("irssi", "0.8.12-7").and_return("/tmp/irssi-0.8.12-7.seed") + file = @provider.get_preseed_file("irssi", "0.8.12-7") + @provider.preseed_package(file) + end + + it "should run debconf-set-selections on the preseed file if it has changed" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "debconf-set-selections /tmp/irssi-0.8.12-7.seed", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }).and_return(true) + file = @provider.get_preseed_file("irssi", "0.8.12-7") + @provider.preseed_package(file) + end + + it "should not run debconf-set-selections if the preseed file has not changed" do + @provider.stub(:check_package_state) + @current_resource.version "0.8.11" + @new_resource.response_file "/tmp/file" + @provider.stub!(:get_preseed_file).and_return(false) + @provider.should_not_receive(:run_command_with_systems_locale) + @provider.run_action(:reconfig) + end + end + + describe "when reconfiguring a package" do + before(:each) do + @provider.stub!(:run_command_with_systems_locale).and_return(true) + end + + it "should run dpkg-reconfigure package" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "dpkg-reconfigure irssi", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }).and_return(true) + @provider.reconfig_package("irssi", "0.8.12-7") + end + end + + describe "when installing a virtual package" do + it "should install the package without specifying a version" do + @provider.is_virtual_package = true + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "apt-get -q -y install libmysqlclient-dev", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @provider.install_package("libmysqlclient-dev", "not_a_real_version") + end + end +end diff --git a/spec/unit/provider/package/dpkg_spec.rb b/spec/unit/provider/package/dpkg_spec.rb new file mode 100644 index 0000000000..aa22e0a2a3 --- /dev/null +++ b/spec/unit/provider/package/dpkg_spec.rb @@ -0,0 +1,216 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org) +# Copyright:: Copyright (c) 2009 Bryan McLellan +# 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::Provider::Package::Dpkg do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Package.new("wget") + @new_resource.source "/tmp/wget_1.11.4-1ubuntu1_amd64.deb" + + @provider = Chef::Provider::Package::Dpkg.new(@new_resource, @run_context) + + @stdin = StringIO.new + @stdout = StringIO.new + @status = mock("Status", :exitstatus => 0) + @stderr = StringIO.new + @pid = mock("PID") + @provider.stub!(:popen4).and_return(@status) + + ::File.stub!(:exists?).and_return(true) + end + + describe "when loading the current resource state" do + + it "should create a current resource with the name of the new_resource" do + @provider.load_current_resource + @provider.current_resource.package_name.should == "wget" + end + + it "should raise an exception if a source is supplied but not found" do + @provider.load_current_resource + @provider.define_resource_requirements + ::File.stub!(:exists?).and_return(false) + lambda { @provider.run_action(:install) }.should raise_error(Chef::Exceptions::Package) + end + + describe 'gets the source package version from dpkg-deb' do + def check_version(version) + @stdout = StringIO.new("wget\t#{version}") + @provider.stub!(:popen4).with("dpkg-deb -W #{@new_resource.source}").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.load_current_resource + @provider.current_resource.package_name.should == "wget" + @new_resource.version.should == version + end + + it 'if short version provided' do + check_version('1.11.4') + end + + it 'if extended version provided' do + check_version('1.11.4-1ubuntu1') + end + + it 'if distro-specific version provided' do + check_version('1.11.4-1ubuntu1~lucid') + end + end + + it "gets the source package name from dpkg-deb correctly when the package name has `-', `+' or `.' characters" do + @stdout = StringIO.new("f.o.o-pkg++2\t1.11.4-1ubuntu1") + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.load_current_resource + @provider.current_resource.package_name.should == "f.o.o-pkg++2" + end + + it "should raise an exception if the source is not set but we are installing" do + @new_resource = Chef::Resource::Package.new("wget") + @provider.new_resource = @new_resource + @provider.define_resource_requirements + @provider.load_current_resource + lambda { @provider.run_action(:install)}.should raise_error(Chef::Exceptions::Package) + end + + it "should return the current version installed if found by dpkg" do + @stdout = StringIO.new(<<-DPKG_S) +Package: wget +Status: install ok installed +Priority: important +Section: web +Installed-Size: 1944 +Maintainer: Ubuntu Core developers <ubuntu-devel-discuss@lists.ubuntu.com> +Architecture: amd64 +Version: 1.11.4-1ubuntu1 +Config-Version: 1.11.4-1ubuntu1 +Depends: libc6 (>= 2.8~20080505), libssl0.9.8 (>= 0.9.8f-5) +Conflicts: wget-ssl +DPKG_S + @provider.stub!(:popen4).with("dpkg -s wget").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + + @provider.load_current_resource + @provider.current_resource.version.should == "1.11.4-1ubuntu1" + end + + it "should raise an exception if dpkg fails to run" do + @status = mock("Status", :exitstatus => -1) + @provider.stub!(:popen4).and_return(@status) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Package) + end + end + + describe Chef::Provider::Package::Dpkg, "install and upgrade" do + it "should run dpkg -i with the package source" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "dpkg -i /tmp/wget_1.11.4-1ubuntu1_amd64.deb", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @provider.install_package("wget", "1.11.4-1ubuntu1") + end + + it "should run dpkg -i if the package is a path and the source is nil" do + @new_resource = Chef::Resource::Package.new("/tmp/wget_1.11.4-1ubuntu1_amd64.deb") + @provider = Chef::Provider::Package::Dpkg.new(@new_resource, @run_context) + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "dpkg -i /tmp/wget_1.11.4-1ubuntu1_amd64.deb", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @provider.install_package("/tmp/wget_1.11.4-1ubuntu1_amd64.deb", "1.11.4-1ubuntu1") + end + + it "should run dpkg -i if the package is a path and the source is nil for an upgrade" do + @new_resource = Chef::Resource::Package.new("/tmp/wget_1.11.4-1ubuntu1_amd64.deb") + @provider = Chef::Provider::Package::Dpkg.new(@new_resource, @run_context) + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "dpkg -i /tmp/wget_1.11.4-1ubuntu1_amd64.deb", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @provider.upgrade_package("/tmp/wget_1.11.4-1ubuntu1_amd64.deb", "1.11.4-1ubuntu1") + end + + it "should run dpkg -i with the package source and options if specified" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "dpkg -i --force-yes /tmp/wget_1.11.4-1ubuntu1_amd64.deb", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @new_resource.stub!(:options).and_return("--force-yes") + + @provider.install_package("wget", "1.11.4-1ubuntu1") + end + it "should upgrade by running install_package" do + @provider.should_receive(:install_package).with("wget", "1.11.4-1ubuntu1") + @provider.upgrade_package("wget", "1.11.4-1ubuntu1") + end + end + + describe Chef::Provider::Package::Dpkg, "remove and purge" do + it "should run dpkg -r to remove the package" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "dpkg -r wget", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @provider.remove_package("wget", "1.11.4-1ubuntu1") + end + + it "should run dpkg -r to remove the package with options if specified" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "dpkg -r --force-yes wget", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @new_resource.stub!(:options).and_return("--force-yes") + + @provider.remove_package("wget", "1.11.4-1ubuntu1") + end + + it "should run dpkg -P to purge the package" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "dpkg -P wget", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @provider.purge_package("wget", "1.11.4-1ubuntu1") + end + + it "should run dpkg -P to purge the package with options if specified" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "dpkg -P --force-yes wget", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @new_resource.stub!(:options).and_return("--force-yes") + + @provider.purge_package("wget", "1.11.4-1ubuntu1") + end + end +end diff --git a/spec/unit/provider/package/easy_install_spec.rb b/spec/unit/provider/package/easy_install_spec.rb new file mode 100644 index 0000000000..02f8399af8 --- /dev/null +++ b/spec/unit/provider/package/easy_install_spec.rb @@ -0,0 +1,112 @@ +# +# Author:: Joe Williams (<joe@joetify.com>) +# Copyright:: Copyright (c) 2009 Joe Williams +# 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::Provider::Package::EasyInstall do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::EasyInstallPackage.new('boto') + @new_resource.version('1.8d') + + @current_resource = Chef::Resource::EasyInstallPackage.new('boto') + @current_resource.version('1.8d') + + @provider = Chef::Provider::Package::EasyInstall.new(@new_resource, @run_context) + Chef::Resource::Package.stub!(:new).and_return(@current_resource) + + @stdin = StringIO.new + @stdout = StringIO.new + @status = mock("Status", :exitstatus => 0) + @stderr = StringIO.new + @pid = 2342 + @provider.stub!(:popen4).and_return(@status) + end + + describe "easy_install_binary_path" do + it "should return a Chef::Provider::EasyInstall object" do + provider = Chef::Provider::Package::EasyInstall.new(@node, @new_resource) + provider.should be_a_kind_of(Chef::Provider::Package::EasyInstall) + end + + it "should set the current resources package name to the new resources package name" do + $stdout.stub!(:write) + @current_resource.should_receive(:package_name).with(@new_resource.package_name) + @provider.load_current_resource + end + + it "should return a relative path to easy_install if no easy_install_binary is given" do + @provider.easy_install_binary_path.should eql("easy_install") + end + + it "should return a specific path to easy_install if a easy_install_binary is given" do + @new_resource.should_receive(:easy_install_binary).and_return("/opt/local/bin/custom/easy_install") + @provider.easy_install_binary_path.should eql("/opt/local/bin/custom/easy_install") + end + + end + + describe "actions_on_package" do + it "should run easy_install with the package name and version" do + @provider.should_receive(:run_command).with({ + :command => "easy_install \"boto==1.8d\"" + }) + @provider.install_package("boto", "1.8d") + end + + it "should run easy_install with the package name and version and specified options" do + @provider.should_receive(:run_command).with({ + :command => "easy_install --always-unzip \"boto==1.8d\"" + }) + @new_resource.stub!(:options).and_return("--always-unzip") + @provider.install_package("boto", "1.8d") + end + + it "should run easy_install with the package name and version" do + @provider.should_receive(:run_command).with({ + :command => "easy_install \"boto==1.8d\"" + }) + @provider.upgrade_package("boto", "1.8d") + end + + it "should run easy_install -m with the package name and version" do + @provider.should_receive(:run_command).with({ + :command => "easy_install -m boto" + }) + @provider.remove_package("boto", "1.8d") + end + + it "should run easy_install -m with the package name and version and specified options" do + @provider.should_receive(:run_command).with({ + :command => "easy_install -x -m boto" + }) + @new_resource.stub!(:options).and_return("-x") + @provider.remove_package("boto", "1.8d") + end + + it "should run easy_install -m with the package name and version" do + @provider.should_receive(:run_command).with({ + :command => "easy_install -m boto" + }) + @provider.purge_package("boto", "1.8d") + end + + end +end diff --git a/spec/unit/provider/package/freebsd_spec.rb b/spec/unit/provider/package/freebsd_spec.rb new file mode 100644 index 0000000000..270c85d934 --- /dev/null +++ b/spec/unit/provider/package/freebsd_spec.rb @@ -0,0 +1,259 @@ +# +# Authors:: Bryan McLellan (btm@loftninjas.org) +# Matthew Landauer (matthew@openaustralia.org) +# Copyright:: Copyright (c) 2009 Bryan McLellan, Matthew Landauer +# 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 'ostruct' + +describe Chef::Provider::Package::Freebsd, "load_current_resource" do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Package.new("zsh") + @current_resource = Chef::Resource::Package.new("zsh") + + @provider = Chef::Provider::Package::Freebsd.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + ::File.stub!(:exist?).with('/usr/ports/Makefile').and_return(false) + end + + describe "when determining the current package state" do + before do + @provider.stub!(:ports_candidate_version).and_return("4.3.6") + end + + it "should create a current resource with the name of the new_resource" do + current_resource = Chef::Provider::Package::Freebsd.new(@new_resource, @run_context).current_resource + current_resource.name.should == "zsh" + end + + it "should return a version if the package is installed" do + @provider.should_receive(:current_installed_version).and_return("4.3.6_7") + @provider.load_current_resource + @current_resource.version.should == "4.3.6_7" + end + + it "should return nil if the package is not installed" do + @provider.should_receive(:current_installed_version).and_return(nil) + @provider.load_current_resource + @current_resource.version.should be_nil + end + + it "should return a candidate version if it exists" do + @provider.should_receive(:current_installed_version).and_return(nil) + @provider.load_current_resource + @provider.candidate_version.should eql("4.3.6") + end + end + + describe "when querying for package state and attributes" do + before do + #@new_resource = Chef::Resource::Package.new("zsh") + + #@provider = Chef::Provider::Package::Freebsd.new(@node, @new_resource) + + #@status = mock("Status", :exitstatus => 0) + #@stdin = mock("STDIN", :null_object => true) + #@stdout = mock("STDOUT", :null_object => true) + #@stderr = mock("STDERR", :null_object => true) + #@pid = mock("PID", :null_object => true) + end + + it "should return the version number when it is installed" do + pkg_info = OpenStruct.new(:stdout => "zsh-4.3.6_7") + @provider.should_receive(:shell_out!).with('pkg_info -E "zsh*"', :env => nil, :returns => [0,1]).and_return(pkg_info) + #@provider.should_receive(:popen4).with('pkg_info -E "zsh*"').and_yield(@pid, @stdin, ["zsh-4.3.6_7"], @stderr).and_return(@status) + @provider.stub!(:package_name).and_return("zsh") + @provider.current_installed_version.should == "4.3.6_7" + end + + it "does not set the current version number when the package is not installed" do + pkg_info = OpenStruct.new(:stdout => "") + @provider.should_receive(:shell_out!).with('pkg_info -E "zsh*"', :env => nil, :returns => [0,1]).and_return(pkg_info) + @provider.stub!(:package_name).and_return("zsh") + @provider.current_installed_version.should be_nil + end + + it "should return the port path for a valid port name" do + whereis = OpenStruct.new(:stdout => "zsh: /usr/ports/shells/zsh") + @provider.should_receive(:shell_out!).with("whereis -s zsh", :env => nil).and_return(whereis) + #@provider.should_receive(:popen4).with("whereis -s zsh").and_yield(@pid, @stdin, ["zsh: /usr/ports/shells/zsh"], @stderr).and_return(@status) + @provider.stub!(:port_name).and_return("zsh") + @provider.port_path.should == "/usr/ports/shells/zsh" + end + + # Not happy with the form of these tests as they are far too closely tied to the implementation and so very fragile. + it "should return the ports candidate version when given a valid port path" do + @provider.stub!(:port_path).and_return("/usr/ports/shells/zsh") + make_v = OpenStruct.new(:stdout => "4.3.6\n") + @provider.should_receive(:shell_out!).with("make -V PORTVERSION", {:cwd=>"/usr/ports/shells/zsh", :returns=>[0, 1], :env=>nil}).and_return(make_v) + @provider.ports_candidate_version.should == "4.3.6" + end + + it "should figure out the package name when we have ports" do + ::File.stub!(:exist?).with('/usr/ports/Makefile').and_return(true) + @provider.stub!(:port_path).and_return("/usr/ports/shells/zsh") + make_v = OpenStruct.new(:stdout => "zsh-4.3.6_7\n") + @provider.should_receive(:shell_out!).with("make -V PKGNAME", {:cwd=>"/usr/ports/shells/zsh", :env=>nil, :returns=>[0, 1]}).and_return(make_v) + #@provider.should_receive(:ports_makefile_variable_value).with("PKGNAME").and_return("zsh-4.3.6_7") + @provider.package_name.should == "zsh" + end + end + + describe Chef::Provider::Package::Freebsd, "install_package" do + before(:each) do + @cmd_result = OpenStruct.new(:status => true) + + @provider.current_resource = @current_resource + @provider.stub!(:package_name).and_return("zsh") + @provider.stub!(:latest_link_name).and_return("zsh") + @provider.stub!(:port_path).and_return("/usr/ports/shells/zsh") + end + + it "should run pkg_add -r with the package name" do + @provider.should_receive(:shell_out!).with("pkg_add -r zsh", :env => nil).and_return(@cmd_result) + @provider.install_package("zsh", "4.3.6_7") + end + + it "should run make install when installing from ports" do + @new_resource.stub!(:source).and_return("ports") + @provider.should_not_receive(:shell_out!).with("make -DBATCH -f /usr/ports/shells/zsh/Makefile install", :timeout => 1200, :env=>nil) + @provider.should_receive(:shell_out!).with("make -DBATCH install", :timeout => 1200, :env=>nil, :cwd => @provider.port_path).and_return(@cmd_result) + @provider.install_package("zsh", "4.3.6_7") + end + end + + describe Chef::Provider::Package::Freebsd, "port path" do + before do + #@node = Chef::Node.new + @new_resource = Chef::Resource::Package.new("zsh") + @new_resource.cookbook_name = "adventureclub" + @provider = Chef::Provider::Package::Freebsd.new(@new_resource, @run_context) + end + + it "should figure out the port path from the package_name using whereis" do + whereis = OpenStruct.new(:stdout => "zsh: /usr/ports/shells/zsh") + @provider.should_receive(:shell_out!).with("whereis -s zsh", :env=>nil).and_return(whereis) + @provider.port_path.should == "/usr/ports/shells/zsh" + end + + it "should use the package_name as the port path when it starts with /" do + new_resource = Chef::Resource::Package.new("/usr/ports/www/wordpress") + provider = Chef::Provider::Package::Freebsd.new(new_resource, @run_context) + provider.should_not_receive(:popen4) + provider.port_path.should == "/usr/ports/www/wordpress" + end + + it "should use the package_name as a relative path from /usr/ports when it contains / but doesn't start with it" do + # @new_resource = mock( "Chef::Resource::Package", + # :package_name => "www/wordpress", + # :cookbook_name => "xenoparadox") + new_resource = Chef::Resource::Package.new("www/wordpress") + provider = Chef::Provider::Package::Freebsd.new(new_resource, @run_context) + provider.should_not_receive(:popen4) + provider.port_path.should == "/usr/ports/www/wordpress" + end + end + + describe Chef::Provider::Package::Freebsd, "ruby-iconv (package with a dash in the name)" do + before(:each) do + @new_resource = Chef::Resource::Package.new("ruby-iconv") + @current_resource = Chef::Resource::Package.new("ruby-iconv") + @provider = Chef::Provider::Package::Freebsd.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + @provider.stub!(:port_path).and_return("/usr/ports/converters/ruby-iconv") + @provider.stub!(:package_name).and_return("ruby18-iconv") + @provider.stub!(:latest_link_name).and_return("ruby18-iconv") + + @install_result = OpenStruct.new(:status => true) + end + + it "should run pkg_add -r with the package name" do + @provider.should_receive(:shell_out!).with("pkg_add -r ruby18-iconv", :env => nil).and_return(@install_result) + @provider.install_package("ruby-iconv", "1.0") + end + + it "should run make install when installing from ports" do + @new_resource.stub!(:source).and_return("ports") + @provider.should_receive(:shell_out!).with("make -DBATCH install", :timeout => 1200, :env=>nil, :cwd => @provider.port_path).and_return(@install_result) + @provider.install_package("ruby-iconv", "1.0") + end + end + + describe Chef::Provider::Package::Freebsd, "remove_package" do + before(:each) do + @pkg_delete = OpenStruct.new(:status => true) + @new_resource.version "4.3.6_7" + @current_resource.version "4.3.6_7" + @provider.current_resource = @current_resource + @provider.stub!(:package_name).and_return("zsh") + end + + it "should run pkg_delete with the package name and version" do + @provider.should_receive(:shell_out!).with("pkg_delete zsh-4.3.6_7", :env => nil).and_return(@pkg_delete) + @provider.remove_package("zsh", "4.3.6_7") + end + end + + # A couple of examples to show up the difficulty of determining the command to install the binary package given the port: + # PORT DIRECTORY INSTALLED PACKAGE NAME COMMAND TO INSTALL PACKAGE + # /usr/ports/lang/perl5.8 perl-5.8.8_1 pkg_add -r perl + # /usr/ports/databases/mysql50-server mysql-server-5.0.45_1 pkg_add -r mysql50-server + # + # So, in one case it appears the command to install the package can be derived from the name of the port directory and in the + # other case it appears the command can be derived from the package name. Very confusing! + # Well, luckily, after much poking around, I discovered that the two can be disambiguated through the use of the LATEST_LINK + # variable which is set by the ports Makefile + # + # PORT DIRECTORY LATEST_LINK INSTALLED PACKAGE NAME COMMAND TO INSTALL PACKAGE + # /usr/ports/lang/perl5.8 perl perl-5.8.8_1 pkg_add -r perl + # /usr/ports/databases/mysql50-server mysql50-server mysql-server-5.0.45_1 pkg_add -r mysql50-server + # + # The variable LATEST_LINK is named that way because the directory that "pkg_add -r" downloads from is called "Latest" and + # contains the "latest" versions of package as symbolic links to the files in the "All" directory. + + describe Chef::Provider::Package::Freebsd, "install_package latest link fixes" do + it "should install the perl binary package with the correct name" do + @new_resource = Chef::Resource::Package.new("perl5.8") + @current_resource = Chef::Resource::Package.new("perl5.8") + @provider = Chef::Provider::Package::Freebsd.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + @provider.stub!(:package_name).and_return("perl") + @provider.stub!(:latest_link_name).and_return("perl") + + cmd = OpenStruct.new(:status => true) + @provider.should_receive(:shell_out!).with("pkg_add -r perl", :env => nil).and_return(cmd) + @provider.install_package("perl5.8", "5.8.8_1") + end + + it "should install the mysql50-server binary package with the correct name" do + + @new_resource = Chef::Resource::Package.new("mysql50-server") + @current_resource = Chef::Resource::Package.new("mysql50-server") + @provider = Chef::Provider::Package::Freebsd.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + @provider.stub!(:package_name).and_return("mysql-server") + @provider.stub!(:latest_link_name).and_return("mysql50-server") + + cmd = OpenStruct.new(:status => true) + @provider.should_receive(:shell_out!).with("pkg_add -r mysql50-server", :env=>nil).and_return(cmd) + @provider.install_package("mysql50-server", "5.0.45_1") + end + end +end diff --git a/spec/unit/provider/package/ips_spec.rb b/spec/unit/provider/package/ips_spec.rb new file mode 100644 index 0000000000..641a527012 --- /dev/null +++ b/spec/unit/provider/package/ips_spec.rb @@ -0,0 +1,209 @@ +# +# Author:: Bryan McLellan <btm@opscode.com> +# Copyright:: Copyright (c) 2012 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 'ostruct' + +# based on the apt specs + +describe Chef::Provider::Package::Ips do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Package.new("crypto/gnupg", @run_context) + @current_resource = Chef::Resource::Package.new("crypto/gnupg", @run_context) + Chef::Resource::Package.stub!(:new).and_return(@current_resource) + @provider = Chef::Provider::Package::Ips.new(@new_resource, @run_context) + + @stdin = StringIO.new + @stderr = StringIO.new + @stdout =<<-PKG_STATUS + Name: crypto/gnupg + Summary: GNU Privacy Guard + Description: A complete and free implementation of the OpenPGP Standard as + defined by RFC4880. + Category: Applications/System Utilities + State: Not installed + Publisher: solaris + Version: 2.0.17 + Build Release: 5.11 + Branch: 0.175.0.0.0.2.537 +Packaging Date: October 19, 2011 09:14:50 AM + Size: 8.07 MB + FMRI: pkg://solaris/crypto/gnupg@2.0.17,5.11-0.175.0.0.0.2.537:20111019T091450Z +PKG_STATUS + @pid = 12345 + @shell_out = OpenStruct.new(:stdout => @stdout,:stdin => @stdin,:stderr => @stderr,:status => @status,:exitstatus => 0) + end + + context "when loading current resource" do + it "should create a current resource with the name of the new_resource" do + @provider.should_receive(:shell_out!).and_return(@shell_out) + Chef::Resource::Package.should_receive(:new).and_return(@current_resource) + @provider.load_current_resource + end + + it "should set the current resources package name to the new resources package name" do + @provider.should_receive(:shell_out!).and_return(@shell_out) + @current_resource.should_receive(:package_name).with(@new_resource.package_name) + @provider.load_current_resource + end + + it "should run pkg info with the package name" do + @provider.should_receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(@shell_out) + @provider.load_current_resource + end + + it "should set the installed version to nil on the current resource if package state is not installed" do + @provider.should_receive(:shell_out!).and_return(@shell_out) + @current_resource.should_receive(:version).with(nil).and_return(true) + @provider.load_current_resource + end + + it "should set the installed version if package has one" do + @stdout.replace(<<-INSTALLED) + Name: crypto/gnupg + Summary: GNU Privacy Guard + Description: A complete and free implementation of the OpenPGP Standard as + defined by RFC4880. + Category: Applications/System Utilities + State: Installed + Publisher: solaris + Version: 2.0.17 + Build Release: 5.11 + Branch: 0.175.0.0.0.2.537 +Packaging Date: October 19, 2011 09:14:50 AM + Size: 8.07 MB + FMRI: pkg://solaris/crypto/gnupg@2.0.17,5.11-0.175.0.0.0.2.537:20111019T091450Z +INSTALLED + @provider.should_receive(:shell_out!).and_return(@shell_out) + @provider.load_current_resource + @current_resource.version.should == "2.0.17" + @provider.candidate_version.should eql("2.0.17") + end + + it "should return the current resouce" do + @provider.should_receive(:shell_out!).and_return(@shell_out) + @provider.load_current_resource.should eql(@current_resource) + end + end + + context "when installing a package" do + it "should run pkg install with the package name and version" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pkg install -q crypto/gnupg@2.0.17" + }) + @provider.install_package("crypto/gnupg", "2.0.17") + end + + + it "should run pkg install with the package name and version and options if specified" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pkg --no-refresh install -q crypto/gnupg@2.0.17" + }) + @new_resource.stub!(:options).and_return("--no-refresh") + @provider.install_package("crypto/gnupg", "2.0.17") + end + + it "should not contain invalid characters for the version string" do + @stdout.replace(<<-PKG_STATUS) + Name: security/sudo + Summary: sudo - authority delegation tool + State: Not Installed + Publisher: omnios + Version: 1.8.4.1 (1.8.4p1) + Build Release: 5.11 + Branch: 0.151002 +Packaging Date: April 1, 2012 05:55:52 PM + Size: 2.57 MB + FMRI: pkg://omnios/security/sudo@1.8.4.1,5.11-0.151002:20120401T175552Z +PKG_STATUS + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pkg install -q security/sudo@1.8.4.1" + }) + @provider.install_package("security/sudo", "1.8.4.1") + end + + it "should not include the human-readable version in the candidate_version" do + @stdout.replace(<<-PKG_STATUS) + Name: security/sudo + Summary: sudo - authority delegation tool + State: Not Installed + Publisher: omnios + Version: 1.8.4.1 (1.8.4p1) + Build Release: 5.11 + Branch: 0.151002 +Packaging Date: April 1, 2012 05:55:52 PM + Size: 2.57 MB + FMRI: pkg://omnios/security/sudo@1.8.4.1,5.11-0.151002:20120401T175552Z +PKG_STATUS + @provider.should_receive(:shell_out!).and_return(@shell_out) + @provider.load_current_resource + @current_resource.version.should be_nil + @provider.candidate_version.should eql("1.8.4.1") + end + + context "using the ips_package resource" do + before do + @new_resource = Chef::Resource::IpsPackage.new("crypto/gnupg", @run_context) + @current_resource = Chef::Resource::IpsPackage.new("crypto/gnupg", @run_context) + @provider = Chef::Provider::Package::Ips.new(@new_resource, @run_context) + end + + context "when accept_license is true" do + before do + @new_resource.stub!(:accept_license).and_return(true) + end + + it "should run pkg install with the --accept flag" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pkg install -q --accept crypto/gnupg@2.0.17" + }) + @provider.install_package("crypto/gnupg", "2.0.17") + end + end + end + end + + context "when upgrading a package" do + it "should run pkg install with the package name and version" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pkg install -q crypto/gnupg@2.0.17" + }) + @provider.upgrade_package("crypto/gnupg", "2.0.17") + end + end + + context "when uninstalling a package" do + it "should run pkg uninstall with the package name and version" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pkg uninstall -q crypto/gnupg@2.0.17" + }) + @provider.remove_package("crypto/gnupg", "2.0.17") + end + + it "should run pkg uninstall with the package name and version and options if specified" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pkg --no-refresh uninstall -q crypto/gnupg@2.0.17" + }) + @new_resource.stub!(:options).and_return("--no-refresh") + @provider.remove_package("crypto/gnupg", "2.0.17") + end + end +end diff --git a/spec/unit/provider/package/macports_spec.rb b/spec/unit/provider/package/macports_spec.rb new file mode 100644 index 0000000000..2f0db3f7a8 --- /dev/null +++ b/spec/unit/provider/package/macports_spec.rb @@ -0,0 +1,203 @@ +# +# Author:: David Balatero (<dbalatero@gmail.com>) +# Copyright:: Copyright (c) 2009 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::Provider::Package::Macports do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Package.new("zsh") + @current_resource = Chef::Resource::Package.new("zsh") + + @provider = Chef::Provider::Package::Macports.new(@new_resource, @run_context) + Chef::Resource::Package.stub!(:new).and_return(@current_resource) + + @status = mock("Status", :exitstatus => 0) + @stdin = StringIO.new + @stdout = StringIO.new + @stderr = StringIO.new + @pid = 2342 + end + + describe "load_current_resource" do + it "should create a current resource with the name of the new_resource" do + @provider.should_receive(:current_installed_version).and_return(nil) + @provider.should_receive(:macports_candidate_version).and_return("4.2.7") + + @provider.load_current_resource + @provider.current_resource.name.should == "zsh" + end + + it "should create a current resource with the version if the package is installed" do + @provider.should_receive(:macports_candidate_version).and_return("4.2.7") + @provider.should_receive(:current_installed_version).and_return("4.2.7") + + @provider.load_current_resource + @provider.candidate_version.should == "4.2.7" + end + + it "should create a current resource with a nil version if the package is not installed" do + @provider.should_receive(:current_installed_version).and_return(nil) + @provider.should_receive(:macports_candidate_version).and_return("4.2.7") + @provider.load_current_resource + @provider.current_resource.version.should be_nil + end + + it "should set a candidate version if one exists" do + @provider.should_receive(:current_installed_version).and_return(nil) + @provider.should_receive(:macports_candidate_version).and_return("4.2.7") + @provider.load_current_resource + @provider.candidate_version.should == "4.2.7" + end + end + + describe "current_installed_version" do + it "should return the current version if the package is installed" do + @stdout.should_receive(:read).and_return(<<EOF +The following ports are currently installed: + openssl @0.9.8k_0 (active) +EOF + ) + + @provider.should_receive(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.current_installed_version.should == "0.9.8k_0" + end + + it "should return nil if a package is not currently installed" do + @stdout.should_receive(:read).and_return(" \n") + @provider.should_receive(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.current_installed_version.should be_nil + end + end + + describe "macports_candidate_version" do + it "should return the latest available version of a given package" do + @stdout.should_receive(:read).and_return("version: 4.2.7\n") + @provider.should_receive(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.macports_candidate_version.should == "4.2.7" + end + + it "should return nil if there is no version for a given package" do + @stdout.should_receive(:read).and_return("Error: port fadsfadsfads not found\n") + @provider.should_receive(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.macports_candidate_version.should be_nil + end + end + + describe "install_package" do + it "should run the port install command with the correct version" do + @current_resource.should_receive(:version).and_return("4.1.6") + @provider.current_resource = @current_resource + @provider.should_receive(:run_command_with_systems_locale).with(:command => "port install zsh @4.2.7") + + @provider.install_package("zsh", "4.2.7") + end + + it "should not do anything if a package already exists with the same version" do + @current_resource.should_receive(:version).and_return("4.2.7") + @provider.current_resource = @current_resource + @provider.should_not_receive(:run_command_with_systems_locale) + + @provider.install_package("zsh", "4.2.7") + end + + it "should add options to the port command when specified" do + @current_resource.should_receive(:version).and_return("4.1.6") + @provider.current_resource = @current_resource + @new_resource.stub!(:options).and_return("-f") + @provider.should_receive(:run_command_with_systems_locale).with(:command => "port -f install zsh @4.2.7") + + @provider.install_package("zsh", "4.2.7") + end + end + + describe "purge_package" do + it "should run the port uninstall command with the correct version" do + @provider.should_receive(:run_command_with_systems_locale).with(:command => "port uninstall zsh @4.2.7") + @provider.purge_package("zsh", "4.2.7") + end + + it "should purge the currently active version if no explicit version is passed in" do + @provider.should_receive(:run_command_with_systems_locale).with(:command => "port uninstall zsh") + @provider.purge_package("zsh", nil) + end + + it "should add options to the port command when specified" do + @new_resource.stub!(:options).and_return("-f") + @provider.should_receive(:run_command_with_systems_locale).with(:command => "port -f uninstall zsh @4.2.7") + @provider.purge_package("zsh", "4.2.7") + end + end + + describe "remove_package" do + it "should run the port deactivate command with the correct version" do + @provider.should_receive(:run_command_with_systems_locale).with(:command => "port deactivate zsh @4.2.7") + @provider.remove_package("zsh", "4.2.7") + end + + it "should remove the currently active version if no explicit version is passed in" do + @provider.should_receive(:run_command_with_systems_locale).with(:command => "port deactivate zsh") + @provider.remove_package("zsh", nil) + end + + it "should add options to the port command when specified" do + @new_resource.stub!(:options).and_return("-f") + @provider.should_receive(:run_command_with_systems_locale).with(:command => "port -f deactivate zsh @4.2.7") + @provider.remove_package("zsh", "4.2.7") + end + end + + describe "upgrade_package" do + it "should run the port upgrade command with the correct version" do + @current_resource.should_receive(:version).at_least(:once).and_return("4.1.6") + @provider.current_resource = @current_resource + + @provider.should_receive(:run_command_with_systems_locale).with(:command => "port upgrade zsh @4.2.7") + + @provider.upgrade_package("zsh", "4.2.7") + end + + it "should not run the port upgrade command if the version is already installed" do + @current_resource.should_receive(:version).at_least(:once).and_return("4.2.7") + @provider.current_resource = @current_resource + @provider.should_not_receive(:run_command_with_systems_locale) + + @provider.upgrade_package("zsh", "4.2.7") + end + + it "should call install_package if the package isn't currently installed" do + @current_resource.should_receive(:version).at_least(:once).and_return(nil) + @provider.current_resource = @current_resource + @provider.should_receive(:install_package).and_return(true) + + @provider.upgrade_package("zsh", "4.2.7") + end + + it "should add options to the port command when specified" do + @new_resource.stub!(:options).and_return("-f") + @current_resource.should_receive(:version).at_least(:once).and_return("4.1.6") + @provider.current_resource = @current_resource + + @provider.should_receive(:run_command_with_systems_locale).with(:command => "port -f upgrade zsh @4.2.7") + + @provider.upgrade_package("zsh", "4.2.7") + end + end +end diff --git a/spec/unit/provider/package/pacman_spec.rb b/spec/unit/provider/package/pacman_spec.rb new file mode 100644 index 0000000000..7e4abcb6d5 --- /dev/null +++ b/spec/unit/provider/package/pacman_spec.rb @@ -0,0 +1,206 @@ +# +# Author:: Jan Zimmek (<jan.zimmek@web.de>) +# Copyright:: Copyright (c) 2010 Jan Zimmek +# 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::Provider::Package::Pacman do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Package.new("nano") + @current_resource = Chef::Resource::Package.new("nano") + + @status = mock("Status", :exitstatus => 0) + @provider = Chef::Provider::Package::Pacman.new(@new_resource, @run_context) + Chef::Resource::Package.stub!(:new).and_return(@current_resource) + @provider.stub!(:popen4).and_return(@status) + @stdin = StringIO.new + @stdout = StringIO.new(<<-ERR) +error: package "nano" not found +ERR + @stderr = StringIO.new + @pid = 2342 + end + + describe "when determining the current package state" do + it "should create a current resource with the name of the new_resource" do + Chef::Resource::Package.should_receive(:new).and_return(@current_resource) + @provider.load_current_resource + end + + it "should set the current resources package name to the new resources package name" do + @current_resource.should_receive(:package_name).with(@new_resource.package_name) + @provider.load_current_resource + end + + it "should run pacman query with the package name" do + @provider.should_receive(:popen4).with("pacman -Qi #{@new_resource.package_name}").and_return(@status) + @provider.load_current_resource + end + + it "should read stdout on pacman" do + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @stdout.should_receive(:each).and_return(true) + @provider.load_current_resource + end + + it "should set the installed version to nil on the current resource if pacman installed version not exists" do + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @current_resource.should_receive(:version).with(nil).and_return(true) + @provider.load_current_resource + end + + it "should set the installed version if pacman has one" do + @stdout = StringIO.new(<<-PACMAN) +Name : nano +Version : 2.2.2-1 +URL : http://www.nano-editor.org +Licenses : GPL +Groups : base +Provides : None +Depends On : glibc ncurses +Optional Deps : None +Required By : None +Conflicts With : None +Replaces : None +Installed Size : 1496.00 K +Packager : Andreas Radke <andyrtr@archlinux.org> +Architecture : i686 +Build Date : Mon 18 Jan 2010 06:16:16 PM CET +Install Date : Mon 01 Feb 2010 10:06:30 PM CET +Install Reason : Explicitly installed +Install Script : Yes +Description : Pico editor clone with enhancements +PACMAN + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.load_current_resource + @current_resource.version.should == "2.2.2-1" + end + + it "should set the candidate version if pacman has one" do + @stdout.stub!(:each).and_yield("core/nano 2.2.3-1 (base)"). + and_yield(" Pico editor clone with enhancements"). + and_yield("community/nanoblogger 3.4.1-1"). + and_yield(" NanoBlogger is a small weblog engine written in Bash for the command line") + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.load_current_resource + @provider.candidate_version.should eql("2.2.3-1") + end + + it "should use pacman.conf to determine valid repo names for package versions" do + @pacman_conf = <<-PACMAN_CONF +[options] +HoldPkg = pacman glibc +Architecture = auto + +[customrepo] +Server = https://my.custom.repo + +[core] +Include = /etc/pacman.d/mirrorlist + +[extra] +Include = /etc/pacman.d/mirrorlist + +[community] +Include = /etc/pacman.d/mirrorlist +PACMAN_CONF + + ::File.stub!(:exists?).with("/etc/pacman.conf").and_return(true) + ::File.stub!(:read).with("/etc/pacman.conf").and_return(@pacman_conf) + @stdout.stub!(:each).and_yield("customrepo/nano 1.2.3-4"). + and_yield(" My custom package") + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + + @provider.load_current_resource + @provider.candidate_version.should eql("1.2.3-4") + end + + it "should raise an exception if pacman fails" do + @status.should_receive(:exitstatus).and_return(2) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Package) + end + + it "should not raise an exception if pacman succeeds" do + @status.should_receive(:exitstatus).and_return(0) + lambda { @provider.load_current_resource }.should_not raise_error(Chef::Exceptions::Package) + end + + it "should raise an exception if pacman does not return a candidate version" do + @stdout.stub!(:each).and_yield("") + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + lambda { @provider.candidate_version }.should raise_error(Chef::Exceptions::Package) + end + + it "should return the current resouce" do + @provider.load_current_resource.should eql(@current_resource) + end + end + + describe Chef::Provider::Package::Pacman, "install_package" do + it "should run pacman install with the package name and version" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pacman --sync --noconfirm --noprogressbar nano" + }) + @provider.install_package("nano", "1.0") + end + + it "should run pacman install with the package name and version and options if specified" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pacman --sync --noconfirm --noprogressbar --debug nano" + }) + @new_resource.stub!(:options).and_return("--debug") + + @provider.install_package("nano", "1.0") + end + end + + describe Chef::Provider::Package::Pacman, "upgrade_package" do + it "should run install_package with the name and version" do + @provider.should_receive(:install_package).with("nano", "1.0") + @provider.upgrade_package("nano", "1.0") + end + end + + describe Chef::Provider::Package::Pacman, "remove_package" do + it "should run pacman remove with the package name" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pacman --remove --noconfirm --noprogressbar nano" + }) + @provider.remove_package("nano", "1.0") + end + + it "should run pacman remove with the package name and options if specified" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pacman --remove --noconfirm --noprogressbar --debug nano" + }) + @new_resource.stub!(:options).and_return("--debug") + + @provider.remove_package("nano", "1.0") + end + end + + describe Chef::Provider::Package::Pacman, "purge_package" do + it "should run remove_package with the name and version" do + @provider.should_receive(:remove_package).with("nano", "1.0") + @provider.purge_package("nano", "1.0") + end + + end +end diff --git a/spec/unit/provider/package/portage_spec.rb b/spec/unit/provider/package/portage_spec.rb new file mode 100644 index 0000000000..e963934f4f --- /dev/null +++ b/spec/unit/provider/package/portage_spec.rb @@ -0,0 +1,276 @@ +# +# Author:: Caleb Tennis (<caleb.tennis@gmail.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' + +describe Chef::Provider::Package::Portage, "load_current_resource" do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Package.new("dev-util/git") + @new_resource_without_category = Chef::Resource::Package.new("git") + @current_resource = Chef::Resource::Package.new("dev-util/git") + + @provider = Chef::Provider::Package::Portage.new(@new_resource, @run_context) + Chef::Resource::Package.stub!(:new).and_return(@current_resource) + end + + describe "when determining the current state of the package" do + + it "should create a current resource with the name of new_resource" do + ::Dir.stub!(:[]).with("/var/db/pkg/dev-util/git-*").and_return(["/var/db/pkg/dev-util/git-1.0.0"]) + Chef::Resource::Package.should_receive(:new).and_return(@current_resource) + @provider.load_current_resource + end + + it "should set the current resource package name to the new resource package name" do + ::Dir.stub!(:[]).with("/var/db/pkg/dev-util/git-*").and_return(["/var/db/pkg/dev-util/git-1.0.0"]) + @current_resource.should_receive(:package_name).with(@new_resource.package_name) + @provider.load_current_resource + end + + it "should return a current resource with the correct version if the package is found" do + ::Dir.stub!(:[]).with("/var/db/pkg/dev-util/git-*").and_return(["/var/db/pkg/dev-util/git-foobar-0.9", "/var/db/pkg/dev-util/git-1.0.0"]) + @provider.load_current_resource + @provider.current_resource.version.should == "1.0.0" + end + + it "should return a current resource with the correct version if the package is found with revision" do + ::Dir.stub!(:[]).with("/var/db/pkg/dev-util/git-*").and_return(["/var/db/pkg/dev-util/git-1.0.0-r1"]) + @provider.load_current_resource + @provider.current_resource.version.should == "1.0.0-r1" + end + + it "should return a current resource with a nil version if the package is not found" do + ::Dir.stub!(:[]).with("/var/db/pkg/dev-util/git-*").and_return(["/var/db/pkg/dev-util/notgit-1.0.0"]) + @provider.load_current_resource + @provider.current_resource.version.should be_nil + end + + it "should return a package name match from /var/db/pkg/* if a category isn't specified and a match is found" do + ::Dir.stub!(:[]).with("/var/db/pkg/*/git-*").and_return(["/var/db/pkg/dev-util/git-foobar-0.9", "/var/db/pkg/dev-util/git-1.0.0"]) + @provider = Chef::Provider::Package::Portage.new(@new_resource_without_category, @run_context) + @provider.load_current_resource + @provider.current_resource.version.should == "1.0.0" + end + + it "should return a current resource with a nil version if a category isn't specified and a name match from /var/db/pkg/* is not found" do + ::Dir.stub!(:[]).with("/var/db/pkg/*/git-*").and_return(["/var/db/pkg/dev-util/notgit-1.0.0"]) + @provider = Chef::Provider::Package::Portage.new(@new_resource_without_category, @run_context) + @provider.load_current_resource + @provider.current_resource.version.should be_nil + end + + it "should throw an exception if a category isn't specified and multiple packages are found" do + ::Dir.stub!(:[]).with("/var/db/pkg/*/git-*").and_return(["/var/db/pkg/dev-util/git-1.0.0", "/var/db/pkg/funny-words/git-1.0.0"]) + @provider = Chef::Provider::Package::Portage.new(@new_resource_without_category, @run_context) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Package) + end + + it "should return a current resource with a nil version if a category is specified and multiple packages are found" do + ::Dir.stub!(:[]).with("/var/db/pkg/dev-util/git-*").and_return(["/var/db/pkg/dev-util/git-1.0.0", "/var/db/pkg/funny-words/git-1.0.0"]) + @provider = Chef::Provider::Package::Portage.new(@new_resource, @run_context) + @provider.load_current_resource + @provider.current_resource.version.should be_nil + end + + it "should return a current resource with a nil version if a category is not specified and multiple packages from the same category are found" do + ::Dir.stub!(:[]).with("/var/db/pkg/*/git-*").and_return(["/var/db/pkg/dev-util/git-1.0.0", "/var/db/pkg/dev-util/git-1.0.1"]) + @provider = Chef::Provider::Package::Portage.new(@new_resource_without_category, @run_context) + @provider.load_current_resource + @provider.current_resource.version.should be_nil + end + end + + describe "once the state of the package is known" do + + describe Chef::Provider::Package::Portage, "candidate_version" do + it "should return the candidate_version variable if already set" do + @provider.candidate_version = "1.0.0" + @provider.should_not_receive(:popen4) + @provider.candidate_version + end + + it "should throw an exception if the exitstatus is not 0" do + @status = mock("Status", :exitstatus => 1) + @provider.stub!(:popen4).and_return(@status) + lambda { @provider.candidate_version }.should raise_error(Chef::Exceptions::Package) + end + + it "should find the candidate_version if a category is specifed and there are no duplicates" do + output = <<EOF +Searching... +[ Results for search key : git ] +[ Applications found : 14 ] + +* app-misc/digitemp [ Masked ] + Latest version available: 3.5.0 + Latest version installed: [ Not Installed ] + Size of files: 261 kB + Homepage: http://www.digitemp.com/ http://www.ibutton.com/ + Description: Temperature logging and reporting using Dallas Semiconductor's iButtons and 1-Wire protocol + License: GPL-2 + +* dev-util/git + Latest version available: 1.6.0.6 + Latest version installed: ignore + Size of files: 2,725 kB + Homepage: http://git.or.cz/ + Description: GIT - the stupid content tracker, the revision control system heavily used by the Linux kernel team + License: GPL-2 + +* dev-util/gitosis [ Masked ] + Latest version available: 0.2_p20080825 + Latest version installed: [ Not Installed ] + Size of files: 31 kB + Homepage: http://eagain.net/gitweb/?p=gitosis.git;a=summary + Description: gitosis -- software for hosting git repositories + License: GPL-2 +EOF + + @status = mock("Status", :exitstatus => 0) + @provider.should_receive(:popen4).and_yield(nil, nil, StringIO.new(output), nil).and_return(@status) + @provider.candidate_version.should == "1.6.0.6" + end + + it "should find the candidate_version if a category is not specifed and there are no duplicates" do + output = <<EOF +Searching... +[ Results for search key : git ] +[ Applications found : 14 ] + +* app-misc/digitemp [ Masked ] + Latest version available: 3.5.0 + Latest version installed: [ Not Installed ] + Size of files: 261 kB + Homepage: http://www.digitemp.com/ http://www.ibutton.com/ + Description: Temperature logging and reporting using Dallas Semiconductor's iButtons and 1-Wire protocol + License: GPL-2 + +* dev-util/git + Latest version available: 1.6.0.6 + Latest version installed: ignore + Size of files: 2,725 kB + Homepage: http://git.or.cz/ + Description: GIT - the stupid content tracker, the revision control system heavily used by the Linux kernel team + License: GPL-2 + +* dev-util/gitosis [ Masked ] + Latest version available: 0.2_p20080825 + Latest version installed: [ Not Installed ] + Size of files: 31 kB + Homepage: http://eagain.net/gitweb/?p=gitosis.git;a=summary + Description: gitosis -- software for hosting git repositories + License: GPL-2 +EOF + + @status = mock("Status", :exitstatus => 0) + @provider = Chef::Provider::Package::Portage.new(@new_resource_without_category, @run_context) + @provider.should_receive(:popen4).and_yield(nil, nil, StringIO.new(output), nil).and_return(@status) + @provider.candidate_version.should == "1.6.0.6" + end + + it "should throw an exception if a category is not specified and there are duplicates" do + output = <<EOF +Searching... +[ Results for search key : git ] +[ Applications found : 14 ] + +* app-misc/digitemp [ Masked ] + Latest version available: 3.5.0 + Latest version installed: [ Not Installed ] + Size of files: 261 kB + Homepage: http://www.digitemp.com/ http://www.ibutton.com/ + Description: Temperature logging and reporting using Dallas Semiconductor's iButtons and 1-Wire protocol + License: GPL-2 + +* app-misc/git + Latest version available: 4.3.20 + Latest version installed: [ Not Installed ] + Size of files: 416 kB + Homepage: http://www.gnu.org/software/git/ + Description: GNU Interactive Tools - increase speed and efficiency of most daily task + License: GPL-2 + +* dev-util/git + Latest version available: 1.6.0.6 + Latest version installed: ignore + Size of files: 2,725 kB + Homepage: http://git.or.cz/ + Description: GIT - the stupid content tracker, the revision control system heavily used by the Linux kernel team + License: GPL-2 + +* dev-util/gitosis [ Masked ] + Latest version available: 0.2_p20080825 + Latest version installed: [ Not Installed ] + Size of files: 31 kB + Homepage: http://eagain.net/gitweb/?p=gitosis.git;a=summary + Description: gitosis -- software for hosting git repositories + License: GPL-2 +EOF + + @status = mock("Status", :exitstatus => 0) + @provider = Chef::Provider::Package::Portage.new(@new_resource_without_category, @run_context) + @provider.should_receive(:popen4).and_yield(nil, nil, StringIO.new(output), nil).and_return(@status) + lambda { @provider.candidate_version }.should raise_error(Chef::Exceptions::Package) + end + + end + + describe Chef::Provider::Package::Portage, "install_package" do + it "should install a normally versioned package using portage" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "emerge -g --color n --nospinner --quiet =dev-util/git-1.0.0" + }) + @provider.install_package("dev-util/git", "1.0.0") + end + + it "should install a tilde versioned package using portage" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "emerge -g --color n --nospinner --quiet ~dev-util/git-1.0.0" + }) + @provider.install_package("dev-util/git", "~1.0.0") + end + + it "should add options to the emerge command when specified" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "emerge -g --color n --nospinner --quiet --oneshot =dev-util/git-1.0.0" + }) + @new_resource.stub!(:options).and_return("--oneshot") + + @provider.install_package("dev-util/git", "1.0.0") + end + end + + describe Chef::Provider::Package::Portage, "remove_package" do + it "should un-emerge the package with no version specified" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "emerge --unmerge --color n --nospinner --quiet dev-util/git" + }) + @provider.remove_package("dev-util/git", nil) + end + + it "should un-emerge the package with a version specified" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "emerge --unmerge --color n --nospinner --quiet =dev-util/git-1.0.0" + }) + @provider.remove_package("dev-util/git", "1.0.0") + end + end + end +end diff --git a/spec/unit/provider/package/rpm_spec.rb b/spec/unit/provider/package/rpm_spec.rb new file mode 100644 index 0000000000..9dd94a7441 --- /dev/null +++ b/spec/unit/provider/package/rpm_spec.rb @@ -0,0 +1,152 @@ +# +# Author:: Joshua Timberman (<joshua@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2008, 2010 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::Provider::Package::Rpm do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Package.new("emacs") + @new_resource.source "/tmp/emacs-21.4-20.el5.i386.rpm" + + @provider = Chef::Provider::Package::Rpm.new(@new_resource, @run_context) + + @status = mock("Status", :exitstatus => 0) + ::File.stub!(:exists?).and_return(true) + end + + describe "when determining the current state of the package" do + + it "should create a current resource with the name of new_resource" do + @provider.stub!(:popen4).and_return(@status) + @provider.load_current_resource + @provider.current_resource.name.should == "emacs" + end + + it "should set the current reource package name to the new resource package name" do + @provider.stub!(:popen4).and_return(@status) + @provider.load_current_resource + @provider.current_resource.package_name.should == 'emacs' + end + + it "should raise an exception if a source is supplied but not found" do + ::File.stub!(:exists?).and_return(false) + lambda { @provider.run_action(:any) }.should raise_error(Chef::Exceptions::Package) + end + + it "should get the source package version from rpm if provided" do + @stdout = StringIO.new("emacs 21.4-20.el5") + @provider.should_receive(:popen4).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' /tmp/emacs-21.4-20.el5.i386.rpm").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.should_receive(:popen4).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' emacs").and_return(@status) + @provider.load_current_resource + @provider.current_resource.package_name.should == "emacs" + @provider.new_resource.version.should == "21.4-20.el5" + end + + it "should return the current version installed if found by rpm" do + @stdout = StringIO.new("emacs 21.4-20.el5") + @provider.should_receive(:popen4).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' /tmp/emacs-21.4-20.el5.i386.rpm").and_return(@status) + @provider.should_receive(:popen4).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' emacs").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.load_current_resource + @provider.current_resource.version.should == "21.4-20.el5" + end + + it "should raise an exception if the source is not set but we are installing" do + new_resource = Chef::Resource::Package.new("emacs") + provider = Chef::Provider::Package::Rpm.new(new_resource, @run_context) + lambda { provider.run_action(:any) }.should raise_error(Chef::Exceptions::Package) + end + + it "should raise an exception if rpm fails to run" do + status = mock("Status", :exitstatus => -1) + @provider.stub!(:popen4).and_return(status) + lambda { @provider.run_action(:any) }.should raise_error(Chef::Exceptions::Package) + end + end + + describe "after the current resource is loaded" do + before do + @current_resource = Chef::Resource::Package.new("emacs") + @provider.current_resource = @current_resource + end + + describe "when installing or upgrading" do + it "should run rpm -i with the package source to install" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "rpm -i /tmp/emacs-21.4-20.el5.i386.rpm" + }) + @provider.install_package("emacs", "21.4-20.el5") + end + + it "should run rpm -U with the package source to upgrade" do + @current_resource.version("21.4-19.el5") + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "rpm -U /tmp/emacs-21.4-20.el5.i386.rpm" + }) + @provider.upgrade_package("emacs", "21.4-20.el5") + end + + it "should install from a path when the package is a path and the source is nil" do + @new_resource = Chef::Resource::Package.new("/tmp/emacs-21.4-20.el5.i386.rpm") + @provider = Chef::Provider::Package::Rpm.new(@new_resource, @run_context) + @new_resource.source.should == "/tmp/emacs-21.4-20.el5.i386.rpm" + @current_resource = Chef::Resource::Package.new("emacs") + @provider.current_resource = @current_resource + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "rpm -i /tmp/emacs-21.4-20.el5.i386.rpm" + }) + @provider.install_package("/tmp/emacs-21.4-20.el5.i386.rpm", "21.4-20.el5") + end + + it "should uprgrade from a path when the package is a path and the source is nil" do + @new_resource = Chef::Resource::Package.new("/tmp/emacs-21.4-20.el5.i386.rpm") + @provider = Chef::Provider::Package::Rpm.new(@new_resource, @run_context) + @new_resource.source.should == "/tmp/emacs-21.4-20.el5.i386.rpm" + @current_resource = Chef::Resource::Package.new("emacs") + @current_resource.version("21.4-19.el5") + @provider.current_resource = @current_resource + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "rpm -U /tmp/emacs-21.4-20.el5.i386.rpm" + }) + @provider.upgrade_package("/tmp/emacs-21.4-20.el5.i386.rpm", "21.4-20.el5") + end + + it "installs with custom options specified in the resource" do + @provider.candidate_version = '11' + @new_resource.options("--dbpath /var/lib/rpm") + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "rpm --dbpath /var/lib/rpm -i /tmp/emacs-21.4-20.el5.i386.rpm" + }) + @provider.install_package(@new_resource.name, @provider.candidate_version) + end + end + + describe "when removing the package" do + it "should run rpm -e to remove the package" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "rpm -e emacs-21.4-20.el5" + }) + @provider.remove_package("emacs", "21.4-20.el5") + end + end + end +end + diff --git a/spec/unit/provider/package/rubygems_spec.rb b/spec/unit/provider/package/rubygems_spec.rb new file mode 100644 index 0000000000..be4c3a99c4 --- /dev/null +++ b/spec/unit/provider/package/rubygems_spec.rb @@ -0,0 +1,614 @@ +# +# Author:: David Balatero (dbalatero@gmail.com) +# +# Copyright:: Copyright (c) 2009 David Balatero +# 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 'pp' + +module GemspecBackcompatCreator + def gemspec(name, version) + if Gem::Specification.new.method(:initialize).arity == 0 + Gem::Specification.new { |s| s.name = name; s.version = version } + else + Gem::Specification.new(name, version) + end + end +end + +require 'spec_helper' +require 'ostruct' + +describe Chef::Provider::Package::Rubygems::CurrentGemEnvironment do + include GemspecBackcompatCreator + + before do + @gem_env = Chef::Provider::Package::Rubygems::CurrentGemEnvironment.new + end + + it "determines the gem paths from the in memory rubygems" do + @gem_env.gem_paths.should == Gem.path + end + + it "determines the installed versions of gems from Gem.source_index" do + gems = [gemspec('rspec-core', Gem::Version.new('1.2.9')), gemspec('rspec-core', Gem::Version.new('1.3.0'))] + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.8.0') + Gem::Specification.should_receive(:find_all_by_name).with('rspec-core', Gem::Dependency.new('rspec-core').requirement).and_return(gems) + else + Gem.source_index.should_receive(:search).with(Gem::Dependency.new('rspec-core', nil)).and_return(gems) + end + @gem_env.installed_versions(Gem::Dependency.new('rspec-core', nil)).should == gems + end + + it "determines the installed versions of gems from the source index (part2: the unmockening)" do + expected = ['rspec-core', Gem::Version.new(RSpec::Core::Version::STRING)] + actual = @gem_env.installed_versions(Gem::Dependency.new('rspec-core', nil)).map { |spec| [spec.name, spec.version] } + actual.should include(expected) + end + + it "yields to a block with an alternate source list set" do + sources_in_block = nil + normal_sources = Gem.sources + begin + @gem_env.with_gem_sources("http://gems.example.org") do + sources_in_block = Gem.sources + raise RuntimeError, "sources should be reset even in case of an error" + end + rescue RuntimeError + end + sources_in_block.should == %w{http://gems.example.org} + Gem.sources.should == normal_sources + end + + it "it doesnt alter the gem sources if none are set" do + sources_in_block = nil + normal_sources = Gem.sources + begin + @gem_env.with_gem_sources(nil) do + sources_in_block = Gem.sources + raise RuntimeError, "sources should be reset even in case of an error" + end + rescue RuntimeError + end + sources_in_block.should == normal_sources + Gem.sources.should == normal_sources + end + + it "finds a matching gem candidate version" do + dep = Gem::Dependency.new('rspec', '>= 0') + dep_installer = Gem::DependencyInstaller.new + @gem_env.stub!(:dependency_installer).and_return(dep_installer) + latest = [[gemspec("rspec", Gem::Version.new("1.3.0")), "http://rubygems.org/"]] + dep_installer.should_receive(:find_gems_with_sources).with(dep).and_return(latest) + @gem_env.candidate_version_from_remote(Gem::Dependency.new('rspec', '>= 0')).should == Gem::Version.new('1.3.0') + end + + it "gives the candidate version as nil if none is found" do + dep = Gem::Dependency.new('rspec', '>= 0') + latest = [] + dep_installer = Gem::DependencyInstaller.new + @gem_env.stub!(:dependency_installer).and_return(dep_installer) + dep_installer.should_receive(:find_gems_with_sources).with(dep).and_return(latest) + @gem_env.candidate_version_from_remote(Gem::Dependency.new('rspec', '>= 0')).should be_nil + end + + it "finds a matching candidate version from a .gem file when the path to the gem is supplied" do + location = CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem' + @gem_env.candidate_version_from_file(Gem::Dependency.new('chef-integration-test', '>= 0'), location).should == Gem::Version.new('0.1.0') + @gem_env.candidate_version_from_file(Gem::Dependency.new('chef-integration-test', '>= 0.2.0'), location).should be_nil + end + + it "finds a matching gem from a specific gemserver when explicit sources are given" do + dep = Gem::Dependency.new('rspec', '>= 0') + latest = [[gemspec("rspec", Gem::Version.new("1.3.0")), "http://rubygems.org/"]] + + @gem_env.should_receive(:with_gem_sources).with('http://gems.example.com').and_yield + dep_installer = Gem::DependencyInstaller.new + @gem_env.stub!(:dependency_installer).and_return(dep_installer) + dep_installer.should_receive(:find_gems_with_sources).with(dep).and_return(latest) + @gem_env.candidate_version_from_remote(Gem::Dependency.new('rspec', '>=0'), 'http://gems.example.com').should == Gem::Version.new('1.3.0') + end + + it "installs a gem with a hash of options for the dependency installer" do + dep_installer = Gem::DependencyInstaller.new + @gem_env.should_receive(:dependency_installer).with(:install_dir => '/foo/bar').and_return(dep_installer) + @gem_env.should_receive(:with_gem_sources).with('http://gems.example.com').and_yield + dep_installer.should_receive(:install).with(Gem::Dependency.new('rspec', '>= 0')) + @gem_env.install(Gem::Dependency.new('rspec', '>= 0'), :install_dir => '/foo/bar', :sources => ['http://gems.example.com']) + end + + it "builds an uninstaller for a gem with options set to avoid requiring user input" do + # default options for uninstaller should be: + # :ignore => true, :executables => true + Gem::Uninstaller.should_receive(:new).with('rspec', :ignore => true, :executables => true) + @gem_env.uninstaller('rspec') + end + + it "uninstalls all versions of a gem" do + uninstaller = mock('gem uninstaller') + uninstaller.should_receive(:uninstall) + @gem_env.should_receive(:uninstaller).with('rspec', :all => true).and_return(uninstaller) + @gem_env.uninstall('rspec') + end + + it "uninstalls a specific version of a gem" do + uninstaller = mock('gem uninstaller') + uninstaller.should_receive(:uninstall) + @gem_env.should_receive(:uninstaller).with('rspec', :version => '1.2.3').and_return(uninstaller) + @gem_env.uninstall('rspec', '1.2.3') + end + +end + +describe Chef::Provider::Package::Rubygems::AlternateGemEnvironment do + include GemspecBackcompatCreator + + before do + Chef::Provider::Package::Rubygems::AlternateGemEnvironment.gempath_cache.clear + Chef::Provider::Package::Rubygems::AlternateGemEnvironment.platform_cache.clear + @gem_env = Chef::Provider::Package::Rubygems::AlternateGemEnvironment.new('/usr/weird/bin/gem') + end + + it "determines the gem paths from shelling out to gem env" do + gem_env_output = ['/path/to/gems', '/another/path/to/gems'].join(File::PATH_SEPARATOR) + shell_out_result = OpenStruct.new(:stdout => gem_env_output) + @gem_env.should_receive(:shell_out!).with('/usr/weird/bin/gem env gempath').and_return(shell_out_result) + @gem_env.gem_paths.should == ['/path/to/gems', '/another/path/to/gems'] + end + + it "caches the gempaths by gem_binary" do + gem_env_output = ['/path/to/gems', '/another/path/to/gems'].join(File::PATH_SEPARATOR) + shell_out_result = OpenStruct.new(:stdout => gem_env_output) + @gem_env.should_receive(:shell_out!).with('/usr/weird/bin/gem env gempath').and_return(shell_out_result) + expected = ['/path/to/gems', '/another/path/to/gems'] + @gem_env.gem_paths.should == ['/path/to/gems', '/another/path/to/gems'] + Chef::Provider::Package::Rubygems::AlternateGemEnvironment.gempath_cache['/usr/weird/bin/gem'].should == expected + end + + it "uses the cached result for gem paths when available" do + gem_env_output = ['/path/to/gems', '/another/path/to/gems'].join(File::PATH_SEPARATOR) + shell_out_result = OpenStruct.new(:stdout => gem_env_output) + @gem_env.should_not_receive(:shell_out!) + expected = ['/path/to/gems', '/another/path/to/gems'] + Chef::Provider::Package::Rubygems::AlternateGemEnvironment.gempath_cache['/usr/weird/bin/gem']= expected + @gem_env.gem_paths.should == ['/path/to/gems', '/another/path/to/gems'] + end + + it "builds the gems source index from the gem paths" do + @gem_env.stub!(:gem_paths).and_return(['/path/to/gems', '/another/path/to/gems']) + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.8.0') + @gem_env.gem_specification + Gem::Specification.dirs.should == [ '/path/to/gems/specifications', '/another/path/to/gems/specifications' ] + else + Gem::SourceIndex.should_receive(:from_gems_in).with('/path/to/gems/specifications', '/another/path/to/gems/specifications') + @gem_env.gem_source_index + end + end + + it "determines the installed versions of gems from the source index" do + gems = [gemspec('rspec', Gem::Version.new('1.2.9')), gemspec('rspec', Gem::Version.new('1.3.0'))] + rspec_dep = Gem::Dependency.new('rspec', nil) + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.8.0') + @gem_env.stub!(:gem_specification).and_return(Gem::Specification) + @gem_env.gem_specification.should_receive(:find_all_by_name).with(rspec_dep.name, rspec_dep.requirement).and_return(gems) + else + @gem_env.stub!(:gem_source_index).and_return(Gem.source_index) + @gem_env.gem_source_index.should_receive(:search).with(rspec_dep).and_return(gems) + end + @gem_env.installed_versions(Gem::Dependency.new('rspec', nil)).should == gems + end + + it "determines the installed versions of gems from the source index (part2: the unmockening)" do + $stdout.stub!(:write) + path_to_gem = if windows? + `where gem`.split[-1] + else + `which gem`.strip + end + pending("cant find your gem executable") if path_to_gem.empty? + gem_env = Chef::Provider::Package::Rubygems::AlternateGemEnvironment.new(path_to_gem) + expected = ['rspec-core', Gem::Version.new(RSpec::Core::Version::STRING)] + actual = gem_env.installed_versions(Gem::Dependency.new('rspec-core', nil)).map { |s| [s.name, s.version] } + actual.should include(expected) + end + + it "detects when the target gem environment is the jruby platform" do + gem_env_out=<<-JRUBY_GEM_ENV +RubyGems Environment: + - RUBYGEMS VERSION: 1.3.6 + - RUBY VERSION: 1.8.7 (2010-05-12 patchlevel 249) [java] + - INSTALLATION DIRECTORY: /Users/you/.rvm/gems/jruby-1.5.0 + - RUBY EXECUTABLE: /Users/you/.rvm/rubies/jruby-1.5.0/bin/jruby + - EXECUTABLE DIRECTORY: /Users/you/.rvm/gems/jruby-1.5.0/bin + - RUBYGEMS PLATFORMS: + - ruby + - universal-java-1.6 + - GEM PATHS: + - /Users/you/.rvm/gems/jruby-1.5.0 + - /Users/you/.rvm/gems/jruby-1.5.0@global + - GEM CONFIGURATION: + - :update_sources => true + - :verbose => true + - :benchmark => false + - :backtrace => false + - :bulk_threshold => 1000 + - "install" => "--env-shebang" + - "update" => "--env-shebang" + - "gem" => "--no-rdoc --no-ri" + - :sources => ["http://rubygems.org/", "http://gems.github.com/"] + - REMOTE SOURCES: + - http://rubygems.org/ + - http://gems.github.com/ +JRUBY_GEM_ENV + @gem_env.should_receive(:shell_out!).with('/usr/weird/bin/gem env').and_return(mock('jruby_gem_env', :stdout => gem_env_out)) + expected = ['ruby', Gem::Platform.new('universal-java-1.6')] + @gem_env.gem_platforms.should == expected + # it should also cache the result + Chef::Provider::Package::Rubygems::AlternateGemEnvironment.platform_cache['/usr/weird/bin/gem'].should == expected + end + + it "uses the cached result for gem platforms if available" do + @gem_env.should_not_receive(:shell_out!) + expected = ['ruby', Gem::Platform.new('universal-java-1.6')] + Chef::Provider::Package::Rubygems::AlternateGemEnvironment.platform_cache['/usr/weird/bin/gem']= expected + @gem_env.gem_platforms.should == expected + end + + it "uses the current gem platforms when the target env is not jruby" do + gem_env_out=<<-RBX_GEM_ENV +RubyGems Environment: + - RUBYGEMS VERSION: 1.3.6 + - RUBY VERSION: 1.8.7 (2010-05-14 patchlevel 174) [x86_64-apple-darwin10.3.0] + - INSTALLATION DIRECTORY: /Users/ddeleo/.rvm/gems/rbx-1.0.0-20100514 + - RUBYGEMS PREFIX: /Users/ddeleo/.rvm/rubies/rbx-1.0.0-20100514 + - RUBY EXECUTABLE: /Users/ddeleo/.rvm/rubies/rbx-1.0.0-20100514/bin/rbx + - EXECUTABLE DIRECTORY: /Users/ddeleo/.rvm/gems/rbx-1.0.0-20100514/bin + - RUBYGEMS PLATFORMS: + - ruby + - x86_64-darwin-10 + - x86_64-rubinius-1.0 + - GEM PATHS: + - /Users/ddeleo/.rvm/gems/rbx-1.0.0-20100514 + - /Users/ddeleo/.rvm/gems/rbx-1.0.0-20100514@global + - GEM CONFIGURATION: + - :update_sources => true + - :verbose => true + - :benchmark => false + - :backtrace => false + - :bulk_threshold => 1000 + - :sources => ["http://rubygems.org/", "http://gems.github.com/"] + - "gem" => "--no-rdoc --no-ri" + - REMOTE SOURCES: + - http://rubygems.org/ + - http://gems.github.com/ +RBX_GEM_ENV + @gem_env.should_receive(:shell_out!).with('/usr/weird/bin/gem env').and_return(mock('rbx_gem_env', :stdout => gem_env_out)) + @gem_env.gem_platforms.should == Gem.platforms + Chef::Provider::Package::Rubygems::AlternateGemEnvironment.platform_cache['/usr/weird/bin/gem'].should == Gem.platforms + end + + it "yields to a block while masquerading as a different gems platform" do + original_platforms = Gem.platforms + platforms_in_block = nil + begin + @gem_env.with_gem_platforms(['ruby', Gem::Platform.new('sparc64-java-1.7')]) do + platforms_in_block = Gem.platforms + raise "gem platforms should get set to the correct value even when an error occurs" + end + rescue RuntimeError + end + platforms_in_block.should == ['ruby', Gem::Platform.new('sparc64-java-1.7')] + Gem.platforms.should == original_platforms + end + +end + +describe Chef::Provider::Package::Rubygems do + before(:each) do + @node = Chef::Node.new + @new_resource = Chef::Resource::GemPackage.new("rspec-core") + @spec_version = @new_resource.version RSpec::Core::Version::STRING + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + # We choose detect omnibus via RbConfig::CONFIG['bindir'] in Chef::Provider::Package::Rubygems.new + RbConfig::CONFIG.stub!(:[]).with('bindir').and_return("/usr/bin/ruby") + @provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) + end + + it "triggers a gem configuration load so a later one will not stomp its config values" do + # ugly, is there a better way? + Gem.instance_variable_get(:@configuration).should_not be_nil + end + + it "uses the CurrentGemEnvironment implementation when no gem_binary_path is provided" do + @provider.gem_env.should be_a_kind_of(Chef::Provider::Package::Rubygems::CurrentGemEnvironment) + end + + it "uses the AlternateGemEnvironment implementation when a gem_binary_path is provided" do + @new_resource.gem_binary('/usr/weird/bin/gem') + provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) + provider.gem_env.gem_binary_location.should == '/usr/weird/bin/gem' + end + + it "searches for a gem binary when running on Omnibus on Unix" do + platform_mock :unix do + RbConfig::CONFIG.stub!(:[]).with('bindir').and_return("/opt/chef/embedded/bin") + ENV.stub!(:[]).with('PATH').and_return("/usr/bin:/usr/sbin:/opt/chef/embedded/bin") + File.stub!(:exists?).with('/usr/bin/gem').and_return(false) + File.stub!(:exists?).with('/usr/sbin/gem').and_return(true) + File.stub!(:exists?).with('/opt/chef/embedded/bin/gem').and_return(true) # should not get here + provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) + provider.gem_env.gem_binary_location.should == '/usr/sbin/gem' + end + end + + it "searches for a gem binary when running on Omnibus on Windows" do + platform_mock :windows do + RbConfig::CONFIG.stub!(:[]).with('bindir').and_return("d:/opscode/chef/embedded/bin") + ENV.stub!(:[]).with('PATH').and_return('C:\windows\system32;C:\windows;C:\Ruby186\bin;d:\opscode\chef\embedded\bin') + File.stub!(:exists?).with('C:\\windows\\system32\\gem').and_return(false) + File.stub!(:exists?).with('C:\\windows\\gem').and_return(false) + File.stub!(:exists?).with('C:\\Ruby186\\bin\\gem').and_return(true) + File.stub!(:exists?).with('d:\\opscode\\chef\\bin\\gem').and_return(false) # should not get here + File.stub!(:exists?).with('d:\\opscode\\chef\\embedded\\bin\\gem').and_return(false) # should not get here + provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) + provider.gem_env.gem_binary_location.should == 'C:\Ruby186\bin\gem' + end + end + + it "smites you when you try to use a hash of install options with an explicit gem binary" do + @new_resource.gem_binary('/foo/bar') + @new_resource.options(:fail => :burger) + lambda {Chef::Provider::Package::Rubygems.new(@new_resource, @run_context)}.should raise_error(ArgumentError) + end + + it "converts the new resource into a gem dependency" do + @provider.gem_dependency.should == Gem::Dependency.new('rspec-core', @spec_version) + @new_resource.version('~> 1.2.0') + @provider.gem_dependency.should == Gem::Dependency.new('rspec-core', '~> 1.2.0') + end + + describe "when determining the currently installed version" do + + it "sets the current version to the version specified by the new resource if that version is installed" do + @provider.load_current_resource + @provider.current_resource.version.should == @spec_version + end + + it "sets the current version to the highest installed version if the requested version is not installed" do + @new_resource.version('9000.0.2') + @provider.load_current_resource + @provider.current_resource.version.should == @spec_version + end + + it "leaves the current version at nil if the package is not installed" do + @new_resource.package_name("no-such-gem-should-exist-with-this-name") + @provider.load_current_resource + @provider.current_resource.version.should be_nil + end + + end + + describe "when determining the candidate version to install" do + + it "does not query for available versions when the current version is the target version" do + @provider.current_resource = @new_resource.dup + @provider.candidate_version.should be_nil + end + + it "determines the candidate version by querying the remote gem servers" do + @new_resource.source('http://mygems.example.com') + version = Gem::Version.new(@spec_version) + @provider.gem_env.should_receive(:candidate_version_from_remote). + with(Gem::Dependency.new('rspec-core', @spec_version), "http://mygems.example.com"). + and_return(version) + @provider.candidate_version.should == @spec_version + end + + it "parses the gem's specification if the requested source is a file" do + @new_resource.package_name('chef-integration-test') + @new_resource.version('>= 0') + @new_resource.source(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') + @provider.candidate_version.should == '0.1.0' + end + + end + + describe "when installing a gem" do + before do + @current_resource = Chef::Resource::GemPackage.new('rspec-core') + @provider.current_resource = @current_resource + @gem_dep = Gem::Dependency.new('rspec-core', @spec_version) + @provider.stub!(:load_current_resource) + end + + describe "in the current gem environment" do + it "installs the gem via the gems api when no explicit options are used" do + @provider.gem_env.should_receive(:install).with(@gem_dep, :sources => nil) + @provider.action_install.should be_true + @provider.converge + end + + it "installs the gem via the gems api when a remote source is provided" do + @new_resource.source('http://gems.example.org') + sources = ['http://gems.example.org'] + @provider.gem_env.should_receive(:install).with(@gem_dep, :sources => sources) + @provider.action_install.should be_true + @provider.converge + end + + it "installs the gem from file via the gems api when no explicit options are used" do + @new_resource.source(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') + @provider.gem_env.should_receive(:install).with(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') + @provider.action_install.should be_true + @provider.converge + end + + it "installs the gem from file via the gems api when the package is a path and the source is nil" do + @new_resource = Chef::Resource::GemPackage.new(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') + @provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + @new_resource.source.should == CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem' + @provider.gem_env.should_receive(:install).with(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') + @provider.action_install.should be_true + @provider.converge + end + + # this catches 'gem_package "foo"' when "./foo" is a file in the cwd, and instead of installing './foo' it fetches the remote gem + it "installs the gem via the gems api, when the package has no file separator characters in it, but a matching file exists in cwd" do + ::File.stub!(:exists?).and_return(true) + @new_resource.package_name('rspec-core') + @provider.gem_env.should_receive(:install).with(@gem_dep, :sources => nil) + @provider.action_install.should be_true + @provider.converge + end + + it "installs the gem by shelling out when options are provided as a String" do + @new_resource.options('-i /alt/install/location') + expected ="gem install rspec-core -q --no-rdoc --no-ri -v \"#{@spec_version}\" -i /alt/install/location" + @provider.should_receive(:shell_out!).with(expected, :env => nil) + @provider.action_install.should be_true + @provider.converge + end + + it "installs the gem via the gems api when options are given as a Hash" do + @new_resource.options(:install_dir => '/alt/install/location') + @provider.gem_env.should_receive(:install).with(@gem_dep, :sources => nil, :install_dir => '/alt/install/location') + @provider.action_install.should be_true + @provider.converge + end + + describe "at a specific version" do + before do + @gem_dep = Gem::Dependency.new('rspec-core', @spec_version) + end + + it "installs the gem via the gems api" do + @provider.gem_env.should_receive(:install).with(@gem_dep, :sources => nil) + @provider.action_install.should be_true + @provider.converge + end + end + describe "at version specified with comparison operator" do + it "skips install if current version satisifies requested version" do + @current_resource.stub(:version).and_return("2.3.3") + @new_resource.stub(:version).and_return(">=2.3.0") + + @provider.gem_env.should_not_receive(:install) + @provider.action_install + @provider.converge + end + + it "allows user to specify gem version with fuzzy operator" do + @current_resource.stub(:version).and_return("2.3.3") + @new_resource.stub(:version).and_return("~>2.3.0") + + @provider.gem_env.should_not_receive(:install) + @provider.action_install + @provider.converge + end + end + end + + describe "in an alternate gem environment" do + it "installs the gem by shelling out to gem install" do + @new_resource.gem_binary('/usr/weird/bin/gem') + @provider.should_receive(:shell_out!).with("/usr/weird/bin/gem install rspec-core -q --no-rdoc --no-ri -v \"#{@spec_version}\"", :env=>nil) + @provider.action_install.should be_true + @provider.converge + end + + it "installs the gem from file by shelling out to gem install" do + @new_resource.gem_binary('/usr/weird/bin/gem') + @new_resource.source(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') + @new_resource.version('>= 0') + @provider.should_receive(:shell_out!).with("/usr/weird/bin/gem install #{CHEF_SPEC_DATA}/gems/chef-integration-test-0.1.0.gem -q --no-rdoc --no-ri -v \">= 0\"", :env=>nil) + @provider.action_install.should be_true + @provider.converge + end + + it "installs the gem from file by shelling out to gem install when the package is a path and the source is nil" do + @new_resource = Chef::Resource::GemPackage.new(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') + @provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + @new_resource.gem_binary('/usr/weird/bin/gem') + @new_resource.version('>= 0') + @new_resource.source.should == CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem' + @provider.should_receive(:shell_out!).with("/usr/weird/bin/gem install #{CHEF_SPEC_DATA}/gems/chef-integration-test-0.1.0.gem -q --no-rdoc --no-ri -v \">= 0\"", :env=>nil) + @provider.action_install.should be_true + @provider.converge + end + end + + end + + describe "when uninstalling a gem" do + before do + @new_resource = Chef::Resource::GemPackage.new("rspec") + @current_resource = @new_resource.dup + @current_resource.version('1.2.3') + @provider.new_resource = @new_resource + @provider.current_resource = @current_resource + end + + describe "in the current gem environment" do + it "uninstalls via the api when no explicit options are used" do + # pre-reqs for action_remove to actually remove the package: + @provider.new_resource.version.should be_nil + @provider.current_resource.version.should_not be_nil + # the behavior we're testing: + @provider.gem_env.should_receive(:uninstall).with('rspec', nil) + @provider.action_remove + @provider.converge + end + + it "uninstalls via the api when options are given as a Hash" do + # pre-reqs for action_remove to actually remove the package: + @provider.new_resource.version.should be_nil + @provider.current_resource.version.should_not be_nil + # the behavior we're testing: + @new_resource.options(:install_dir => '/alt/install/location') + @provider.gem_env.should_receive(:uninstall).with('rspec', nil, :install_dir => '/alt/install/location') + @provider.action_remove + @provider.converge + end + + it "uninstalls via the gem command when options are given as a String" do + @new_resource.options('-i /alt/install/location') + @provider.should_receive(:shell_out!).with("gem uninstall rspec -q -x -I -a -i /alt/install/location", :env=>nil) + @provider.action_remove + @provider.converge + end + + it "uninstalls a specific version of a gem when a version is provided" do + @new_resource.version('1.2.3') + @provider.gem_env.should_receive(:uninstall).with('rspec', '1.2.3') + @provider.action_remove + @provider.converge + end + end + + describe "in an alternate gem environment" do + it "uninstalls via the gem command" do + @new_resource.gem_binary('/usr/weird/bin/gem') + @provider.should_receive(:shell_out!).with("/usr/weird/bin/gem uninstall rspec -q -x -I -a", :env=>nil) + @provider.action_remove + @provider.converge + end + end + end +end + diff --git a/spec/unit/provider/package/smartos_spec.rb b/spec/unit/provider/package/smartos_spec.rb new file mode 100644 index 0000000000..b4cfe7e409 --- /dev/null +++ b/spec/unit/provider/package/smartos_spec.rb @@ -0,0 +1,83 @@ +# +# Author:: Trevor O (trevoro@joyent.com) +# Copyright:: Copyright (c) 2012 Opscode +# 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 File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "spec_helper")) +require 'ostruct' + +describe Chef::Provider::Package::SmartOS, "load_current_resource" do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Package.new("varnish") + @current_resource = Chef::Resource::Package.new("varnish") + + + @status = mock("Status", :exitstatus => 0) + @provider = Chef::Provider::Package::SmartOS.new(@new_resource, @run_context) + Chef::Resource::Package.stub!(:new).and_return(@current_resource) + @stdin = StringIO.new + @stdout = "varnish-2.1.5nb2\n" + @stderr = StringIO.new + @pid = 10 + @shell_out = OpenStruct.new(:stdout => @stdout, :stdin => @stdin, :stderr => @stderr, :status => @status, :exitstatus => 0) + end + + describe "when loading current resource" do + + it "should create a current resource with the name of the new_resource" do + @provider.should_receive(:shell_out!).and_return(@shell_out) + Chef::Resource::Package.should_receive(:new).and_return(@current_resource) + @provider.load_current_resource + end + + it "should set the current resource package name" do + @provider.should_receive(:shell_out!).and_return(@shell_out) + @current_resource.should_receive(:package_name).with(@new_resource.package_name) + @provider.load_current_resource + end + + it "should set the installed version if it is installed" do + @provider.should_receive(:shell_out!).and_return(@shell_out) + @provider.load_current_resource + @current_resource.version.should == "2.1.5nb2" + end + + it "should set the installed version to nil if it's not installed" do + out = OpenStruct.new(:stdout => nil) + @provider.should_receive(:shell_out!).and_return(out) + @provider.load_current_resource + @current_resource.version.should == nil + end + + + end + + describe "when manipulating a resource" do + + it "run pkgin and install the package" do + out = OpenStruct.new(:stdout => nil) + @provider.should_receive(:shell_out!).with("pkg_info -E \"varnish*\"", {:env => nil, :returns=>[0,1]}).and_return(@shell_out) + @provider.should_receive(:shell_out!).with("pkgin -y install varnish-2.1.5nb2", {:env=>nil}).and_return(out) + @provider.load_current_resource + @provider.install_package("varnish", "2.1.5nb2") + end + + end + +end diff --git a/spec/unit/provider/package/solaris_spec.rb b/spec/unit/provider/package/solaris_spec.rb new file mode 100644 index 0000000000..dd7cea2aaa --- /dev/null +++ b/spec/unit/provider/package/solaris_spec.rb @@ -0,0 +1,181 @@ +# +# Author:: Toomas Pelberg (<toomasp@gmx.net>) +# Copyright:: Copyright (c) 2010 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::Provider::Package::Solaris do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Package.new("SUNWbash") + @new_resource.source("/tmp/bash.pkg") + + @provider = Chef::Provider::Package::Solaris.new(@new_resource, @run_context) + ::File.stub!(:exists?).and_return(true) + end + + describe "assessing the current package status" do + before do + @pkginfo =<<-PKGINFO +PKGINST: SUNWbash +NAME: GNU Bourne-Again shell (bash) +CATEGORY: system +ARCH: sparc +VERSION: 11.10.0,REV=2005.01.08.05.16 +BASEDIR: / +VENDOR: Sun Microsystems, Inc. +DESC: GNU Bourne-Again shell (bash) version 3.0 +PSTAMP: sfw10-patch20070430084444 +INSTDATE: Nov 04 2009 01:02 +HOTLINE: Please contact your local service provider +PKGINFO + + @status = mock("Status", :exitstatus => 0) + end + + it "should create a current resource with the name of new_resource" do + @provider.stub!(:popen4).and_return(@status) + @provider.load_current_resource + @provider.current_resource.name.should == "SUNWbash" + end + + it "should set the current reource package name to the new resource package name" do + @provider.stub!(:popen4).and_return(@status) + @provider.load_current_resource + @provider.current_resource.package_name.should == "SUNWbash" + end + + it "should raise an exception if a source is supplied but not found" do + @provider.stub!(:popen4).and_return(@status) + ::File.stub!(:exists?).and_return(false) + @provider.define_resource_requirements + @provider.load_current_resource + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Package) + end + + + it "should get the source package version from pkginfo if provided" do + @stdout = StringIO.new(@pkginfo) + @stdin, @stderr = StringIO.new, StringIO.new + @provider.should_receive(:popen4).with("pkginfo -l -d /tmp/bash.pkg SUNWbash").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.should_receive(:popen4).with("pkginfo -l SUNWbash").and_return(@status) + @provider.load_current_resource + + @provider.current_resource.package_name.should == "SUNWbash" + @new_resource.version.should == "11.10.0,REV=2005.01.08.05.16" + end + + it "should return the current version installed if found by pkginfo" do + @stdout = StringIO.new(@pkginfo) + @stdin, @stderr = StringIO.new, StringIO.new + @provider.should_receive(:popen4).with("pkginfo -l -d /tmp/bash.pkg SUNWbash").and_return(@status) + @provider.should_receive(:popen4).with("pkginfo -l SUNWbash").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.load_current_resource + @provider.current_resource.version.should == "11.10.0,REV=2005.01.08.05.16" + end + + it "should raise an exception if the source is not set but we are installing" do + @new_resource = Chef::Resource::Package.new("SUNWbash") + @provider = Chef::Provider::Package::Solaris.new(@new_resource, @run_context) + @provider.stub!(:popen4).and_return(@status) + lambda { @provider.run_action(:install) }.should raise_error(Chef::Exceptions::Package) + end + + it "should raise an exception if pkginfo fails to run" do + @status = mock("Status", :exitstatus => -1) + @provider.stub!(:popen4).and_return(@status) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Package) + end + + it "should return a current resource with a nil version if the package is not found" do + @stdout = StringIO.new + @provider.should_receive(:popen4).with("pkginfo -l -d /tmp/bash.pkg SUNWbash").and_return(@status) + @provider.should_receive(:popen4).with("pkginfo -l SUNWbash").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.load_current_resource + @provider.current_resource.version.should be_nil + end + end + + describe "candidate_version" do + it "should return the candidate_version variable if already setup" do + @provider.candidate_version = "11.10.0,REV=2005.01.08.05.16" + @provider.should_not_receive(:popen4) + @provider.candidate_version + end + + it "should lookup the candidate_version if the variable is not already set" do + @status = mock("Status", :exitstatus => 0) + @provider.stub!(:popen4).and_return(@status) + @provider.should_receive(:popen4) + @provider.candidate_version + end + + it "should throw and exception if the exitstatus is not 0" do + @status = mock("Status", :exitstatus => 1) + @provider.stub!(:popen4).and_return(@status) + lambda { @provider.candidate_version }.should raise_error(Chef::Exceptions::Package) + end + + end + + describe "install and upgrade" do + it "should run pkgadd -n -d with the package source to install" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pkgadd -n -d /tmp/bash.pkg all" + }) + @provider.install_package("SUNWbash", "11.10.0,REV=2005.01.08.05.16") + end + + it "should run pkgadd -n -d when the package is a path to install" do + @new_resource = Chef::Resource::Package.new("/tmp/bash.pkg") + @provider = Chef::Provider::Package::Solaris.new(@new_resource, @run_context) + @new_resource.source.should == "/tmp/bash.pkg" + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pkgadd -n -d /tmp/bash.pkg all" + }) + @provider.install_package("/tmp/bash.pkg", "11.10.0,REV=2005.01.08.05.16") + end + + it "should run pkgadd -n -a /tmp/myadmin -d with the package options -a /tmp/myadmin" do + @new_resource.stub!(:options).and_return("-a /tmp/myadmin") + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pkgadd -n -a /tmp/myadmin -d /tmp/bash.pkg all" + }) + @provider.install_package("SUNWbash", "11.10.0,REV=2005.01.08.05.16") + end + end + + describe "remove" do + it "should run pkgrm -n to remove the package" do + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pkgrm -n SUNWbash" + }) + @provider.remove_package("SUNWbash", "11.10.0,REV=2005.01.08.05.16") + end + + it "should run pkgrm -n -a /tmp/myadmin with options -a /tmp/myadmin" do + @new_resource.stub!(:options).and_return("-a /tmp/myadmin") + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "pkgrm -n -a /tmp/myadmin SUNWbash" + }) + @provider.remove_package("SUNWbash", "11.10.0,REV=2005.01.08.05.16") + end + + end +end diff --git a/spec/unit/provider/package/yum_spec.rb b/spec/unit/provider/package/yum_spec.rb new file mode 100644 index 0000000000..0002ec39f3 --- /dev/null +++ b/spec/unit/provider/package/yum_spec.rb @@ -0,0 +1,1795 @@ +# +# 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' + +describe Chef::Provider::Package::Yum do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Package.new('cups') + @status = mock("Status", :exitstatus => 0) + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_installed => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => "1.2.4-11.18.el5_2.3", + :package_available? => true, + :version_available? => true, + :allow_multi_install => [ "kernel" ], + :package_repository => "base" + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @stderr = StringIO.new + @pid = mock("PID") + end + + describe "when loading the current system state" do + it "should create a current resource with the name of the new_resource" do + @provider.load_current_resource + @provider.current_resource.name.should == "cups" + end + + it "should set the current resources package name to the new resources package name" do + @provider.load_current_resource + @provider.current_resource.package_name.should == "cups" + end + + it "should set the installed version to nil on the current resource if no installed package" do + @yum_cache.stub!(:installed_version).and_return(nil) + @provider.load_current_resource + @provider.current_resource.version.should be_nil + end + + it "should set the installed version if yum has one" do + @provider.load_current_resource + @provider.current_resource.version.should == "1.2.4-11.18.el5" + end + + it "should set the candidate version if yum info has one" do + @provider.load_current_resource + @provider.candidate_version.should eql("1.2.4-11.18.el5_2.3") + end + + it "should return the current resouce" do + @provider.load_current_resource.should eql(@provider.current_resource) + end + + describe "when arch in package_name" do + it "should set the arch if no existing package_name is found and new_package_name+new_arch is available" do + @new_resource = Chef::Resource::YumPackage.new('testing.noarch') + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache' + ) + @yum_cache.stub!(:installed_version) do |package_name, arch| + # nothing installed for package_name/new_package_name + nil + end + @yum_cache.stub!(:candidate_version) do |package_name, arch| + if package_name == "testing.noarch" || package_name == "testing.more.noarch" + nil + # candidate for new_package_name + elsif package_name == "testing" || package_name == "testing.more" + "1.1" + end + end + @yum_cache.stub!(:package_available?).and_return(true) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + @provider.new_resource.package_name.should == "testing" + @provider.new_resource.arch.should == "noarch" + @provider.arch.should == "noarch" + + @new_resource = Chef::Resource::YumPackage.new('testing.more.noarch') + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + @provider.new_resource.package_name.should == "testing.more" + @provider.new_resource.arch.should == "noarch" + @provider.arch.should == "noarch" + end + + it "should not set the arch when an existing package_name is found" do + @new_resource = Chef::Resource::YumPackage.new('testing.beta3') + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache' + ) + @yum_cache.stub!(:installed_version) do |package_name, arch| + # installed for package_name + if package_name == "testing.beta3" || package_name == "testing.beta3.more" + "1.1" + elsif package_name == "testing" || package_name == "testing.beta3" + nil + end + end + @yum_cache.stub!(:candidate_version) do |package_name, arch| + # no candidate for package_name/new_package_name + nil + end + @yum_cache.stub!(:package_available?).and_return(true) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + # annoying side effect of the fun stub'ing above + @provider.load_current_resource + @provider.new_resource.package_name.should == "testing.beta3" + @provider.new_resource.arch.should == nil + @provider.arch.should == nil + + @new_resource = Chef::Resource::YumPackage.new('testing.beta3.more') + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + @provider.new_resource.package_name.should == "testing.beta3.more" + @provider.new_resource.arch.should == nil + @provider.arch.should == nil + end + + it "should not set the arch when no existing package_name or new_package_name+new_arch is found" do + @new_resource = Chef::Resource::YumPackage.new('testing.beta3') + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache' + ) + @yum_cache.stub!(:installed_version) do |package_name, arch| + # nothing installed for package_name/new_package_name + nil + end + @yum_cache.stub!(:candidate_version) do |package_name, arch| + # no candidate for package_name/new_package_name + nil + end + @yum_cache.stub!(:package_available?).and_return(true) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + @provider.new_resource.package_name.should == "testing.beta3" + @provider.new_resource.arch.should == nil + @provider.arch.should == nil + + @new_resource = Chef::Resource::YumPackage.new('testing.beta3.more') + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + @provider.new_resource.package_name.should == "testing.beta3.more" + @provider.new_resource.arch.should == nil + @provider.arch.should == nil + end + + it "should ensure it doesn't clobber an existing arch if passed" do + @new_resource = Chef::Resource::YumPackage.new('testing.i386') + @new_resource.arch("x86_64") + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache' + ) + @yum_cache.stub!(:installed_version) do |package_name, arch| + # nothing installed for package_name/new_package_name + nil + end + @yum_cache.stub!(:candidate_version) do |package_name, arch| + if package_name == "testing.noarch" + nil + # candidate for new_package_name + elsif package_name == "testing" + "1.1" + end + end.and_return("something") + @yum_cache.stub!(:package_available?).and_return(true) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + @provider.new_resource.package_name.should == "testing.i386" + @provider.new_resource.arch.should == "x86_64" + end + end + + it "should flush the cache if :before is true" do + @new_resource.stub!(:flush_cache).and_return({:after => false, :before => true}) + @yum_cache.should_receive(:reload).once + @provider.load_current_resource + end + + it "should flush the cache if :before is false" do + @new_resource.stub!(:flush_cache).and_return({:after => false, :before => false}) + @yum_cache.should_not_receive(:reload) + @provider.load_current_resource + end + + it "should search provides if package name can't be found then set package_name to match" do + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_installed => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => "1.2.4-11.18.el5", + :package_available? => false, + :version_available? => true + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + pkg = Chef::Provider::Package::Yum::RPMPackage.new("test-package", "1.2.4-11.18.el5", "x86_64", []) + @yum_cache.should_receive(:packages_from_require).and_return([pkg]) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + @new_resource.package_name.should == "test-package" + end + + it "should search provides if package name can't be found, warn about multiple matches, but use the first one" do + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_installed => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => "1.2.4-11.18.el5", + :package_available? => false, + :version_available? => true + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + pkg_x = Chef::Provider::Package::Yum::RPMPackage.new("test-package-x", "1.2.4-11.18.el5", "x86_64", []) + pkg_y = Chef::Provider::Package::Yum::RPMPackage.new("test-package-y", "1.2.6-11.3.el5", "i386", []) + @yum_cache.should_receive(:packages_from_require).and_return([pkg_x, pkg_y]) + Chef::Log.should_receive(:warn).exactly(1).times.with(%r{matched multiple Provides}) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + @new_resource.package_name.should == "test-package-x" + end + + it "should search provides if no package is available - if no match in installed provides then load the complete set" do + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_installed => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => "1.2.4-11.18.el5", + :package_available? => false, + :version_available? => true + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @yum_cache.should_receive(:packages_from_require).twice.and_return([]) + @yum_cache.should_receive(:reload_provides) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + end + + it "should search provides if no package is available and not load the complete set if action is :remove or :purge" do + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_installed => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => "1.2.4-11.18.el5", + :package_available? => false, + :version_available? => true + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @yum_cache.should_receive(:packages_from_require).once.and_return([]) + @yum_cache.should_not_receive(:reload_provides) + @new_resource.action(:remove) + @provider.load_current_resource + @yum_cache.should_receive(:packages_from_require).once.and_return([]) + @yum_cache.should_not_receive(:reload_provides) + @new_resource.action(:purge) + @provider.load_current_resource + end + + it "should search provides if no package is available - if no match in provides leave the name intact" do + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_provides => true, + :reload_installed => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => "1.2.4-11.18.el5", + :package_available? => false, + :version_available? => true + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @yum_cache.should_receive(:packages_from_require).twice.and_return([]) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + @new_resource.package_name.should == "cups" + end + end + + describe "when installing a package" do + it "should run yum install with the package name and version" do + @provider.load_current_resource + Chef::Provider::Package::Yum::RPMUtils.stub!(:rpmvercmp).and_return(-1) + @provider.should_receive(:yum_command).with( + "yum -d0 -e0 -y install emacs-1.0" + ) + @provider.install_package("emacs", "1.0") + end + + it "should run yum localinstall if given a path to an rpm" do + @new_resource.stub!(:source).and_return("/tmp/emacs-21.4-20.el5.i386.rpm") + @provider.should_receive(:yum_command).with( + "yum -d0 -e0 -y localinstall /tmp/emacs-21.4-20.el5.i386.rpm" + ) + @provider.install_package("emacs", "21.4-20.el5") + end + + it "should run yum localinstall if given a path to an rpm as the package" do + @new_resource = Chef::Resource::Package.new("/tmp/emacs-21.4-20.el5.i386.rpm") + ::File.stub!(:exists?).and_return(true) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @new_resource.source.should == "/tmp/emacs-21.4-20.el5.i386.rpm" + @provider.should_receive(:yum_command).with( + "yum -d0 -e0 -y localinstall /tmp/emacs-21.4-20.el5.i386.rpm" + ) + @provider.install_package("/tmp/emacs-21.4-20.el5.i386.rpm", "21.4-20.el5") + end + + it "should run yum install with the package name, version and arch" do + @provider.load_current_resource + @new_resource.stub!(:arch).and_return("i386") + Chef::Provider::Package::Yum::RPMUtils.stub!(:rpmvercmp).and_return(-1) + @provider.should_receive(:yum_command).with( + "yum -d0 -e0 -y install emacs-21.4-20.el5.i386" + ) + @provider.install_package("emacs", "21.4-20.el5") + end + + it "installs the package with the options given in the resource" do + @provider.load_current_resource + @provider.candidate_version = '11' + @new_resource.stub!(:options).and_return("--disablerepo epmd") + Chef::Provider::Package::Yum::RPMUtils.stub!(:rpmvercmp).and_return(-1) + @provider.should_receive(:yum_command).with( + "yum -d0 -e0 -y --disablerepo epmd install cups-11" + ) + @provider.install_package(@new_resource.name, @provider.candidate_version) + end + + it "should raise an exception if the package is not available" do + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_from_cache => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => "1.2.4-11.18.el5_2.3", + :package_available? => true, + :version_available? => nil + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + lambda { @provider.install_package("lolcats", "0.99") }.should raise_error(Chef::Exceptions::Package, %r{Version .* not found}) + end + + it "should raise an exception if candidate version is older than the installed version and allow_downgrade is false" do + @new_resource.stub!(:allow_downgrade).and_return(false) + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_installed => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => "1.2.4-11.15.el5", + :package_available? => true, + :version_available? => true, + :allow_multi_install => [ "kernel" ] + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + lambda { @provider.install_package("cups", "1.2.4-11.15.el5") }.should raise_error(Chef::Exceptions::Package, %r{is newer than candidate package}) + end + + it "should not raise an exception if candidate version is older than the installed version and the package is list in yum's installonlypkg option" do + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_installed => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => "1.2.4-11.15.el5", + :package_available? => true, + :version_available? => true, + :allow_multi_install => [ "cups" ], + :package_repository => "base" + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + @provider.should_receive(:yum_command).with( + "yum -d0 -e0 -y install cups-1.2.4-11.15.el5" + ) + @provider.install_package("cups", "1.2.4-11.15.el5") + end + + it "should run yum downgrade if candidate version is older than the installed version and allow_downgrade is true" do + @new_resource.stub!(:allow_downgrade).and_return(true) + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_installed => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => "1.2.4-11.15.el5", + :package_available? => true, + :version_available? => true, + :allow_multi_install => [], + :package_repository => "base" + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + @provider.should_receive(:yum_command).with( + "yum -d0 -e0 -y downgrade cups-1.2.4-11.15.el5" + ) + @provider.install_package("cups", "1.2.4-11.15.el5") + end + + it "should run yum install then flush the cache if :after is true" do + @new_resource.stub!(:flush_cache).and_return({:after => true, :before => false}) + @provider.load_current_resource + Chef::Provider::Package::Yum::RPMUtils.stub!(:rpmvercmp).and_return(-1) + @provider.should_receive(:yum_command).with( + "yum -d0 -e0 -y install emacs-1.0" + ) + @yum_cache.should_receive(:reload).once + @provider.install_package("emacs", "1.0") + end + + it "should run yum install then not flush the cache if :after is false" do + @new_resource.stub!(:flush_cache).and_return({:after => false, :before => false}) + @provider.load_current_resource + Chef::Provider::Package::Yum::RPMUtils.stub!(:rpmvercmp).and_return(-1) + @provider.should_receive(:yum_command).with( + "yum -d0 -e0 -y install emacs-1.0" + ) + @yum_cache.should_not_receive(:reload) + @provider.install_package("emacs", "1.0") + end + end + + describe "when upgrading a package" do + it "should run yum install if the package is installed and a version is given" do + @provider.load_current_resource + @provider.candidate_version = '11' + Chef::Provider::Package::Yum::RPMUtils.stub!(:rpmvercmp).and_return(-1) + @provider.should_receive(:yum_command).with( + "yum -d0 -e0 -y install cups-11" + ) + @provider.upgrade_package(@new_resource.name, @provider.candidate_version) + end + + it "should run yum install if the package is not installed" do + @provider.load_current_resource + @current_resource = Chef::Resource::Package.new('cups') + @provider.candidate_version = '11' + Chef::Provider::Package::Yum::RPMUtils.stub!(:rpmvercmp).and_return(-1) + @provider.should_receive(:yum_command).with( + "yum -d0 -e0 -y install cups-11" + ) + @provider.upgrade_package(@new_resource.name, @provider.candidate_version) + end + + it "should raise an exception if candidate version is older than the installed version" do + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_installed => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => "1.2.4-11.15.el5", + :package_available? => true, + :version_available? => true, + :allow_multi_install => [ "kernel" ] + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + lambda { @provider.upgrade_package("cups", "1.2.4-11.15.el5") }.should raise_error(Chef::Exceptions::Package, %r{is newer than candidate package}) + end + + # Test our little workaround, some crossover into Chef::Provider::Package territory + it "should call action_upgrade in the parent if the current resource version is nil" do + @yum_cache.stub!(:installed_version).and_return(nil) + @provider.load_current_resource + @current_resource = Chef::Resource::Package.new('cups') + @provider.candidate_version = '11' + @provider.should_receive(:upgrade_package).with( + "cups", + "11" + ) + @provider.action_upgrade + @provider.converge + end + + it "should call action_upgrade in the parent if the candidate version is nil" do + @provider.load_current_resource + @current_resource = Chef::Resource::Package.new('cups') + @provider.candidate_version = nil + @provider.should_not_receive(:upgrade_package) + @provider.action_upgrade + @provider.converge + end + + it "should call action_upgrade in the parent if the candidate is newer" do + @provider.load_current_resource + @current_resource = Chef::Resource::Package.new('cups') + @provider.candidate_version = '11' + @provider.should_receive(:upgrade_package).with( + "cups", + "11" + ) + @provider.action_upgrade + @provider.converge + end + + it "should not call action_upgrade in the parent if the candidate is older" do + @yum_cache.stub!(:installed_version).and_return("12") + @provider.load_current_resource + @current_resource = Chef::Resource::Package.new('cups') + @provider.candidate_version = '11' + @provider.should_not_receive(:upgrade_package) + @provider.action_upgrade + @provider.converge + end + end + + describe "when removing a package" do + it "should run yum remove with the package name" do + @provider.should_receive(:yum_command).with( + "yum -d0 -e0 -y remove emacs-1.0" + ) + @provider.remove_package("emacs", "1.0") + end + + it "should run yum remove with the package name and arch" do + @new_resource.stub!(:arch).and_return("x86_64") + @provider.should_receive(:yum_command).with( + "yum -d0 -e0 -y remove emacs-1.0.x86_64" + ) + @provider.remove_package("emacs", "1.0") + end + end + + describe "when purging a package" do + it "should run yum remove with the package name" do + @provider.should_receive(:yum_command).with( + "yum -d0 -e0 -y remove emacs-1.0" + ) + @provider.purge_package("emacs", "1.0") + end + end + + describe "when running yum" do + it "should run yum once if it exits with a return code of 0" do + @status = mock("Status", :exitstatus => 0) + @provider.stub!(:output_of_command).and_return([@status, "", ""]) + @provider.should_receive(:output_of_command).once.with( + "yum -d0 -e0 -y install emacs-1.0", + {} + ) + @provider.yum_command("yum -d0 -e0 -y install emacs-1.0") + end + + it "should run yum once if it exits with a return code > 0 and no scriptlet failures" do + @status = mock("Status", :exitstatus => 2) + @provider.stub!(:output_of_command).and_return([@status, "failure failure", "problem problem"]) + @provider.should_receive(:output_of_command).once.with( + "yum -d0 -e0 -y install emacs-1.0", + {} + ) + lambda { @provider.yum_command("yum -d0 -e0 -y install emacs-1.0") }.should raise_error(Chef::Exceptions::Exec) + end + + it "should run yum once if it exits with a return code of 1 and %pre scriptlet failures" do + @status = mock("Status", :exitstatus => 1) + @provider.stub!(:output_of_command).and_return([@status, "error: %pre(demo-1-1.el5.centos.x86_64) scriptlet failed, exit status 2", ""]) + @provider.should_receive(:output_of_command).once.with( + "yum -d0 -e0 -y install emacs-1.0", + {} + ) + # will still raise an exception, can't stub out the subsequent call + lambda { @provider.yum_command("yum -d0 -e0 -y install emacs-1.0") }.should raise_error(Chef::Exceptions::Exec) + end + + it "should run yum twice if it exits with a return code of 1 and %post scriptlet failures" do + @status = mock("Status", :exitstatus => 1) + @provider.stub!(:output_of_command).and_return([@status, "error: %post(demo-1-1.el5.centos.x86_64) scriptlet failed, exit status 2", ""]) + @provider.should_receive(:output_of_command).twice.with( + "yum -d0 -e0 -y install emacs-1.0", + {} + ) + # will still raise an exception, can't stub out the subsequent call + lambda { @provider.yum_command("yum -d0 -e0 -y install emacs-1.0") }.should raise_error(Chef::Exceptions::Exec) + end + end +end + +describe Chef::Provider::Package::Yum::RPMUtils do + describe "version_parse" do + before do + @rpmutils = Chef::Provider::Package::Yum::RPMUtils + end + + it "parses known good epoch strings" do + [ + [ "0:3.3", [ 0, "3.3", nil ] ], + [ "9:1.7.3", [ 9, "1.7.3", nil ] ], + [ "15:20020927", [ 15, "20020927", nil ] ] + ].each do |x, y| + @rpmutils.version_parse(x).should == y + end + end + + it "parses strange epoch strings" do + [ + [ ":3.3", [ 0, "3.3", nil ] ], + [ "-1:1.7.3", [ nil, nil, "1:1.7.3" ] ], + [ "-:20020927", [ nil, nil, ":20020927" ] ] + ].each do |x, y| + @rpmutils.version_parse(x).should == y + end + end + + it "parses known good version strings" do + [ + [ "3.3", [ nil, "3.3", nil ] ], + [ "1.7.3", [ nil, "1.7.3", nil ] ], + [ "20020927", [ nil, "20020927", nil ] ] + ].each do |x, y| + @rpmutils.version_parse(x).should == y + end + end + + it "parses strange version strings" do + [ + [ "3..3", [ nil, "3..3", nil ] ], + [ "0001.7.3", [ nil, "0001.7.3", nil ] ], + [ "20020927,3", [ nil, "20020927,3", nil ] ] + ].each do |x, y| + @rpmutils.version_parse(x).should == y + end + end + + it "parses known good version release strings" do + [ + [ "3.3-0.pre3.1.60.el5_5.1", [ nil, "3.3", "0.pre3.1.60.el5_5.1" ] ], + [ "1.7.3-1jpp.2.el5", [ nil, "1.7.3", "1jpp.2.el5" ] ], + [ "20020927-46.el5", [ nil, "20020927", "46.el5" ] ] + ].each do |x, y| + @rpmutils.version_parse(x).should == y + end + end + + it "parses strange version release strings" do + [ + [ "3.3-", [ nil, "3.3", nil ] ], + [ "-1jpp.2.el5", [ nil, nil, "1jpp.2.el5" ] ], + [ "-0020020927-46.el5", [ nil, "-0020020927", "46.el5" ] ] + ].each do |x, y| + @rpmutils.version_parse(x).should == y + end + end + end + + describe "rpmvercmp" do + before do + @rpmutils = Chef::Provider::Package::Yum::RPMUtils + end + + it "should validate version compare logic for standard examples" do + [ + # numeric + [ "0.0.2", "0.0.1", 1 ], + [ "0.2.0", "0.1.0", 1 ], + [ "2.0.0", "1.0.0", 1 ], + [ "0.0.1", "0.0.1", 0 ], + [ "0.0.1", "0.0.2", -1 ], + [ "0.1.0", "0.2.0", -1 ], + [ "1.0.0", "2.0.0", -1 ], + # alpha + [ "bb", "aa", 1 ], + [ "ab", "aa", 1 ], + [ "aa", "aa", 0 ], + [ "aa", "bb", -1 ], + [ "aa", "ab", -1 ], + [ "BB", "AA", 1 ], + [ "AA", "AA", 0 ], + [ "AA", "BB", -1 ], + [ "aa", "AA", 1 ], + [ "AA", "aa", -1 ], + # alphanumeric + [ "0.0.1b", "0.0.1a", 1 ], + [ "0.1b.0", "0.1a.0", 1 ], + [ "1b.0.0", "1a.0.0", 1 ], + [ "0.0.1a", "0.0.1a", 0 ], + [ "0.0.1a", "0.0.1b", -1 ], + [ "0.1a.0", "0.1b.0", -1 ], + [ "1a.0.0", "1b.0.0", -1 ], + # alphanumeric against alphanumeric + [ "0.0.1", "0.0.a", 1 ], + [ "0.1.0", "0.a.0", 1 ], + [ "1.0.0", "a.0.0", 1 ], + [ "0.0.a", "0.0.a", 0 ], + [ "0.0.a", "0.0.1", -1 ], + [ "0.a.0", "0.1.0", -1 ], + [ "a.0.0", "1.0.0", -1 ], + # alphanumeric against numeric + [ "0.0.2", "0.0.1a", 1 ], + [ "0.0.2a", "0.0.1", 1 ], + [ "0.0.1", "0.0.2a", -1 ], + [ "0.0.1a", "0.0.2", -1 ], + # length + [ "0.0.1aa", "0.0.1a", 1 ], + [ "0.0.1aa", "0.0.1aa", 0 ], + [ "0.0.1a", "0.0.1aa", -1 ], + ].each do |x, y, result| + @rpmutils.rpmvercmp(x,y).should == result + end + end + + it "should validate version compare logic for strange examples" do + [ + [ "2,0,0", "1.0.0", 1 ], + [ "0.0.1", "0,0.1", 0 ], + [ "1.0.0", "2,0,0", -1 ], + [ "002.0.0", "001.0.0", 1 ], + [ "001..0.1", "001..0.0", 1 ], + [ "-001..1", "-001..0", 1 ], + [ "1.0.1", nil, 1 ], + [ nil, nil, 0 ], + [ nil, "1.0.1", -1 ], + [ "1.0.1", "", 1 ], + [ "", "", 0 ], + [ "", "1.0.1", -1 ] + ].each do |x, y, result| + @rpmutils.rpmvercmp(x,y).should == result + end + end + + it "tests isalnum good input" do + [ 'a', 'z', 'A', 'Z', '0', '9' ].each do |t| + @rpmutils.isalnum(t).should == true + end + end + + it "tests isalnum bad input" do + [ '-', '.', '!', '^', ':', '_' ].each do |t| + @rpmutils.isalnum(t).should == false + end + end + + it "tests isalpha good input" do + [ 'a', 'z', 'A', 'Z', ].each do |t| + @rpmutils.isalpha(t).should == true + end + end + + it "tests isalpha bad input" do + [ '0', '9', '-', '.', '!', '^', ':', '_' ].each do |t| + @rpmutils.isalpha(t).should == false + end + end + + it "tests isdigit good input" do + [ '0', '9', ].each do |t| + @rpmutils.isdigit(t).should == true + end + end + + it "tests isdigit bad input" do + [ 'A', 'z', '-', '.', '!', '^', ':', '_' ].each do |t| + @rpmutils.isdigit(t).should == false + end + end + end + +end + +describe Chef::Provider::Package::Yum::RPMVersion do + describe "new - with parsing" do + before do + @rpmv = Chef::Provider::Package::Yum::RPMVersion.new("1:1.6.5-9.36.el5") + end + + it "should expose evr (name-version-release) available" do + @rpmv.e.should == 1 + @rpmv.v.should == "1.6.5" + @rpmv.r.should == "9.36.el5" + + @rpmv.evr.should == "1:1.6.5-9.36.el5" + end + + it "should output a version-release string" do + @rpmv.to_s.should == "1.6.5-9.36.el5" + end + end + + describe "new - no parsing" do + before do + @rpmv = Chef::Provider::Package::Yum::RPMVersion.new("1", "1.6.5", "9.36.el5") + end + + it "should expose evr (name-version-release) available" do + @rpmv.e.should == 1 + @rpmv.v.should == "1.6.5" + @rpmv.r.should == "9.36.el5" + + @rpmv.evr.should == "1:1.6.5-9.36.el5" + end + + it "should output a version-release string" do + @rpmv.to_s.should == "1.6.5-9.36.el5" + end + end + + it "should raise an error unless passed 1 or 3 args" do + lambda { + Chef::Provider::Package::Yum::RPMVersion.new() + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMVersion.new("1:1.6.5-9.36.el5") + }.should_not raise_error + lambda { + Chef::Provider::Package::Yum::RPMVersion.new("1:1.6.5-9.36.el5", "extra") + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMVersion.new("1", "1.6.5", "9.36.el5") + }.should_not raise_error + lambda { + Chef::Provider::Package::Yum::RPMVersion.new("1", "1.6.5", "9.36.el5", "extra") + }.should raise_error(ArgumentError) + end + + # thanks version_class_spec.rb! + describe "compare" do + it "should sort based on complete epoch-version-release data" do + [ + # smaller, larger + [ "0:1.6.5-9.36.el5", + "1:1.6.5-9.36.el5" ], + [ "0:2.3-15.el5", + "0:3.3-15.el5" ], + [ "0:alpha9.8-27.2", + "0:beta9.8-27.2" ], + [ "0:0.09-14jpp.3", + "0:0.09-15jpp.3" ], + [ "0:0.9.0-0.6.20110211.el5", + "0:0.9.0-0.6.20120211.el5" ], + [ "0:1.9.1-4.el5", + "0:1.9.1-5.el5" ], + [ "0:1.4.10-7.20090624svn.el5", + "0:1.4.10-7.20090625svn.el5" ], + [ "0:2.3.4-2.el5", + "0:2.3.4-2.el6" ] + ].each do |smaller, larger| + sm = Chef::Provider::Package::Yum::RPMVersion.new(smaller) + lg = Chef::Provider::Package::Yum::RPMVersion.new(larger) + sm.should be < lg + lg.should be > sm + sm.should_not == lg + end + end + + it "should sort based on partial epoch-version-release data" do + [ + # smaller, larger + [ ":1.6.5-9.36.el5", + "1:1.6.5-9.36.el5" ], + [ "2.3-15.el5", + "3.3-15.el5" ], + [ "alpha9.8", + "beta9.8" ], + [ "14jpp", + "15jpp" ], + [ "0.9.0-0.6", + "0.9.0-0.7" ], + [ "0:1.9", + "3:1.9" ], + [ "2.3-2.el5", + "2.3-2.el6" ] + ].each do |smaller, larger| + sm = Chef::Provider::Package::Yum::RPMVersion.new(smaller) + lg = Chef::Provider::Package::Yum::RPMVersion.new(larger) + sm.should be < lg + lg.should be > sm + sm.should_not == lg + end + end + + it "should verify equality of complete epoch-version-release data" do + [ + [ "0:1.6.5-9.36.el5", + "0:1.6.5-9.36.el5" ], + [ "0:2.3-15.el5", + "0:2.3-15.el5" ], + [ "0:alpha9.8-27.2", + "0:alpha9.8-27.2" ] + ].each do |smaller, larger| + sm = Chef::Provider::Package::Yum::RPMVersion.new(smaller) + lg = Chef::Provider::Package::Yum::RPMVersion.new(larger) + sm.should be == lg + end + end + + it "should verify equality of partial epoch-version-release data" do + [ + [ ":1.6.5-9.36.el5", + "0:1.6.5-9.36.el5" ], + [ "2.3-15.el5", + "2.3-15.el5" ], + [ "alpha9.8-3", + "alpha9.8-3" ] + ].each do |smaller, larger| + sm = Chef::Provider::Package::Yum::RPMVersion.new(smaller) + lg = Chef::Provider::Package::Yum::RPMVersion.new(larger) + sm.should be == lg + end + end + end + + describe "partial compare" do + it "should compare based on partial epoch-version-release data" do + [ + # smaller, larger + [ "0:1.1.1-1", + "1:" ], + [ "0:1.1.1-1", + "0:1.1.2" ], + [ "0:1.1.1-1", + "0:1.1.2-1" ], + [ "0:", + "1:1.1.1-1" ], + [ "0:1.1.1", + "0:1.1.2-1" ], + [ "0:1.1.1-1", + "0:1.1.2-1" ], + ].each do |smaller, larger| + sm = Chef::Provider::Package::Yum::RPMVersion.new(smaller) + lg = Chef::Provider::Package::Yum::RPMVersion.new(larger) + sm.partial_compare(lg).should be == -1 + lg.partial_compare(sm).should be == 1 + sm.partial_compare(lg).should_not be == 0 + end + end + + it "should verify equality based on partial epoch-version-release data" do + [ + [ "0:", + "0:1.1.1-1" ], + [ "0:1.1.1", + "0:1.1.1-1" ], + [ "0:1.1.1-1", + "0:1.1.1-1" ], + ].each do |smaller, larger| + sm = Chef::Provider::Package::Yum::RPMVersion.new(smaller) + lg = Chef::Provider::Package::Yum::RPMVersion.new(larger) + sm.partial_compare(lg).should be == 0 + end + end + end + +end + +describe Chef::Provider::Package::Yum::RPMPackage do + describe "new - with parsing" do + before do + @rpm = Chef::Provider::Package::Yum::RPMPackage.new("testing", "1:1.6.5-9.36.el5", "x86_64", []) + end + + it "should expose nevra (name-epoch-version-release-arch) available" do + @rpm.name.should == "testing" + @rpm.version.e.should == 1 + @rpm.version.v.should == "1.6.5" + @rpm.version.r.should == "9.36.el5" + @rpm.arch.should == "x86_64" + + @rpm.nevra.should == "testing-1:1.6.5-9.36.el5.x86_64" + @rpm.to_s.should == @rpm.nevra + end + + it "should always have at least one provide, itself" do + @rpm.provides.size.should == 1 + @rpm.provides[0].name == "testing" + @rpm.provides[0].version.evr == "1:1.6.5-9.36.el5" + @rpm.provides[0].flag == :== + end + end + + describe "new - no parsing" do + before do + @rpm = Chef::Provider::Package::Yum::RPMPackage.new("testing", "1", "1.6.5", "9.36.el5", "x86_64", []) + end + + it "should expose nevra (name-epoch-version-release-arch) available" do + @rpm.name.should == "testing" + @rpm.version.e.should == 1 + @rpm.version.v.should == "1.6.5" + @rpm.version.r.should == "9.36.el5" + @rpm.arch.should == "x86_64" + + @rpm.nevra.should == "testing-1:1.6.5-9.36.el5.x86_64" + @rpm.to_s.should == @rpm.nevra + end + + it "should always have at least one provide, itself" do + @rpm.provides.size.should == 1 + @rpm.provides[0].name == "testing" + @rpm.provides[0].version.evr == "1:1.6.5-9.36.el5" + @rpm.provides[0].flag == :== + end + end + + it "should raise an error unless passed 4 or 6 args" do + lambda { + Chef::Provider::Package::Yum::RPMPackage.new() + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMPackage.new("testing") + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMPackage.new("testing", "1:1.6.5-9.36.el5") + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMPackage.new("testing", "1:1.6.5-9.36.el5", "x86_64") + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMPackage.new("testing", "1:1.6.5-9.36.el5", "x86_64", []) + }.should_not raise_error + lambda { + Chef::Provider::Package::Yum::RPMPackage.new("testing", "1", "1.6.5", "9.36.el5", "x86_64") + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMPackage.new("testing", "1", "1.6.5", "9.36.el5", "x86_64", []) + }.should_not raise_error + lambda { + Chef::Provider::Package::Yum::RPMPackage.new("testing", "1", "1.6.5", "9.36.el5", "x86_64", [], "extra") + }.should raise_error(ArgumentError) + end + + describe "<=>" do + it "should sort alphabetically based on package name" do + [ + [ "a-test", + "b-test" ], + [ "B-test", + "a-test" ], + [ "A-test", + "B-test" ], + [ "Aa-test", + "aA-test" ], + [ "1test", + "2test" ], + ].each do |smaller, larger| + sm = Chef::Provider::Package::Yum::RPMPackage.new(smaller, "0:0.0.1-1", "x86_64", []) + lg = Chef::Provider::Package::Yum::RPMPackage.new(larger, "0:0.0.1-1", "x86_64", []) + sm.should be < lg + lg.should be > sm + sm.should_not == lg + end + end + + it "should sort alphabetically based on package arch" do + [ + [ "i386", + "x86_64" ], + [ "i386", + "noarch" ], + [ "noarch", + "x86_64" ], + ].each do |smaller, larger| + sm = Chef::Provider::Package::Yum::RPMPackage.new("test-package", "0:0.0.1-1", smaller, []) + lg = Chef::Provider::Package::Yum::RPMPackage.new("test-package", "0:0.0.1-1", larger, []) + sm.should be < lg + lg.should be > sm + sm.should_not == lg + end + end + end + +end + +describe Chef::Provider::Package::Yum::RPMDbPackage do + before(:each) do + # name, version, arch, installed, available, repoid + @rpm_x = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-b", "0:1.6.5-9.36.el5", "noarch", [], false, true, "base") + @rpm_y = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-b", "0:1.6.5-9.36.el5", "noarch", [], true, true, "extras") + @rpm_z = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-b", "0:1.6.5-9.36.el5", "noarch", [], true, false, "other") + end + + describe "initialize" do + it "should return a Chef::Provider::Package::Yum::RPMDbPackage object" do + @rpm_x.should be_kind_of(Chef::Provider::Package::Yum::RPMDbPackage) + end + end + + describe "available" do + it "should return true" do + @rpm_x.available.should be == true + @rpm_y.available.should be == true + @rpm_z.available.should be == false + end + end + + describe "installed" do + it "should return true" do + @rpm_x.installed.should be == false + @rpm_y.installed.should be == true + @rpm_z.installed.should be == true + end + end + + describe "repoid" do + it "should return the source repository repoid" do + @rpm_x.repoid.should be == "base" + @rpm_y.repoid.should be == "extras" + @rpm_z.repoid.should be == "other" + end + end +end + +describe Chef::Provider::Package::Yum::RPMDependency do + describe "new - with parsing" do + before do + @rpmdep = Chef::Provider::Package::Yum::RPMDependency.new("testing", "1:1.6.5-9.36.el5", :==) + end + + it "should expose name, version, flag available" do + @rpmdep.name.should == "testing" + @rpmdep.version.e.should == 1 + @rpmdep.version.v.should == "1.6.5" + @rpmdep.version.r.should == "9.36.el5" + @rpmdep.flag.should == :== + end + end + + describe "new - no parsing" do + before do + @rpmdep = Chef::Provider::Package::Yum::RPMDependency.new("testing", "1", "1.6.5", "9.36.el5", :==) + end + + it "should expose name, version, flag available" do + @rpmdep.name.should == "testing" + @rpmdep.version.e.should == 1 + @rpmdep.version.v.should == "1.6.5" + @rpmdep.version.r.should == "9.36.el5" + @rpmdep.flag.should == :== + end + end + + it "should raise an error unless passed 3 or 5 args" do + lambda { + Chef::Provider::Package::Yum::RPMDependency.new() + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMDependency.new("testing") + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMDependency.new("testing", "1:1.6.5-9.36.el5") + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMDependency.new("testing", "1:1.6.5-9.36.el5", :==) + }.should_not raise_error + lambda { + Chef::Provider::Package::Yum::RPMDependency.new("testing", "1:1.6.5-9.36.el5", :==, "extra") + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMDependency.new("testing", "1", "1.6.5", "9.36.el5", :==) + }.should_not raise_error + lambda { + Chef::Provider::Package::Yum::RPMDependency.new("testing", "1", "1.6.5", "9.36.el5", :==, "extra") + }.should raise_error(ArgumentError) + end + + describe "parse" do + it "should parse a name, flag, version string into a valid RPMDependency object" do + @rpmdep = Chef::Provider::Package::Yum::RPMDependency.parse("testing >= 1:1.6.5-9.36.el5") + + @rpmdep.name.should == "testing" + @rpmdep.version.e.should == 1 + @rpmdep.version.v.should == "1.6.5" + @rpmdep.version.r.should == "9.36.el5" + @rpmdep.flag.should == :>= + end + + it "should parse a name into a valid RPMDependency object" do + @rpmdep = Chef::Provider::Package::Yum::RPMDependency.parse("testing") + + @rpmdep.name.should == "testing" + @rpmdep.version.e.should == nil + @rpmdep.version.v.should == nil + @rpmdep.version.r.should == nil + @rpmdep.flag.should == :== + end + + it "should parse an invalid string into the name of a RPMDependency object" do + @rpmdep = Chef::Provider::Package::Yum::RPMDependency.parse("testing blah >") + + @rpmdep.name.should == "testing blah >" + @rpmdep.version.e.should == nil + @rpmdep.version.v.should == nil + @rpmdep.version.r.should == nil + @rpmdep.flag.should == :== + end + + it "should parse various valid flags" do + [ + [ ">", :> ], + [ ">=", :>= ], + [ "=", :== ], + [ "==", :== ], + [ "<=", :<= ], + [ "<", :< ] + ].each do |before, after| + @rpmdep = Chef::Provider::Package::Yum::RPMDependency.parse("testing #{before} 1:1.1-1") + @rpmdep.flag.should == after + end + end + + it "should parse various invalid flags and treat them as names" do + [ + [ "<>", :== ], + [ "!=", :== ], + [ ">>", :== ], + [ "<<", :== ], + [ "!", :== ], + [ "~", :== ] + ].each do |before, after| + @rpmdep = Chef::Provider::Package::Yum::RPMDependency.parse("testing #{before} 1:1.1-1") + @rpmdep.name.should == "testing #{before} 1:1.1-1" + @rpmdep.flag.should == after + end + end + end + + describe "satisfy?" do + it "should raise an error unless a RPMDependency is passed" do + @rpmprovide = Chef::Provider::Package::Yum::RPMDependency.new("testing", "1:1.6.5-9.36.el5", :==) + @rpmrequire = Chef::Provider::Package::Yum::RPMDependency.new("testing", "1:1.6.5-9.36.el5", :>=) + lambda { + @rpmprovide.satisfy?("hi") + }.should raise_error(ArgumentError) + lambda { + @rpmprovide.satisfy?(@rpmrequire) + }.should_not raise_error + end + + it "should validate dependency satisfaction logic for standard examples" do + [ + # names + [ "test", "test", true ], + [ "test", "foo", false ], + # full: epoch:version-relese + [ "testing = 1:1.1-1", "testing > 1:1.1-0", true ], + [ "testing = 1:1.1-1", "testing >= 1:1.1-0", true ], + [ "testing = 1:1.1-1", "testing >= 1:1.1-1", true ], + [ "testing = 1:1.1-1", "testing = 1:1.1-1", true ], + [ "testing = 1:1.1-1", "testing == 1:1.1-1", true ], + [ "testing = 1:1.1-1", "testing <= 1:1.1-1", true ], + [ "testing = 1:1.1-1", "testing <= 1:1.1-0", false ], + [ "testing = 1:1.1-1", "testing < 1:1.1-0", false ], + # partial: epoch:version + [ "testing = 1:1.1", "testing > 1:1.0", true ], + [ "testing = 1:1.1", "testing >= 1:1.0", true ], + [ "testing = 1:1.1", "testing >= 1:1.1", true ], + [ "testing = 1:1.1", "testing = 1:1.1", true ], + [ "testing = 1:1.1", "testing == 1:1.1", true ], + [ "testing = 1:1.1", "testing <= 1:1.1", true ], + [ "testing = 1:1.1", "testing <= 1:1.0", false ], + [ "testing = 1:1.1", "testing < 1:1.0", false ], + # partial: epoch + [ "testing = 1:", "testing > 0:", true ], + [ "testing = 1:", "testing >= 0:", true ], + [ "testing = 1:", "testing >= 1:", true ], + [ "testing = 1:", "testing = 1:", true ], + [ "testing = 1:", "testing == 1:", true ], + [ "testing = 1:", "testing <= 1:", true ], + [ "testing = 1:", "testing <= 0:", false ], + [ "testing = 1:", "testing < 0:", false ], + # mix and match! + [ "testing = 1:1.1-1", "testing == 1:1.1", true ], + [ "testing = 1:1.1-1", "testing == 1:", true ], + ].each do |prov, req, result| + @rpmprovide = Chef::Provider::Package::Yum::RPMDependency.parse(prov) + @rpmrequire = Chef::Provider::Package::Yum::RPMDependency.parse(req) + + @rpmprovide.satisfy?(@rpmrequire).should == result + @rpmrequire.satisfy?(@rpmprovide).should == result + end + end + end + +end + +# thanks resource_collection_spec.rb! +describe Chef::Provider::Package::Yum::RPMDb do + before(:each) do + @rpmdb = Chef::Provider::Package::Yum::RPMDb.new + # name, version, arch, installed, available + deps_v = [ + Chef::Provider::Package::Yum::RPMDependency.parse("libz.so.1()(64bit)"), + Chef::Provider::Package::Yum::RPMDependency.parse("test-package-a = 0:1.6.5-9.36.el5") + ] + deps_z = [ + Chef::Provider::Package::Yum::RPMDependency.parse("libz.so.1()(64bit)"), + Chef::Provider::Package::Yum::RPMDependency.parse("config(test) = 0:1.6.5-9.36.el5"), + Chef::Provider::Package::Yum::RPMDependency.parse("test-package-c = 0:1.6.5-9.36.el5") + ] + @rpm_v = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-a", "0:1.6.5-9.36.el5", "i386", deps_v, true, false, "base") + @rpm_w = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-b", "0:1.6.5-9.36.el5", "i386", [], true, true, "extras") + @rpm_x = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-b", "0:1.6.5-9.36.el5", "x86_64", [], false, true, "extras") + @rpm_y = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-b", "1:1.6.5-9.36.el5", "x86_64", [], true, true, "extras") + @rpm_z = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-c", "0:1.6.5-9.36.el5", "noarch", deps_z, true, true, "base") + @rpm_z_mirror = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-c", "0:1.6.5-9.36.el5", "noarch", deps_z, true, true, "base") + end + + describe "initialize" do + it "should return a Chef::Provider::Package::Yum::RPMDb object" do + @rpmdb.should be_kind_of(Chef::Provider::Package::Yum::RPMDb) + end + end + + describe "push" do + it "should accept an RPMDbPackage object through pushing" do + lambda { @rpmdb.push(@rpm_w) }.should_not raise_error + end + + it "should accept multiple RPMDbPackage object through pushing" do + lambda { @rpmdb.push(@rpm_w, @rpm_x, @rpm_y, @rpm_z) }.should_not raise_error + end + + it "should only accept an RPMDbPackage object" do + lambda { @rpmdb.push("string") }.should raise_error + end + + it "should add the package to the package db" do + @rpmdb.push(@rpm_w) + @rpmdb["test-package-b"].should_not be == nil + end + + it "should add conditionally add the package to the available list" do + @rpmdb.available_size.should be == 0 + @rpmdb.push(@rpm_v, @rpm_w) + @rpmdb.available_size.should be == 1 + end + + it "should add conditionally add the package to the installed list" do + @rpmdb.installed_size.should be == 0 + @rpmdb.push(@rpm_w, @rpm_x) + @rpmdb.installed_size.should be == 1 + end + + it "should have a total of 2 packages in the RPMDb" do + @rpmdb.size.should be == 0 + @rpmdb.push(@rpm_w, @rpm_x, @rpm_y, @rpm_z) + @rpmdb.size.should be == 2 + end + + it "should keep the Array unique when a duplicate is pushed" do + @rpmdb.push(@rpm_z, @rpm_z_mirror) + @rpmdb["test-package-c"].size.should be == 1 + end + + it "should register the package provides in the provides index" do + @rpmdb.push(@rpm_v, @rpm_w, @rpm_z) + @rpmdb.lookup_provides("test-package-a")[0].should be == @rpm_v + @rpmdb.lookup_provides("config(test)")[0].should be == @rpm_z + @rpmdb.lookup_provides("libz.so.1()(64bit)")[0].should be == @rpm_v + @rpmdb.lookup_provides("libz.so.1()(64bit)")[1].should be == @rpm_z + end + end + + describe "<<" do + it "should accept an RPMPackage object through the << operator" do + lambda { @rpmdb << @rpm_w }.should_not raise_error + end + end + + describe "lookup" do + it "should return an Array of RPMPackage objects by index" do + @rpmdb << @rpm_w + @rpmdb.lookup("test-package-b").should be_kind_of(Array) + end + end + + describe "[]" do + it "should return an Array of RPMPackage objects though the [index] operator" do + @rpmdb << @rpm_w + @rpmdb["test-package-b"].should be_kind_of(Array) + end + + it "should return an Array of 3 RPMPackage objects" do + @rpmdb.push(@rpm_w, @rpm_x, @rpm_y, @rpm_z) + @rpmdb["test-package-b"].size.should be == 3 + end + + it "should return an Array of RPMPackage objects sorted from newest to oldest" do + @rpmdb.push(@rpm_w, @rpm_x, @rpm_y, @rpm_z) + @rpmdb["test-package-b"][0].should be == @rpm_y + @rpmdb["test-package-b"][1].should be == @rpm_x + @rpmdb["test-package-b"][2].should be == @rpm_w + end + end + + describe "lookup_provides" do + it "should return an Array of RPMPackage objects by index" do + @rpmdb << @rpm_z + x = @rpmdb.lookup_provides("config(test)") + x.should be_kind_of(Array) + x[0].should be == @rpm_z + end + end + + describe "clear" do + it "should clear the RPMDb" do + @rpmdb.should_receive(:clear_available).once + @rpmdb.should_receive(:clear_installed).once + @rpmdb.push(@rpm_w, @rpm_x, @rpm_y, @rpm_z) + @rpmdb.size.should_not be == 0 + @rpmdb.lookup_provides("config(test)").should be_kind_of(Array) + @rpmdb.clear + @rpmdb.lookup_provides("config(test)").should be == nil + @rpmdb.size.should be == 0 + end + end + + describe "clear_available" do + it "should clear the available list" do + @rpmdb.push(@rpm_w, @rpm_x, @rpm_y, @rpm_z) + @rpmdb.available_size.should_not be == 0 + @rpmdb.clear_available + @rpmdb.available_size.should be == 0 + end + end + + describe "available?" do + it "should return true if a package is available" do + @rpmdb.available?(@rpm_w).should be == false + @rpmdb.push(@rpm_v, @rpm_w) + @rpmdb.available?(@rpm_v).should be == false + @rpmdb.available?(@rpm_w).should be == true + end + end + + describe "clear_installed" do + it "should clear the installed list" do + @rpmdb.push(@rpm_w, @rpm_x, @rpm_y, @rpm_z) + @rpmdb.installed_size.should_not be == 0 + @rpmdb.clear_installed + @rpmdb.installed_size.should be == 0 + end + end + + describe "installed?" do + it "should return true if a package is installed" do + @rpmdb.installed?(@rpm_w).should be == false + @rpmdb.push(@rpm_w, @rpm_x) + @rpmdb.installed?(@rpm_w).should be == true + @rpmdb.installed?(@rpm_x).should be == false + end + end + + describe "whatprovides" do + it "should raise an error unless a RPMDependency is passed" do + @rpmprovide = Chef::Provider::Package::Yum::RPMDependency.new("testing", "1:1.6.5-9.36.el5", :==) + @rpmrequire = Chef::Provider::Package::Yum::RPMDependency.new("testing", "1:1.6.5-9.36.el5", :>=) + lambda { + @rpmdb.whatprovides("hi") + }.should raise_error(ArgumentError) + lambda { + @rpmdb.whatprovides(@rpmrequire) + }.should_not raise_error + end + + it "should return an Array of packages statisfying a RPMDependency" do + @rpmdb.push(@rpm_v, @rpm_w, @rpm_z) + + @rpmrequire = Chef::Provider::Package::Yum::RPMDependency.parse("test-package-a >= 1.6.5") + x = @rpmdb.whatprovides(@rpmrequire) + x.should be_kind_of(Array) + x[0].should be == @rpm_v + + @rpmrequire = Chef::Provider::Package::Yum::RPMDependency.parse("libz.so.1()(64bit)") + x = @rpmdb.whatprovides(@rpmrequire) + x.should be_kind_of(Array) + x[0].should be == @rpm_v + x[1].should be == @rpm_z + end + end + +end + +describe Chef::Provider::Package::Yum::YumCache do + # allow for the reset of a Singleton + # thanks to Ian White (http://blog.ardes.com/2006/12/11/testing-singletons-with-ruby) + class << Chef::Provider::Package::Yum::YumCache + def reset_instance + Singleton.send :__init__, self + self + end + end + + before(:each) do + yum_dump_good_output = <<EOF +[option installonlypkgs] kernel kernel-bigmem kernel-enterprise +erlang-mochiweb 0 1.4.1 5.el5 x86_64 ['erlang-mochiweb = 1.4.1-5.el5', 'mochiweb = 1.4.1-5.el5'] i installed +zip 0 2.31 2.el5 x86_64 ['zip = 2.31-2.el5'] r base +zisofs-tools 0 1.0.6 3.2.2 x86_64 [] a extras +zlib 0 1.2.3 3 x86_64 ['zlib = 1.2.3-3', 'libz.so.1()(64bit)'] r base +zlib 0 1.2.3 3 i386 ['zlib = 1.2.3-3', 'libz.so.1'] r base +zlib-devel 0 1.2.3 3 i386 [] a extras +zlib-devel 0 1.2.3 3 x86_64 ['zlib-devel = 1.2.3-3'] r base +znc 0 0.098 1.el5 x86_64 [] a base +znc-devel 0 0.098 1.el5 i386 [] a extras +znc-devel 0 0.098 1.el5 x86_64 [] a base +znc-extra 0 0.098 1.el5 x86_64 [] a base +znc-modtcl 0 0.098 1.el5 x86_64 [] a base +znc-test.beta1 0 0.098 1.el5 x86_64 [] a extras +znc-test.test.beta1 0 0.098 1.el5 x86_64 [] a base +EOF + + yum_dump_bad_output_separators = <<EOF +zip 0 2.31 2.el5 x86_64 ['zip = 2.31-2.el5'] r base +zlib 0 1.2.3 3 x86_64 ['zlib = 1.2.3-3', 'libz.so.1()(64bit)'] i base bad +zlib-devel 0 1.2.3 3 i386 [] a extras +bad zlib-devel 0 1.2.3 3 x86_64 ['zlib-devel = 1.2.3-3'] i installed +znc-modtcl 0 0.098 1.el5 x86_64 [] a base bad +EOF + + yum_dump_bad_output_type = <<EOF +zip 0 2.31 2.el5 x86_64 ['zip = 2.31-2.el5'] r base +zlib 0 1.2.3 3 x86_64 ['zlib = 1.2.3-3', 'libz.so.1()(64bit)'] c base +zlib-devel 0 1.2.3 3 i386 [] a extras +zlib-devel 0 1.2.3 3 x86_64 ['zlib-devel = 1.2.3-3'] bad installed +znc-modtcl 0 0.098 1.el5 x86_64 [] a base +EOF + + yum_dump_error = <<EOF +yum-dump Config Error: File contains no section headers. +file: file://///etc/yum.repos.d/CentOS-Base.repo, line: 12 +'qeqwewe\n' +EOF + + @status = mock("Status", :exitstatus => 0) + @status_bad = mock("Status", :exitstatus => 1) + @stdin = mock("STDIN", :nil_object => true) + @stdout = mock("STDOUT", :nil_object => true) + @stdout_good = yum_dump_good_output.split("\n") + @stdout_bad_type = yum_dump_bad_output_type.split("\n") + @stdout_bad_separators = yum_dump_bad_output_separators.split("\n") + @stderr = mock("STDERR", :nil_object => true) + @stderr.stub!(:readlines).and_return(yum_dump_error.split("\n")) + @pid = mock("PID", :nil_object => true) + + # new singleton each time + Chef::Provider::Package::Yum::YumCache.reset_instance + @yc = Chef::Provider::Package::Yum::YumCache.instance + # load valid data + @yc.stub!(:popen4).and_yield(@pid, @stdin, @stdout_good, @stderr).and_return(@status) + end + + describe "initialize" do + it "should return a Chef::Provider::Package::Yum::YumCache object" do + @yc.should be_kind_of(Chef::Provider::Package::Yum::YumCache) + end + + it "should register reload for start of Chef::Client runs" do + Chef::Provider::Package::Yum::YumCache.reset_instance + Chef::Client.should_receive(:when_run_starts) do |&b| + b.should_not be_nil + end + @yc = Chef::Provider::Package::Yum::YumCache.instance + end + end + + describe "refresh" do + it "should implicitly call yum-dump.py only once by default after being instantiated" do + @yc.should_receive(:popen4).once + @yc.installed_version("zlib") + @yc.reset + @yc.installed_version("zlib") + end + + it "should run yum-dump.py using the system python when next_refresh is for :all" do + @yc.reload + @yc.should_receive(:popen4).with(%r{^/usr/bin/python .*/yum-dump.py --options --installed-provides$}, :waitlast=>true) + @yc.refresh + end + + it "should run yum-dump.py with the installed flag when next_refresh is for :installed" do + @yc.reload_installed + @yc.should_receive(:popen4).with(%r{^/usr/bin/python .*/yum-dump.py --installed$}, :waitlast=>true) + @yc.refresh + end + + it "should run yum-dump.py with the all-provides flag when next_refresh is for :provides" do + @yc.reload_provides + @yc.should_receive(:popen4).with(%r{^/usr/bin/python .*/yum-dump.py --options --all-provides$}, :waitlast=>true) + @yc.refresh + end + + it "should warn about invalid data with too many separators" do + @yc.stub!(:popen4).and_yield(@pid, @stdin, @stdout_bad_separators, @stderr).and_return(@status) + Chef::Log.should_receive(:warn).exactly(3).times.with(%r{Problem parsing}) + @yc.refresh + end + + it "should warn about invalid data with an incorrect type" do + @yc.stub!(:popen4).and_yield(@pid, @stdin, @stdout_bad_type, @stderr).and_return(@status) + Chef::Log.should_receive(:warn).exactly(2).times.with(%r{Problem parsing}) + @yc.refresh + end + + it "should warn about no output from yum-dump.py" do + @yc.stub!(:popen4).and_yield(@pid, @stdin, [], @stderr).and_return(@status) + Chef::Log.should_receive(:warn).exactly(1).times.with(%r{no output from yum-dump.py}) + @yc.refresh + end + + it "should raise exception yum-dump.py exits with a non zero status" do + @yc.stub!(:popen4).and_yield(@pid, @stdin, [], @stderr).and_return(@status_bad) + lambda { @yc.refresh}.should raise_error(Chef::Exceptions::Package, %r{CentOS-Base.repo, line: 12}) + end + + it "should parse type 'i' into an installed state for a package" do + @yc.available_version("erlang-mochiweb").should be == nil + @yc.installed_version("erlang-mochiweb").should_not be == nil + end + + it "should parse type 'a' into an available state for a package" do + @yc.available_version("znc").should_not be == nil + @yc.installed_version("znc").should be == nil + end + + it "should parse type 'r' into an installed and available states for a package" do + @yc.available_version("zip").should_not be == nil + @yc.installed_version("zip").should_not be == nil + end + + it "should parse installonlypkgs from yum-dump.py options output" do + @yc.allow_multi_install.should be == %w{kernel kernel-bigmem kernel-enterprise} + end + end + + describe "installed_version" do + it "should take one or two arguments" do + lambda { @yc.installed_version("zip") }.should_not raise_error(ArgumentError) + lambda { @yc.installed_version("zip", "i386") }.should_not raise_error(ArgumentError) + lambda { @yc.installed_version("zip", "i386", "extra") }.should raise_error(ArgumentError) + end + + it "should return version-release for matching package regardless of arch" do + @yc.installed_version("zip", "x86_64").should be == "2.31-2.el5" + @yc.installed_version("zip", nil).should be == "2.31-2.el5" + end + + it "should return version-release for matching package and arch" do + @yc.installed_version("zip", "x86_64").should be == "2.31-2.el5" + @yc.installed_version("zisofs-tools", "i386").should be == nil + end + + it "should return nil for an unmatched package" do + @yc.installed_version(nil, nil).should be == nil + @yc.installed_version("test1", nil).should be == nil + @yc.installed_version("test2", "x86_64").should be == nil + end + end + + describe "available_version" do + it "should take one or two arguments" do + lambda { @yc.available_version("zisofs-tools") }.should_not raise_error(ArgumentError) + lambda { @yc.available_version("zisofs-tools", "i386") }.should_not raise_error(ArgumentError) + lambda { @yc.available_version("zisofs-tools", "i386", "extra") }.should raise_error(ArgumentError) + end + + it "should return version-release for matching package regardless of arch" do + @yc.available_version("zip", "x86_64").should be == "2.31-2.el5" + @yc.available_version("zip", nil).should be == "2.31-2.el5" + end + + it "should return version-release for matching package and arch" do + @yc.available_version("zip", "x86_64").should be == "2.31-2.el5" + @yc.available_version("zisofs-tools", "i386").should be == nil + end + + it "should return nil for an unmatched package" do + @yc.available_version(nil, nil).should be == nil + @yc.available_version("test1", nil).should be == nil + @yc.available_version("test2", "x86_64").should be == nil + end + end + + describe "version_available?" do + it "should take two or three arguments" do + lambda { @yc.version_available?("zisofs-tools") }.should raise_error(ArgumentError) + lambda { @yc.version_available?("zisofs-tools", "1.0.6-3.2.2") }.should_not raise_error(ArgumentError) + lambda { @yc.version_available?("zisofs-tools", "1.0.6-3.2.2", "x86_64") }.should_not raise_error(ArgumentError) + end + + it "should return true if our package-version-arch is available" do + @yc.version_available?("zisofs-tools", "1.0.6-3.2.2", "x86_64").should be == true + end + + it "should return true if our package-version, no arch, is available" do + @yc.version_available?("zisofs-tools", "1.0.6-3.2.2", nil).should be == true + @yc.version_available?("zisofs-tools", "1.0.6-3.2.2").should be == true + end + + it "should return false if our package-version-arch isn't available" do + @yc.version_available?("zisofs-tools", "1.0.6-3.2.2", "pretend").should be == false + @yc.version_available?("zisofs-tools", "pretend", "x86_64").should be == false + @yc.version_available?("pretend", "1.0.6-3.2.2", "x86_64").should be == false + end + + it "should return false if our package-version, no arch, isn't available" do + @yc.version_available?("zisofs-tools", "pretend", nil).should be == false + @yc.version_available?("zisofs-tools", "pretend").should be == false + @yc.version_available?("pretend", "1.0.6-3.2.2").should be == false + end + end + + describe "package_repository" do + it "should take two or three arguments" do + lambda { @yc.package_repository("zisofs-tools") }.should raise_error(ArgumentError) + lambda { @yc.package_repository("zisofs-tools", "1.0.6-3.2.2") }.should_not raise_error(ArgumentError) + lambda { @yc.package_repository("zisofs-tools", "1.0.6-3.2.2", "x86_64") }.should_not raise_error(ArgumentError) + end + + it "should return repoid for package-version-arch" do + @yc.package_repository("zlib-devel", "1.2.3-3", "i386").should be == "extras" + @yc.package_repository("zlib-devel", "1.2.3-3", "x86_64").should be == "base" + end + + it "should return repoid for package-version, no arch" do + @yc.package_repository("zisofs-tools", "1.0.6-3.2.2", nil).should be == "extras" + @yc.package_repository("zisofs-tools", "1.0.6-3.2.2").should be == "extras" + end + + it "should return nil when no match for package-version-arch" do + @yc.package_repository("zisofs-tools", "1.0.6-3.2.2", "pretend").should be == nil + @yc.package_repository("zisofs-tools", "pretend", "x86_64").should be == nil + @yc.package_repository("pretend", "1.0.6-3.2.2", "x86_64").should be == nil + end + + it "should return nil when no match for package-version, no arch" do + @yc.package_repository("zisofs-tools", "pretend", nil).should be == nil + @yc.package_repository("zisofs-tools", "pretend").should be == nil + @yc.package_repository("pretend", "1.0.6-3.2.2").should be == nil + end + end + + describe "reset" do + it "should empty the installed and available packages RPMDb" do + @yc.available_version("zip", "x86_64").should be == "2.31-2.el5" + @yc.installed_version("zip", "x86_64").should be == "2.31-2.el5" + @yc.reset + @yc.available_version("zip", "x86_64").should be == nil + @yc.installed_version("zip", "x86_64").should be == nil + end + end + + describe "package_available?" do + it "should return true a package name is available" do + @yc.package_available?("zisofs-tools").should be == true + @yc.package_available?("moo").should be == false + @yc.package_available?(nil).should be == false + end + + it "should return true a package name + arch is available" do + @yc.package_available?("zlib-devel.i386").should be == true + @yc.package_available?("zisofs-tools.x86_64").should be == true + @yc.package_available?("znc-test.beta1.x86_64").should be == true + @yc.package_available?("znc-test.beta1").should be == true + @yc.package_available?("znc-test.test.beta1").should be == true + @yc.package_available?("moo.i386").should be == false + @yc.package_available?("zisofs-tools.beta").should be == false + @yc.package_available?("znc-test.test").should be == false + end + end + +end diff --git a/spec/unit/provider/package/zypper_spec.rb b/spec/unit/provider/package/zypper_spec.rb new file mode 100644 index 0000000000..fab78f4917 --- /dev/null +++ b/spec/unit/provider/package/zypper_spec.rb @@ -0,0 +1,159 @@ +# +# 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' + +describe Chef::Provider::Package::Zypper do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Package.new("cups") + + @current_resource = Chef::Resource::Package.new("cups") + + @status = mock("Status", :exitstatus => 0) + + @provider = Chef::Provider::Package::Zypper.new(@new_resource, @run_context) + Chef::Resource::Package.stub!(:new).and_return(@current_resource) + @provider.stub!(:popen4).and_return(@status) + @stderr = StringIO.new + @stdout = StringIO.new + @pid = mock("PID") + @provider.stub!(:`).and_return("2.0") + end + + describe "when loading the current package state" do + it "should create a current resource with the name of the new_resource" do + Chef::Resource::Package.should_receive(:new).and_return(@current_resource) + @provider.load_current_resource + end + + it "should set the current resources package name to the new resources package name" do + @current_resource.should_receive(:package_name).with(@new_resource.package_name) + @provider.load_current_resource + end + + it "should run zypper info with the package name" do + @provider.should_receive(:popen4).with("zypper info #{@new_resource.package_name}").and_return(@status) + @provider.load_current_resource + end + + it "should set the installed version to nil on the current resource if zypper info installed version is (none)" do + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @current_resource.should_receive(:version).with(nil).and_return(true) + @provider.load_current_resource + end + + it "should set the installed version if zypper info has one" do + @stdout = StringIO.new("Version: 1.0\nInstalled: Yes\n") + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @current_resource.should_receive(:version).with("1.0").and_return(true) + @provider.load_current_resource + end + + it "should set the candidate version if zypper info has one" do + @stdout = StringIO.new("Version: 1.0\nInstalled: No\nStatus: out-of-date (version 0.9 installed)") + + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.load_current_resource + @provider.candidate_version.should eql("1.0") + end + + it "should raise an exception if zypper info fails" do + @status.should_receive(:exitstatus).and_return(1) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Package) + end + + it "should not raise an exception if zypper info succeeds" do + @status.should_receive(:exitstatus).and_return(0) + lambda { @provider.load_current_resource }.should_not raise_error(Chef::Exceptions::Package) + end + + it "should return the current resouce" do + @provider.load_current_resource.should eql(@current_resource) + end + end + + describe "install_package" do + it "should run zypper install with the package name and version" do + @provider.should_receive(:run_command).with({ + :command => "zypper -n --no-gpg-checks install -l emacs=1.0", + }) + @provider.install_package("emacs", "1.0") + end + end + + describe "upgrade_package" do + it "should run zypper update with the package name and version" do + @provider.should_receive(:run_command).with({ + :command => "zypper -n --no-gpg-checks install -l emacs=1.0", + }) + @provider.upgrade_package("emacs", "1.0") + end + end + + describe "remove_package" do + it "should run zypper remove with the package name" do + @provider.should_receive(:run_command).with({ + :command => "zypper -n --no-gpg-checks remove emacs=1.0", + }) + @provider.remove_package("emacs", "1.0") + end + end + + describe "purge_package" do + it "should run remove_package with the name and version" do + @provider.should_receive(:remove_package).with("emacs", "1.0") + @provider.purge_package("emacs", "1.0") + end + end + + describe "on an older zypper" do + before(:each) do + @provider.stub!(:`).and_return("0.11.6") + end + + describe "install_package" do + it "should run zypper install with the package name and version" do + @provider.should_receive(:run_command).with({ + :command => "zypper install -y emacs" + }) + @provider.install_package("emacs", "1.0") + end + end + + describe "upgrade_package" do + it "should run zypper update with the package name and version" do + @provider.should_receive(:run_command).with({ + :command => "zypper install -y emacs" + }) + @provider.upgrade_package("emacs", "1.0") + end + end + + describe "remove_package" do + it "should run zypper remove with the package name" do + @provider.should_receive(:run_command).with({ + :command => "zypper remove -y emacs" + }) + @provider.remove_package("emacs", "1.0") + end + end + end +end diff --git a/spec/unit/provider/package_spec.rb b/spec/unit/provider/package_spec.rb new file mode 100644 index 0000000000..4052bc1ffd --- /dev/null +++ b/spec/unit/provider/package_spec.rb @@ -0,0 +1,429 @@ +# +# 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' + +describe Chef::Provider::Package do + before do + #Terrible, but we need to implement a pseduo-filesystem for testing + #to not have this line. Only affects updating state fields. + Chef::Provider::CookbookFile.any_instance.stub(:update_new_file_state) + + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Package.new('emacs') + @current_resource = Chef::Resource::Package.new('emacs') + @provider = Chef::Provider::Package.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + + @provider.candidate_version = "1.0" + end + + describe "when installing a package" do + before(:each) do + @provider.current_resource = @current_resource + @provider.stub!(:install_package).and_return(true) + end + + it "should raise a Chef::Exceptions::Package if no version is specified, and no candidate is available" do + @provider.candidate_version = nil + lambda { @provider.run_action(:install) }.should raise_error(Chef::Exceptions::Package) + end + + it "should call preseed_package if a response_file is given" do + @new_resource.response_file("foo") + @provider.should_receive(:get_preseed_file).with( + @new_resource.name, + @provider.candidate_version + ).and_return("/var/cache/preseed-test") + + @provider.should_receive(:preseed_package).with( + "/var/cache/preseed-test" + ).and_return(true) + @provider.run_action(:install) + end + + it "should not call preseed_package if a response_file is not given" do + @provider.should_not_receive(:preseed_package) + @provider.run_action(:install) + end + + it "should install the package at the candidate_version if it is not already installed" do + @provider.should_receive(:install_package).with( + @new_resource.name, + @provider.candidate_version + ).and_return(true) + @provider.run_action(:install) + @new_resource.should be_updated_by_last_action + end + + it "should install the package at the version specified if it is not already installed" do + @new_resource.version("1.0") + @provider.should_receive(:install_package).with( + @new_resource.name, + @new_resource.version + ).and_return(true) + @provider.run_action(:install) + @new_resource.should be_updated_by_last_action + end + + it "should install the package at the version specified if a different version is installed" do + @new_resource.version("1.0") + @current_resource.stub!(:version).and_return("0.99") + @provider.should_receive(:install_package).with( + @new_resource.name, + @new_resource.version + ).and_return(true) + @provider.run_action(:install) + @new_resource.should be_updated_by_last_action + end + + it "should not install the package if it is already installed and no version is specified" do + @current_resource.version("1.0") + @provider.should_not_receive(:install_package) + @provider.run_action(:install) + @new_resource.should_not be_updated_by_last_action + end + + it "should not install the package if it is already installed at the version specified" do + @current_resource.version("1.0") + @new_resource.version("1.0") + @provider.should_not_receive(:install_package) + @provider.run_action(:install) + @new_resource.should_not be_updated_by_last_action + end + + it "should call the candidate_version accessor only once if the package is already installed and no version is specified" do + @current_resource.version("1.0") + @provider.stub!(:candidate_version).and_return("1.0") + @provider.run_action(:install) + end + + it "should call the candidate_version accessor only once if the package is already installed at the version specified" do + @current_resource.version("1.0") + @new_resource.version("1.0") + @provider.run_action(:install) + end + + it "should set the resource to updated if it installs the package" do + @provider.run_action(:install) + @new_resource.should be_updated + end + + end + + describe "when upgrading the package" do + before(:each) do + @provider.stub!(:upgrade_package).and_return(true) + end + + it "should upgrade the package if the current version is not the candidate version" do + @provider.should_receive(:upgrade_package).with( + @new_resource.name, + @provider.candidate_version + ).and_return(true) + @provider.run_action(:upgrade) + @new_resource.should be_updated_by_last_action + end + + it "should set the resource to updated if it installs the package" do + @provider.run_action(:upgrade) + @new_resource.should be_updated + end + + it "should not install the package if the current version is the candidate version" do + @current_resource.version "1.0" + @provider.should_not_receive(:upgrade_package) + @provider.run_action(:upgrade) + @new_resource.should_not be_updated_by_last_action + end + + it "should print the word 'uninstalled' if there was no original version" do + @current_resource.stub!(:version).and_return(nil) + Chef::Log.should_receive(:info).with("package[emacs] upgraded from uninstalled to 1.0") + @provider.run_action(:upgrade) + @new_resource.should be_updated_by_last_action + end + + it "should raise a Chef::Exceptions::Package if current version and candidate are nil" do + @current_resource.stub!(:version).and_return(nil) + @provider.candidate_version = nil + lambda { @provider.run_action(:upgrade) }.should raise_error(Chef::Exceptions::Package) + end + + it "should not install the package if candidate version is nil" do + @current_resource.version "1.0" + @provider.candidate_version = nil + @provider.should_not_receive(:upgrade_package) + @provider.run_action(:upgrade) + @new_resource.should_not be_updated_by_last_action + end + end + + describe "When removing the package" do + before(:each) do + @provider.stub!(:remove_package).and_return(true) + @current_resource.version '1.4.2' + end + + it "should remove the package if it is installed" do + @provider.should be_removing_package + @provider.should_receive(:remove_package).with('emacs', nil) + @provider.run_action(:remove) + @new_resource.should be_updated + @new_resource.should be_updated_by_last_action + end + + it "should remove the package at a specific version if it is installed at that version" do + @new_resource.version "1.4.2" + @provider.should be_removing_package + @provider.should_receive(:remove_package).with('emacs', '1.4.2') + @provider.run_action(:remove) + @new_resource.should be_updated_by_last_action + end + + it "should not remove the package at a specific version if it is not installed at that version" do + @new_resource.version "1.0" + @provider.should_not be_removing_package + @provider.should_not_receive(:remove_package) + @provider.run_action(:remove) + @new_resource.should_not be_updated_by_last_action + end + + it "should not remove the package if it is not installed" do + @provider.should_not_receive(:remove_package) + @current_resource.stub!(:version).and_return(nil) + @provider.run_action(:remove) + @new_resource.should_not be_updated_by_last_action + end + + it "should set the resource to updated if it removes the package" do + @provider.run_action(:remove) + @new_resource.should be_updated + end + + end + + describe "When purging the package" do + before(:each) do + @provider.stub!(:purge_package).and_return(true) + @current_resource.version '1.4.2' + end + + it "should purge the package if it is installed" do + @provider.should be_removing_package + @provider.should_receive(:purge_package).with('emacs', nil) + @provider.run_action(:purge) + @new_resource.should be_updated + @new_resource.should be_updated_by_last_action + end + + it "should purge the package at a specific version if it is installed at that version" do + @new_resource.version "1.4.2" + @provider.should be_removing_package + @provider.should_receive(:purge_package).with('emacs', '1.4.2') + @provider.run_action(:purge) + @new_resource.should be_updated_by_last_action + end + + it "should not purge the package at a specific version if it is not installed at that version" do + @new_resource.version "1.0" + @provider.should_not be_removing_package + @provider.should_not_receive(:purge_package) + @provider.run_action(:purge) + @new_resource.should_not be_updated_by_last_action + end + + it "should not purge the package if it is not installed" do + @current_resource.instance_variable_set(:@version, nil) + @provider.should_not be_removing_package + + @provider.should_not_receive(:purge_package) + @provider.run_action(:purge) + @new_resource.should_not be_updated_by_last_action + end + + it "should set the resource to updated if it purges the package" do + @provider.run_action(:purge) + @new_resource.should be_updated + end + + end + + describe "when reconfiguring the package" do + before(:each) do + @provider.stub!(:reconfig_package).and_return(true) + end + + it "should info log, reconfigure the package and update the resource" do + @current_resource.stub!(:version).and_return('1.0') + @new_resource.stub!(:response_file).and_return(true) + @provider.should_receive(:get_preseed_file).and_return('/var/cache/preseed-test') + @provider.stub!(:preseed_package).and_return(true) + @provider.stub!(:reconfig_package).and_return(true) + Chef::Log.should_receive(:info).with("package[emacs] reconfigured") + @provider.should_receive(:reconfig_package) + @provider.run_action(:reconfig) + @new_resource.should be_updated + @new_resource.should be_updated_by_last_action + end + + it "should debug log and not reconfigure the package if the package is not installed" do + @current_resource.stub!(:version).and_return(nil) + Chef::Log.should_receive(:debug).with("package[emacs] is NOT installed - nothing to do") + @provider.should_not_receive(:reconfig_package) + @provider.run_action(:reconfig) + @new_resource.should_not be_updated_by_last_action + end + + it "should debug log and not reconfigure the package if no response_file is given" do + @current_resource.stub!(:version).and_return('1.0') + @new_resource.stub!(:response_file).and_return(nil) + Chef::Log.should_receive(:debug).with("package[emacs] no response_file provided - nothing to do") + @provider.should_not_receive(:reconfig_package) + @provider.run_action(:reconfig) + @new_resource.should_not be_updated_by_last_action + end + + it "should debug log and not reconfigure the package if the response_file has not changed" do + @current_resource.stub!(:version).and_return('1.0') + @new_resource.stub!(:response_file).and_return(true) + @provider.should_receive(:get_preseed_file).and_return(false) + @provider.stub!(:preseed_package).and_return(false) + Chef::Log.should_receive(:debug).with("package[emacs] preseeding has not changed - nothing to do") + @provider.should_not_receive(:reconfig_package) + @provider.run_action(:reconfig) + @new_resource.should_not be_updated_by_last_action + end + end + + describe "when running commands to be implemented by subclasses" do + it "should raises UnsupportedAction for install" do + lambda { @provider.install_package('emacs', '1.4.2') }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "should raises UnsupportedAction for upgrade" do + lambda { @provider.upgrade_package('emacs', '1.4.2') }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "should raises UnsupportedAction for remove" do + lambda { @provider.remove_package('emacs', '1.4.2') }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "should raises UnsupportedAction for purge" do + lambda { @provider.purge_package('emacs', '1.4.2') }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "should raise UnsupportedAction for preseed_package" do + preseed_file = "/tmp/sun-jdk-package-preseed-file.seed" + lambda { @provider.preseed_package(preseed_file) }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "should raise UnsupportedAction for reconfig" do + lambda { @provider.reconfig_package('emacs', '1.4.2') }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + end + + describe "when given a response file" do + before(:each) do + @cookbook_repo = File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks")) + Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest, @cookbook_repo) } + + @node = Chef::Node.new + cl = Chef::CookbookLoader.new(@cookbook_repo) + cl.load_cookbooks + @cookbook_collection = Chef::CookbookCollection.new(cl) + @run_context = Chef::RunContext.new(@node, @cookbook_collection, @events) + + @node.automatic_attrs[:platform] = 'PLATFORM: just testing' + @node.automatic_attrs[:platform_version] = 'PLATFORM VERSION: just testing' + + @new_resource.response_file('java.response') + @new_resource.cookbook_name = 'java' + end + + describe "creating the cookbook file resource to fetch the response file" do + before do + Chef::FileCache.should_receive(:create_cache_path).with('preseed/java').and_return("/tmp/preseed/java") + end + it "sets the preseed resource's runcontext to its own run context" do + Chef::FileCache.rspec_reset + Chef::FileCache.stub!(:create_cache_path).and_return("/tmp/preseed/java") + @provider.preseed_resource('java', '6').run_context.should_not be_nil + @provider.preseed_resource('java', '6').run_context.should equal(@provider.run_context) + end + + it "should set the cookbook name of the remote file to the new resources cookbook name" do + @provider.preseed_resource('java', '6').cookbook_name.should == 'java' + end + + it "should set remote files source to the new resources response file" do + @provider.preseed_resource('java', '6').source.should == 'java.response' + end + + it "should never back up the cached response file" do + @provider.preseed_resource('java', '6').backup.should be_false + end + + it "sets the install path of the resource to $file_cache/$cookbook/$pkg_name-$pkg_version.seed" do + @provider.preseed_resource('java', '6').path.should == '/tmp/preseed/java/java-6.seed' + end + end + + describe "when installing the preseed file to the cache location" do + before do + @node.automatic_attrs[:platform] = :just_testing + @node.automatic_attrs[:platform_version] = :just_testing + + @response_file_destination = Dir.tmpdir + '/preseed--java--java-6.seed' + + @response_file_resource = Chef::Resource::CookbookFile.new(@response_file_destination, @run_context) + @response_file_resource.cookbook_name = 'java' + @response_file_resource.backup(false) + @response_file_resource.source('java.response') + + + @provider.should_receive(:preseed_resource).with('java', '6').and_return(@response_file_resource) + end + + after do + FileUtils.rm(@response_file_destination) if ::File.exist?(@response_file_destination) + end + + it "creates the preseed file in the cache" do + @response_file_resource.should_receive(:run_action).with(:create) + @provider.get_preseed_file("java", "6") + end + + it "returns the path to the response file if the response file was updated" do + @provider.get_preseed_file("java", "6").should == @response_file_destination + end + + it "should return false if the response file has not been updated" do + @response_file_resource.updated_by_last_action(false) + @response_file_resource.should_not be_updated_by_last_action + # don't let the response_file_resource set updated to true + @response_file_resource.should_receive(:run_action).with(:create) + @provider.get_preseed_file("java", "6").should be(false) + end + + end + + end +end diff --git a/spec/unit/provider/remote_directory_spec.rb b/spec/unit/provider/remote_directory_spec.rb new file mode 100644 index 0000000000..19a17c269f --- /dev/null +++ b/spec/unit/provider/remote_directory_spec.rb @@ -0,0 +1,204 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2010 Daniel DeLeo +# 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 'digest/md5' +require 'tmpdir' +require 'chef/mixin/file_class' + +class Chef::CFCCheck + include Chef::Mixin::FileClass +end + +describe Chef::Provider::RemoteDirectory do + before do + Chef::FileAccessControl.any_instance.stub(:set_all) + #Terrible, but we need to implement a pseduo-filesystem for testing + #to not have this line. Only affects updating state fields. + Chef::Provider::CookbookFile.any_instance.stub(:update_new_file_state) + + @resource = Chef::Resource::RemoteDirectory.new("/tmp/tafty") + # in CHEF_SPEC_DATA/cookbooks/openldap/files/default/remotedir + @resource.source "remotedir" + @resource.cookbook('openldap') + + @cookbook_repo = ::File.expand_path(::File.join(CHEF_SPEC_DATA, "cookbooks")) + Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest, @cookbook_repo) } + + @node = Chef::Node.new + cl = Chef::CookbookLoader.new(@cookbook_repo) + cl.load_cookbooks + @cookbook_collection = Chef::CookbookCollection.new(cl) + + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, @cookbook_collection, @events) + + @provider = Chef::Provider::RemoteDirectory.new(@resource, @run_context) + @provider.current_resource = @resource.clone + end + + describe "when access control is configured on the resource" do + before do + @resource.mode "0750" + @resource.group "wheel" + @resource.owner "root" + + @resource.files_mode "0640" + @resource.files_group "staff" + @resource.files_owner "toor" + @resource.files_backup 23 + + @resource.source "remotedir_root" + end + + it "configures access control on intermediate directorys" do + directory_resource = @provider.send(:resource_for_directory, "/tmp/intermediate_dir") + directory_resource.path.should == "/tmp/intermediate_dir" + directory_resource.mode.should == "0750" + directory_resource.group.should == "wheel" + directory_resource.owner.should == "root" + directory_resource.recursive.should be_true + end + + it "configures access control on files in the directory" do + @resource.cookbook "berlin_style_tasty_cupcakes" + cookbook_file = @provider.send(:cookbook_file_resource, + "/target/destination/path.txt", + "relative/source/path.txt") + cookbook_file.cookbook_name.should == "berlin_style_tasty_cupcakes" + cookbook_file.source.should == "remotedir_root/relative/source/path.txt" + cookbook_file.mode.should == "0640" + cookbook_file.group.should == "staff" + cookbook_file.owner.should == "toor" + cookbook_file.backup.should == 23 + end + end + + describe "when creating the remote directory" do + before do + @node.automatic_attrs[:platform] = :just_testing + @node.automatic_attrs[:platform_version] = :just_testing + + @destination_dir = Dir.mktmpdir << "/remote_directory_test" + @resource.path(@destination_dir) + end + + after {FileUtils.rm_rf(@destination_dir)} + + it "transfers the directory with all contents" do + @provider.run_action(:create) + ::File.exist?(@destination_dir + '/remote_dir_file1.txt').should be_true + ::File.exist?(@destination_dir + '/remote_dir_file2.txt').should be_true + ::File.exist?(@destination_dir + '/remotesubdir/remote_subdir_file1.txt').should be_true + ::File.exist?(@destination_dir + '/remotesubdir/remote_subdir_file2.txt').should be_true + ::File.exist?(@destination_dir + '/remotesubdir/.a_dotfile').should be_true + ::File.exist?(@destination_dir + '/.a_dotdir/.a_dotfile_in_a_dotdir').should be_true + end + + describe "only if it is missing" do + it "should not overwrite existing files" do + @resource.overwrite(true) + @provider.run_action(:create) + + File.open(@destination_dir + '/remote_dir_file1.txt', 'a') {|f| f.puts "blah blah blah" } + File.open(@destination_dir + '/remotesubdir/remote_subdir_file1.txt', 'a') {|f| f.puts "blah blah blah" } + file1md5 = Digest::MD5.hexdigest(File.read(@destination_dir + '/remote_dir_file1.txt')) + subdirfile1md5 = Digest::MD5.hexdigest(File.read(@destination_dir + '/remotesubdir/remote_subdir_file1.txt')) + + @provider.run_action(:create_if_missing) + + file1md5.eql?(Digest::MD5.hexdigest(File.read(@destination_dir + '/remote_dir_file1.txt'))).should be_true + subdirfile1md5.eql?(Digest::MD5.hexdigest(File.read(@destination_dir + '/remotesubdir/remote_subdir_file1.txt'))).should be_true + end + end + + describe "with purging enabled" do + before {@resource.purge(true)} + + it "removes existing files if purge is true" do + @provider.run_action(:create) + FileUtils.touch(@destination_dir + '/marked_for_death.txt') + FileUtils.touch(@destination_dir + '/remotesubdir/marked_for_death_again.txt') + @provider.run_action(:create) + + ::File.exist?(@destination_dir + '/remote_dir_file1.txt').should be_true + ::File.exist?(@destination_dir + '/remote_dir_file2.txt').should be_true + ::File.exist?(@destination_dir + '/remotesubdir/remote_subdir_file1.txt').should be_true + ::File.exist?(@destination_dir + '/remotesubdir/remote_subdir_file2.txt').should be_true + + ::File.exist?(@destination_dir + '/marked_for_death.txt').should be_false + ::File.exist?(@destination_dir + '/remotesubdir/marked_for_death_again.txt').should be_false + end + + it "removes files in subdirectories before files above" do + @provider.run_action(:create) + FileUtils.mkdir_p(@destination_dir + '/a/multiply/nested/directory/') + FileUtils.touch(@destination_dir + '/a/foo.txt') + FileUtils.touch(@destination_dir + '/a/multiply/bar.txt') + FileUtils.touch(@destination_dir + '/a/multiply/nested/baz.txt') + FileUtils.touch(@destination_dir + '/a/multiply/nested/directory/qux.txt') + @provider.run_action(:create) + ::File.exist?(@destination_dir + '/a/foo.txt').should be_false + ::File.exist?(@destination_dir + '/a/multiply/bar.txt').should be_false + ::File.exist?(@destination_dir + '/a/multiply/nested/baz.txt').should be_false + ::File.exist?(@destination_dir + '/a/multiply/nested/directory/qux.txt').should be_false + end + + it "removes directory symlinks properly" do + symlinked_dir_path = @destination_dir + '/symlinked_dir' + @provider.action = :create + @provider.run_action + + @fclass = Chef::CFCCheck.new + + Dir.mktmpdir do |tmp_dir| + begin + @fclass.file_class.symlink(tmp_dir.dup, symlinked_dir_path) + ::File.exist?(symlinked_dir_path).should be_true + + @provider.run_action + + ::File.exist?(symlinked_dir_path).should be_false + ::File.exist?(tmp_dir).should be_true + rescue Chef::Exceptions::Win32APIError => e + pending "This must be run as an Administrator to create symlinks" + end + end + end + end + + describe "with overwrite disabled" do + before {@resource.purge(false)} + before {@resource.overwrite(false)} + + it "leaves modifications alone" do + @provider.run_action(:create) + ::File.open(@destination_dir + '/remote_dir_file1.txt', 'a') {|f| f.puts "blah blah blah" } + ::File.open(@destination_dir + '/remotesubdir/remote_subdir_file1.txt', 'a') {|f| f.puts "blah blah blah" } + file1md5 = Digest::MD5.hexdigest(::File.read(@destination_dir + '/remote_dir_file1.txt')) + subdirfile1md5 = Digest::MD5.hexdigest(::File.read(@destination_dir + '/remotesubdir/remote_subdir_file1.txt')) + @provider.stub!(:update_new_file_state) + @provider.run_action(:create) + file1md5.eql?(Digest::MD5.hexdigest(::File.read(@destination_dir + '/remote_dir_file1.txt'))).should be_true + subdirfile1md5.eql?(Digest::MD5.hexdigest(::File.read(@destination_dir + '/remotesubdir/remote_subdir_file1.txt'))).should be_true + end + end + + end +end + diff --git a/spec/unit/provider/remote_file_spec.rb b/spec/unit/provider/remote_file_spec.rb new file mode 100644 index 0000000000..78d7e77121 --- /dev/null +++ b/spec/unit/provider/remote_file_spec.rb @@ -0,0 +1,324 @@ +# +# 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' + +describe Chef::Provider::RemoteFile, "action_create" do + before(:each) do + @resource = Chef::Resource::RemoteFile.new("seattle") + @resource.path(File.expand_path(File.join(CHEF_SPEC_DATA, "seattle.txt"))) + @resource.source("http://foo") + @node = Chef::Node.new + @node.name "latte" + + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @provider = Chef::Provider::RemoteFile.new(@resource, @run_context) + #To prevent the current_resource.checksum from being overridden. + @provider.stub!(:load_current_resource) + end + + describe "when checking if the file is at the target version" do + it "considers the current file to be at the target version if it exists and matches the user-provided checksum" do + @provider.current_resource = @resource.dup + @resource.checksum("0fd012fdc96e96f8f7cf2046522a54aed0ce470224513e45da6bc1a17a4924aa") + @provider.current_resource.checksum("0fd012fdc96e96f8f7cf2046522a54aed0ce470224513e45da6bc1a17a4924aa") + @provider.current_resource_matches_target_checksum?.should be_true + end + end + + describe "when fetching the file from the remote" do + before(:each) do + @tempfile = Tempfile.new("chef-rspec-remote_file_spec-line#{__LINE__}--") + + @rest = mock(Chef::REST, { }) + Chef::REST.stub!(:new).and_return(@rest) + @rest.stub!(:streaming_request).and_return(@tempfile) + @rest.stub!(:create_url) { |url| url } + @resource.cookbook_name = "monkey" + + @provider.stub!(:checksum).and_return("0fd012fdc96e96f8f7cf2046522a54aed0ce470224513e45da6bc1a17a4924aa") + @provider.current_resource = @resource.clone + @provider.current_resource.checksum("0fd012fdc96e96f8f7cf2046522a54aed0ce470224513e45da6bc1a17a4924aa") + File.stub!(:exists?).and_return(true) + FileUtils.stub!(:cp).and_return(true) + Chef::Platform.stub!(:find_platform_and_version).and_return([ :mac_os_x, "10.5.1" ]) + end + + after do + @tempfile.close! + end + + before do + @resource.source("http://opscode.com/seattle.txt") + end + + describe "and the target location's enclosing directory does not exist" do + before do + @resource.path("/tmp/this/path/does/not/exist/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 + + shared_examples_for "source specified with multiple URIs" do + it "should try to download the next URI when the first one fails" do + @rest.should_receive(:streaming_request).with("http://foo", {}).once.and_raise(SocketError) + @rest.should_receive(:streaming_request).with("http://bar", {}).once.and_return(@tempfile) + @provider.run_action(:create) + end + + it "should raise an exception when all the URIs fail" do + @rest.should_receive(:streaming_request).with("http://foo", {}).once.and_raise(SocketError) + @rest.should_receive(:streaming_request).with("http://bar", {}).once.and_raise(SocketError) + lambda { @provider.run_action(:create) }.should raise_error(SocketError) + end + + it "should download from only one URI when the first one works" do + @rest.should_receive(:streaming_request).once.and_return(@tempfile) + @provider.run_action(:create) + end + + end + + describe "and the source specifies multiple URIs using multiple arguments" do + it_should_behave_like "source specified with multiple URIs" + + before(:each) do + @resource.source("http://foo", "http://bar") + end + end + + describe "and the source specifies multiple URIs using an array" do + it_should_behave_like "source specified with multiple URIs" + + before(:each) do + @resource.source([ "http://foo", "http://bar" ]) + end + end + + describe "and the resource specifies a checksum" do + + describe "and the existing file matches the checksum exactly" do + before do + @resource.checksum("0fd012fdc96e96f8f7cf2046522a54aed0ce470224513e45da6bc1a17a4924aa") + end + + it "does not download the file" do + @rest.should_not_receive(:fetch).with("http://opscode.com/seattle.txt").and_return(@tempfile) + @provider.run_action(:create) + end + + it "does not update the resource" do + @provider.run_action(:create) + @provider.new_resource.should_not be_updated + end + + end + + describe "and the existing file matches the given partial checksum" do + before do + @resource.checksum("0fd012fd") + end + + it "should not download the file if the checksum is a partial match from the beginning" do + @rest.should_not_receive(:fetch).with("http://opscode.com/seattle.txt").and_return(@tempfile) + @provider.run_action(:create) + end + + it "does not update the resource" do + @provider.run_action(:create) + @provider.new_resource.should_not be_updated + end + + end + + describe "and the existing file doesn't match the given checksum" do + it "downloads the file" do + @resource.checksum("this hash doesn't match") + @rest.should_receive(:streaming_request).with("http://opscode.com/seattle.txt", {}).and_return(@tempfile) + @provider.stub!(:update_new_file_state) + @provider.run_action(:create) + end + + it "does not consider the checksum a match if the matching string is offset" do + # i.e., the existing file is "0fd012fdc96e96f8f7cf2046522a54aed0ce470224513e45da6bc1a17a4924aa" + @resource.checksum("fd012fd") + @rest.should_receive(:streaming_request).with("http://opscode.com/seattle.txt", {}).and_return(@tempfile) + @provider.stub!(:update_new_file_state) + @provider.run_action(:create) + end + end + + end + + describe "and the resource doesn't specify a checksum" do + it "should download the file from the remote URL" do + @resource.checksum(nil) + @rest.should_receive(:streaming_request).with("http://opscode.com/seattle.txt", {}).and_return(@tempfile) + @provider.run_action(:create) + end + end + + # CHEF-3140 + # Some servers return tarballs as content type tar and encoding gzip, which + # is totally wrong. When this happens and gzip isn't disabled, Chef::REST + # will decompress the file for you, which is not at all what you expected + # to happen (you end up with an uncomressed tar archive instead of the + # gzipped tar archive you expected). To work around this behavior, we + # detect when users are fetching gzipped files and turn off gzip in + # Chef::REST. + + context "and the target file is a tarball" do + before do + @resource.path(File.expand_path(File.join(CHEF_SPEC_DATA, "seattle.tar.gz"))) + Chef::REST.should_receive(:new).with("http://opscode.com/seattle.txt", nil, nil, :disable_gzip => true).and_return(@rest) + end + + it "disables gzip in the http client" do + @provider.action_create + end + + end + + context "and the source appears to be a tarball" do + before do + @resource.source("http://example.com/tarball.tgz") + Chef::REST.should_receive(:new).with("http://example.com/tarball.tgz", nil, nil, :disable_gzip => true).and_return(@rest) + end + + it "disables gzip in the http client" do + @provider.action_create + end + end + + it "should raise an exception if it's any other kind of retriable response than 304" do + r = Net::HTTPMovedPermanently.new("one", "two", "three") + e = Net::HTTPRetriableError.new("301", r) + @rest.stub!(:streaming_request).and_raise(e) + lambda { @provider.run_action(:create) }.should raise_error(Net::HTTPRetriableError) + end + + it "should raise an exception if anything else happens" do + r = Net::HTTPBadRequest.new("one", "two", "three") + e = Net::HTTPServerException.new("fake exception", r) + @rest.stub!(:streaming_request).and_raise(e) + lambda { @provider.run_action(:create) }.should raise_error(Net::HTTPServerException) + end + + it "should checksum the raw file" do + @provider.should_receive(:checksum).with(@tempfile.path).and_return("0fd012fdc96e96f8f7cf2046522a54aed0ce470224513e45da6bc1a17a4924aa") + @provider.run_action(:create) + end + + describe "when the target file does not exist" do + before do + ::File.stub!(:exists?).with(@resource.path).and_return(false) + @provider.stub!(:get_from_server).and_return(@tempfile) + end + + it "should copy the raw file to the new resource" do + FileUtils.should_receive(:cp).with(@tempfile.path, @resource.path).and_return(true) + @provider.stub!(:update_new_file_state) + @provider.run_action(:create) + end + + it "should set the new resource to updated" do + @provider.stub!(:update_new_file_state) + @provider.run_action(:create) + @resource.should be_updated + end + + describe "and create_if_missing is invoked" do + it "should invoke action_create" do + @provider.should_receive(:action_create) + @provider.run_action(:create_if_missing) + end + end + end + + describe "when the target file already exists" do + before do + ::File.stub!(:exists?).with(@resource.path).and_return(true) + @provider.stub!(:diff_current).and_return([ + "--- /tmp/foo 2012-08-30 21:28:17.632782551 +0000", + "+++ /tmp/bar 2012-08-30 21:28:20.816975437 +0000", + "@@ -1 +1 @@", + "-foo bar", + "+bar foo" + ]) + @provider.stub!(:get_from_server).and_return(@tempfile) + end + + describe "and create_if_missing is invoked" do + it "should take no action" do + @provider.should_not_receive(:action_create) + @provider.run_action(:create_if_missing) + end + end + + describe "and the file downloaded from the remote is identical to the current" do + it "shouldn't backup the original file" do + @provider.should_not_receive(:backup).with(@resource.path) + @provider.run_action(:create) + end + + it "doesn't mark the resource as updated" do + @provider.run_action(:create) + @provider.new_resource.should_not be_updated + end + end + + describe "and the checksum doesn't match" do + before do + sha2_256 = "0fd012fdc96e96f8f7cf2046522a54aed0ce470224513e45da6bc1a17a4924aa-NO_MATCHY" + @provider.current_resource.checksum(sha2_256) + end + + it "should backup the original file" do + @provider.stub!(:update_new_file_state) + @provider.should_receive(:backup).with(@resource.path).and_return(true) + @provider.run_action(:create) + end + + it "should copy the raw file to the new resource" do + @provider.stub!(:update_new_file_state) + FileUtils.should_receive(:cp).with(@tempfile.path, @resource.path).and_return(true) + @provider.run_action(:create) + end + + it "should set the new resource to updated" do + @provider.stub!(:update_new_file_state) + @provider.run_action(:create) + @resource.should be_updated + end + end + + it "should set permissions" do + @provider.should_receive(:set_all_access_controls).and_return(true) + @provider.run_action(:create) + end + + + end + + end +end diff --git a/spec/unit/provider/route_spec.rb b/spec/unit/provider/route_spec.rb new file mode 100644 index 0000000000..3c5db0b7a1 --- /dev/null +++ b/spec/unit/provider/route_spec.rb @@ -0,0 +1,230 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org) +# Copyright:: Copyright (c) 2009 Bryan McLellan +# 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::Provider::Route do + before do + @node = Chef::Node.new + @cookbook_collection = Chef::CookbookCollection.new([]) + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, @cookbook_collection, @events) + + @new_resource = Chef::Resource::Route.new('10.0.0.10') + @new_resource.gateway "10.0.0.9" + @current_resource = Chef::Resource::Route.new('10.0.0.10') + @current_resource.gateway "10.0.0.9" + + @provider = Chef::Provider::Route.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + end + + describe Chef::Provider::Route, "hex2ip" do + it "should return nil if ip address is invalid" do + @provider.hex2ip('foo').should be_nil # does not even look like an ip + @provider.hex2ip('ABCDEFGH').should be_nil # 8 chars, but invalid + end + + it "should return quad-dotted notation for a valid IP" do + @provider.hex2ip('01234567').should == '103.69.35.1' + @provider.hex2ip('0064a8c0').should == '192.168.100.0' + @provider.hex2ip('00FFFFFF').should == '255.255.255.0' + end + end + + + describe Chef::Provider::Route, "load_current_resource" do + context "on linux" do + before do + @node.automatic_attrs[:os] = 'linux' + routing_table = "Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT\n" + + "eth0 0064A8C0 0984A8C0 0003 0 0 0 00FFFFFF 0 0 0\n" + route_file = StringIO.new(routing_table) + File.stub!(:open).with("/proc/net/route", "r").and_return(route_file) + end + + it "should set is_running to false when a route is not detected" do + resource = Chef::Resource::Route.new('10.10.10.0/24') + resource.stub!(:gateway).and_return("10.0.0.1") + resource.stub!(:device).and_return("eth0") + provider = Chef::Provider::Route.new(resource, @run_context) + + provider.load_current_resource + provider.is_running.should be_false + end + + it "should detect existing routes and set is_running attribute correctly" do + resource = Chef::Resource::Route.new('192.168.100.0/24') + resource.stub!(:gateway).and_return("192.168.132.9") + resource.stub!(:device).and_return("eth0") + provider = Chef::Provider::Route.new(resource, @run_context) + + provider.load_current_resource + provider.is_running.should be_true + end + + it "should use gateway value when matching routes" do + resource = Chef::Resource::Route.new('192.168.100.0/24') + resource.stub!(:gateway).and_return("10.10.10.10") + resource.stub!(:device).and_return("eth0") + provider = Chef::Provider::Route.new(resource, @run_context) + + provider.load_current_resource + provider.is_running.should be_false + end + end + end + + describe Chef::Provider::Route, "action_add" do + it "should add the route if it does not exist" do + @provider.stub!(:run_command).and_return(true) + @current_resource.stub!(:gateway).and_return(nil) + @provider.should_receive(:generate_command).once.with(:add) + @provider.should_receive(:generate_config) + @provider.run_action(:add) + @new_resource.should be_updated + end + + it "should not add the route if it exists" do + @provider.stub!(:run_command).and_return(true) + @provider.stub!(:is_running).and_return(true) + @provider.should_not_receive(:generate_command).with(:add) + @provider.should_receive(:generate_config) + @provider.run_action(:add) + @new_resource.should_not be_updated + end + end + + describe Chef::Provider::Route, "action_delete" do + it "should delete the route if it exists" do + @provider.stub!(:run_command).and_return(true) + @provider.should_receive(:generate_command).once.with(:delete) + @provider.stub!(:is_running).and_return(true) + @provider.run_action(:delete) + @new_resource.should be_updated + end + + it "should not delete the route if it does not exist" do + @current_resource.stub!(:gateway).and_return(nil) + @provider.stub!(:run_command).and_return(true) + @provider.should_not_receive(:generate_command).with(:add) + @provider.run_action(:delete) + @new_resource.should_not be_updated + end + end + + describe Chef::Provider::Route, "generate_command for action_add" do + it "should include a netmask when a one is specified" do + @new_resource.stub!(:netmask).and_return('255.255.0.0') + @provider.generate_command(:add).should match(/\/\d{1,2}\s/) + end + + it "should not include a netmask when a one is specified" do + @new_resource.stub!(:netmask).and_return(nil) + @provider.generate_command(:add).should_not match(/\/\d{1,2}\s/) + end + + it "should include ' via $gateway ' when a gateway is specified" do + @provider.generate_command(:add).should match(/\svia\s#{@new_resource.gateway}\s/) + end + + it "should not include ' via $gateway ' when a gateway is not specified" do + @new_resource.stub!(:gateway).and_return(nil) + @provider.generate_command(:add).should_not match(/\svia\s#{@new_resource.gateway}\s/) + end + end + + describe Chef::Provider::Route, "generate_command for action_delete" do + it "should include a netmask when a one is specified" do + @new_resource.stub!(:netmask).and_return('255.255.0.0') + @provider.generate_command(:delete).should match(/\/\d{1,2}\s/) + end + + it "should not include a netmask when a one is specified" do + @new_resource.stub!(:netmask).and_return(nil) + @provider.generate_command(:delete).should_not match(/\/\d{1,2}\s/) + end + + it "should include ' via $gateway ' when a gateway is specified" do + @provider.generate_command(:delete).should match(/\svia\s#{@new_resource.gateway}\s/) + end + + it "should not include ' via $gateway ' when a gateway is not specified" do + @new_resource.stub!(:gateway).and_return(nil) + @provider.generate_command(:delete).should_not match(/\svia\s#{@new_resource.gateway}\s/) + end + end + + describe Chef::Provider::Route, "config_file_contents for action_add" do + it "should include a netmask when a one is specified" do + @new_resource.stub!(:netmask).and_return('255.255.0.0') + @provider.config_file_contents(:add, { :target => @new_resource.target, :netmask => @new_resource.netmask}).should match(/\/\d{1,2}.*\n$/) + end + + it "should not include a netmask when a one is specified" do + @provider.config_file_contents(:add, { :target => @new_resource.target}).should_not match(/\/\d{1,2}.*\n$/) + end + + it "should include ' via $gateway ' when a gateway is specified" do + @provider.config_file_contents(:add, { :target => @new_resource.target, :gateway => @new_resource.gateway}).should match(/\svia\s#{@new_resource.gateway}\n/) + end + + it "should not include ' via $gateway ' when a gateway is not specified" do + @provider.generate_command(:add).should_not match(/\svia\s#{@new_resource.gateway}\n/) + end + end + + describe Chef::Provider::Route, "config_file_contents for action_delete" do + it "should return an empty string" do + @provider.config_file_contents(:delete).should match(/^$/) + end + end + + describe Chef::Provider::Route, "generate_config method" do + %w[ centos redhat fedora ].each do |platform| + it "should write a route file on #{platform} platform" do + @node.automatic_attrs[:platform] = platform + + route_file = StringIO.new + File.should_receive(:new).with("/etc/sysconfig/network-scripts/route-eth0", "w").and_return(route_file) + #Chef::Log.should_receive(:debug).with("route[10.0.0.10] writing route.eth0\n10.0.0.10 via 10.0.0.9\n") + @run_context.resource_collection << @new_resource + + @provider.generate_config + @provider.converge + end + end + + it "should put all routes for a device in a route config file" do + @node.automatic_attrs[:platform] = 'centos' + + route_file = StringIO.new + File.should_receive(:new).and_return(route_file) + @run_context.resource_collection << Chef::Resource::Route.new('192.168.1.0/24 via 192.168.0.1') + @run_context.resource_collection << Chef::Resource::Route.new('192.168.2.0/24 via 192.168.0.1') + @run_context.resource_collection << Chef::Resource::Route.new('192.168.3.0/24 via 192.168.0.1') + + @provider.generate_config + @provider.converge + route_file.string.split("\n").should have(3).items + route_file.string.should match(/^192.168.1.0\/24 via 192.168.0.1$/) + route_file.string.should match(/^192.168.2.0\/24 via 192.168.0.1$/) + route_file.string.should match(/^192.168.3.0\/24 via 192.168.0.1$/) + end + end +end diff --git a/spec/unit/provider/ruby_block_spec.rb b/spec/unit/provider/ruby_block_spec.rb new file mode 100644 index 0000000000..7fc58c9c70 --- /dev/null +++ b/spec/unit/provider/ruby_block_spec.rb @@ -0,0 +1,46 @@ +# +# Author:: AJ Christensen (<aj@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode +# 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::Provider::RubyBlock, "initialize" do + before(:each) do + $evil_global_evil_laugh = :wahwah + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::RubyBlock.new("bloc party") + @new_resource.block { $evil_global_evil_laugh = :mwahahaha} + @provider = Chef::Provider::RubyBlock.new(@new_resource, @run_context) + end + + it "should call the block and flag the resource as updated" do + @provider.run_action(:run) + $evil_global_evil_laugh.should == :mwahahaha + @new_resource.should be_updated + end + + it "accepts `create' as an alias for `run'" do + # SEE ALSO: CHEF-3500 + # "create" used to be the default action, it was renamed. + @provider.run_action(:create) + $evil_global_evil_laugh.should == :mwahahaha + @new_resource.should be_updated + end +end + diff --git a/spec/unit/provider/script_spec.rb b/spec/unit/provider/script_spec.rb new file mode 100644 index 0000000000..5111a94578 --- /dev/null +++ b/spec/unit/provider/script_spec.rb @@ -0,0 +1,96 @@ +# +# Author:: Adam Jacob (adam@opscode.com) +# Copyright:: Copyright (c) 2009 Opscode +# 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::Provider::Script, "action_run" do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Script.new('run some perl code') + @new_resource.code "$| = 1; print 'i like beans'" + @new_resource.interpreter 'perl' + + @provider = Chef::Provider::Script.new(@new_resource, @run_context) + + @script_file = StringIO.new + @script_file.stub!(:path).and_return('/tmp/the_script_file') + + @provider.stub!(:shell_out!).and_return(true) + end + + it "creates a temporary file to store the script" do + @provider.script_file.should be_an_instance_of(Tempfile) + end + + it "unlinks the tempfile when finished" do + tempfile_path = @provider.script_file.path + @provider.unlink_script_file + File.exist?(tempfile_path).should be_false + end + + it "sets the owner and group for the script file" do + @new_resource.user 'toor' + @new_resource.group 'wheel' + @provider.stub!(:script_file).and_return(@script_file) + FileUtils.should_receive(:chown).with('toor', 'wheel', "/tmp/the_script_file") + @provider.set_owner_and_group + end + + context "with the script file set to the correct owner and group" do + before do + @provider.stub!(:set_owner_and_group) + @provider.stub!(:script_file).and_return(@script_file) + end + describe "when writing the script to the file" do + it "should put the contents of the script in the temp file" do + @provider.action_run + @script_file.rewind + @script_file.string.should == "$| = 1; print 'i like beans'\n" + end + + it "closes before executing the script and unlinks it when finished" do + @provider.action_run + @script_file.should be_closed + end + + end + + describe "when running the script" do + it 'should set the command to "interpreter" "tempfile"' do + @provider.action_run + @new_resource.command.should == '"perl" "/tmp/the_script_file"' + end + + describe "with flags set on the resource" do + before do + @new_resource.flags '-f' + end + + it "should set the command to 'interpreter flags tempfile'" do + @provider.action_run + @new_resource.command.should == '"perl" -f "/tmp/the_script_file"' + end + + end + + end + end + +end diff --git a/spec/unit/provider/service/arch_service_spec.rb b/spec/unit/provider/service/arch_service_spec.rb new file mode 100644 index 0000000000..a7afa28da1 --- /dev/null +++ b/spec/unit/provider/service/arch_service_spec.rb @@ -0,0 +1,330 @@ +# +# Author:: Jan Zimmek (<jan.zimmek@web.de>) +# Author:: AJ Christensen (<aj@hjksolutions.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 'ostruct' + + +# most of this code has been ripped from init_service_spec.rb +# and is only slightly modified to match "arch" needs. + +describe Chef::Provider::Service::Arch, "load_current_resource" do + before(:each) do + @node = Chef::Node.new + @node.automatic_attrs[:command] = {:ps => "ps -ef"} + + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Service.new("chef") + @new_resource.pattern("chef") + @new_resource.supports({:status => false}) + + + @provider = Chef::Provider::Service::Arch.new(@new_resource, @run_context) + + ::File.stub!(:exists?).with("/etc/rc.conf").and_return(true) + ::File.stub!(:read).with("/etc/rc.conf").and_return("DAEMONS=(network apache sshd)") + end + + describe "when first created" do + it "should set the current resources service name to the new resources service name" do + @provider.stub(:shell_out).and_return(OpenStruct.new(:exitstatus => 0, :stdout => "")) + @provider.load_current_resource + @provider.current_resource.service_name.should == 'chef' + end + end + + + describe "when the service supports status" do + before do + @new_resource.supports({:status => true}) + end + + it "should run '/etc/rc.d/service_name status'" do + @provider.should_receive(:shell_out).with("/etc/rc.d/chef status").and_return(OpenStruct.new(:exitstatus => 0)) + @provider.load_current_resource + end + + it "should set running to true if the the status command returns 0" do + @provider.stub!(:shell_out).with("/etc/rc.d/chef status").and_return(OpenStruct.new(:exitstatus => 0)) + @provider.load_current_resource + @provider.current_resource.running.should be_true + end + + it "should set running to false if the status command returns anything except 0" do + @provider.stub!(:shell_out).with("/etc/rc.d/chef status").and_return(OpenStruct.new(:exitstatus => 1)) + @provider.load_current_resource + @provider.current_resource.running.should be_false + end + + it "should set running to false if the status command raises" do + @provider.stub!(:shell_out).with("/etc/rc.d/chef status").and_raise(Mixlib::ShellOut::ShellCommandFailed) + @provider.load_current_resource + @provider.current_resource.running.should be_false + end + + end + + + describe "when a status command has been specified" do + before do + @new_resource.status_command("/etc/rc.d/chefhasmonkeypants status") + end + + it "should run the services status command if one has been specified" do + @provider.should_receive(:shell_out).with("/etc/rc.d/chefhasmonkeypants status").and_return(OpenStruct.new(:exitstatus => 0)) + @provider.load_current_resource + end + + end + + it "should raise error if the node has a nil ps attribute and no other means to get status" do + @node.automatic_attrs[:command] = {:ps => nil} + @provider.define_resource_requirements + @provider.action = :start + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + + it "should raise error if the node has an empty ps attribute and no other means to get status" do + @node.automatic_attrs[:command] = {:ps => ""} + @provider.define_resource_requirements + @provider.action = :start + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + + + it "should fail if file /etc/rc.conf does not exist" do + ::File.stub!(:exists?).with("/etc/rc.conf").and_return(false) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Service) + end + + it "should fail if file /etc/rc.conf does not contain DAEMONS array" do + ::File.stub!(:read).with("/etc/rc.conf").and_return("") + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Service) + end + + describe "when discovering service status with ps" do + before do + @stdout = StringIO.new(<<-DEFAULT_PS) +aj 7842 5057 0 21:26 pts/2 00:00:06 vi init.rb +aj 7903 5016 0 21:26 pts/5 00:00:00 /bin/bash +aj 8119 6041 0 21:34 pts/3 00:00:03 vi init_service_spec.rb +DEFAULT_PS + @status = mock("Status", :exitstatus => 0, :stdout => @stdout) + @provider.stub!(:shell_out!).and_return(@status) + + @node.automatic_attrs[:command] = {:ps => "ps -ef"} + end + + it "determines the service is running when it appears in ps" do + @stdout = StringIO.new(<<-RUNNING_PS) +aj 7842 5057 0 21:26 pts/2 00:00:06 chef +aj 7842 5057 0 21:26 pts/2 00:00:06 poos +RUNNING_PS + @status.stub!(:stdout).and_return(@stdout) + @provider.load_current_resource + @provider.current_resource.running.should be_true + end + + it "determines the service is not running when it does not appear in ps" do + @provider.stub!(:shell_out!).and_return(@status) + @provider.load_current_resource + @provider.current_resource.running.should be_false + end + + it "should raise an exception if ps fails" do + @provider.stub!(:shell_out!).and_raise(Mixlib::ShellOut::ShellCommandFailed) + @provider.load_current_resource + @provider.action = :start + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + end + + it "should return existing entries in DAEMONS array" do + ::File.stub!(:read).with("/etc/rc.conf").and_return("DAEMONS=(network !apache ssh)") + @provider.daemons.should == ['network', '!apache', 'ssh'] + end + + context "when the current service status is known" do + before do + @current_resource = Chef::Resource::Service.new("chef") + @provider.current_resource = @current_resource + end + + describe Chef::Provider::Service::Arch, "enable_service" do + # before(:each) do + # @new_resource = mock("Chef::Resource::Service", + # :null_object => true, + # :name => "chef", + # :service_name => "chef", + # :running => false + # ) + # @new_resource.stub!(:start_command).and_return(false) + # + # @provider = Chef::Provider::Service::Arch.new(@node, @new_resource) + # Chef::Resource::Service.stub!(:new).and_return(@current_resource) + # end + + it "should add chef to DAEMONS array" do + ::File.stub!(:read).with("/etc/rc.conf").and_return("DAEMONS=(network)") + @provider.should_receive(:update_daemons).with(['network', 'chef']) + @provider.enable_service() + end + end + + describe Chef::Provider::Service::Arch, "disable_service" do + # before(:each) do + # @new_resource = mock("Chef::Resource::Service", + # :null_object => true, + # :name => "chef", + # :service_name => "chef", + # :running => false + # ) + # @new_resource.stub!(:start_command).and_return(false) + # + # @provider = Chef::Provider::Service::Arch.new(@node, @new_resource) + # Chef::Resource::Service.stub!(:new).and_return(@current_resource) + # end + + it "should remove chef from DAEMONS array" do + ::File.stub!(:read).with("/etc/rc.conf").and_return("DAEMONS=(network chef)") + @provider.should_receive(:update_daemons).with(['network', '!chef']) + @provider.disable_service() + end + end + + + describe Chef::Provider::Service::Arch, "start_service" do + # before(:each) do + # @new_resource = mock("Chef::Resource::Service", + # :null_object => true, + # :name => "chef", + # :service_name => "chef", + # :running => false + # ) + # @new_resource.stub!(:start_command).and_return(false) + # + # @provider = Chef::Provider::Service::Arch.new(@node, @new_resource) + # Chef::Resource::Service.stub!(:new).and_return(@current_resource) + # end + + it "should call the start command if one is specified" do + @new_resource.stub!(:start_command).and_return("/etc/rc.d/chef startyousillysally") + @provider.should_receive(:shell_out!).with("/etc/rc.d/chef startyousillysally") + @provider.start_service() + end + + it "should call '/etc/rc.d/service_name start' if no start command is specified" do + @provider.should_receive(:shell_out!).with("/etc/rc.d/#{@new_resource.service_name} start") + @provider.start_service() + end + end + + describe Chef::Provider::Service::Arch, "stop_service" do + # before(:each) do + # @new_resource = mock("Chef::Resource::Service", + # :null_object => true, + # :name => "chef", + # :service_name => "chef", + # :running => false + # ) + # @new_resource.stub!(:stop_command).and_return(false) + # + # @provider = Chef::Provider::Service::Arch.new(@node, @new_resource) + # Chef::Resource::Service.stub!(:new).and_return(@current_resource) + # end + + it "should call the stop command if one is specified" do + @new_resource.stub!(:stop_command).and_return("/etc/rc.d/chef itoldyoutostop") + @provider.should_receive(:shell_out!).with("/etc/rc.d/chef itoldyoutostop") + @provider.stop_service() + end + + it "should call '/etc/rc.d/service_name stop' if no stop command is specified" do + @provider.should_receive(:shell_out!).with("/etc/rc.d/#{@new_resource.service_name} stop") + @provider.stop_service() + end + end + + describe Chef::Provider::Service::Arch, "restart_service" do + # before(:each) do + # @new_resource = mock("Chef::Resource::Service", + # :null_object => true, + # :name => "chef", + # :service_name => "chef", + # :running => false + # ) + # @new_resource.stub!(:restart_command).and_return(false) + # @new_resource.stub!(:supports).and_return({:restart => false}) + # + # @provider = Chef::Provider::Service::Arch.new(@node, @new_resource) + # Chef::Resource::Service.stub!(:new).and_return(@current_resource) + # end + + it "should call 'restart' on the service_name if the resource supports it" do + @new_resource.stub!(:supports).and_return({:restart => true}) + @provider.should_receive(:shell_out!).with("/etc/rc.d/#{@new_resource.service_name} restart") + @provider.restart_service() + end + + it "should call the restart_command if one has been specified" do + @new_resource.stub!(:restart_command).and_return("/etc/rc.d/chef restartinafire") + @provider.should_receive(:shell_out!).with("/etc/rc.d/#{@new_resource.service_name} restartinafire") + @provider.restart_service() + end + + it "should just call stop, then start when the resource doesn't support restart and no restart_command is specified" do + @provider.should_receive(:stop_service) + @provider.should_receive(:sleep).with(1) + @provider.should_receive(:start_service) + @provider.restart_service() + end + end + + describe Chef::Provider::Service::Arch, "reload_service" do + # before(:each) do + # @new_resource = mock("Chef::Resource::Service", + # :null_object => true, + # :name => "chef", + # :service_name => "chef", + # :running => false + # ) + # @new_resource.stub!(:reload_command).and_return(false) + # @new_resource.stub!(:supports).and_return({:reload => false}) + # + # @provider = Chef::Provider::Service::Arch.new(@node, @new_resource) + # Chef::Resource::Service.stub!(:new).and_return(@current_resource) + # end + + it "should call 'reload' on the service if it supports it" do + @new_resource.stub!(:supports).and_return({:reload => true}) + @provider.should_receive(:shell_out!).with("/etc/rc.d/#{@new_resource.service_name} reload") + @provider.reload_service() + end + + it "should should run the user specified reload command if one is specified and the service doesn't support reload" do + @new_resource.stub!(:reload_command).and_return("/etc/rc.d/chef lollerpants") + @provider.should_receive(:shell_out!).with("/etc/rc.d/#{@new_resource.service_name} lollerpants") + @provider.reload_service() + end + end + end +end diff --git a/spec/unit/provider/service/debian_service_spec.rb b/spec/unit/provider/service/debian_service_spec.rb new file mode 100644 index 0000000000..bea9360561 --- /dev/null +++ b/spec/unit/provider/service/debian_service_spec.rb @@ -0,0 +1,254 @@ +# +# Author:: AJ Christensen (<aj@hjksolutions.com>) +# Copyright:: Copyright (c) 2008 HJK Solutions, LLC +# 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::Provider::Service::Debian, "load_current_resource" do + before(:each) do + @node = Chef::Node.new + @node.automatic_attrs[:command] = {:ps => 'fuuuu'} + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Service.new("chef") + + @current_resource = Chef::Resource::Service.new("chef") + + @provider = Chef::Provider::Service::Debian.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + + @pid, @stdin, @stdout, @stderr = nil, nil, nil, nil + + end + + it "ensures /usr/sbin/update-rc.d is available" do + File.should_receive(:exists?).with("/usr/sbin/update-rc.d").and_return(false) + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements } .should raise_error(Chef::Exceptions::Service) + end + + describe "when update-rc.d shows the init script linked to rc*.d/" do + before do + @provider.stub!(:assert_update_rcd_available) + + result=<<-UPDATE_RC_D_SUCCESS +Removing any system startup links for /etc/init.d/chef ... + /etc/rc0.d/K20chef + /etc/rc1.d/K20chef + /etc/rc2.d/S20chef + /etc/rc3.d/S20chef + /etc/rc4.d/S20chef + /etc/rc5.d/S20chef + /etc/rc6.d/K20chef + UPDATE_RC_D_SUCCESS + @stdout = StringIO.new(result) + @stderr = StringIO.new + @status = mock("Status", :exitstatus => 0, :stdout => @stdout) + @provider.stub!(:shell_out!).and_return(@status) + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + end + + it "says the service is enabled" do + @provider.service_currently_enabled?(@provider.get_priority).should be_true + end + + it "stores the 'enabled' state" do + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + @provider.load_current_resource.should equal(@current_resource) + @current_resource.enabled.should be_true + end + end + + {"Debian/Lenny and older" => { + "linked" => { + "stdout" => " Removing any system startup links for /etc/init.d/chef ... + /etc/rc0.d/K20chef + /etc/rc1.d/K20chef + /etc/rc2.d/S20chef + /etc/rc3.d/S20chef + /etc/rc4.d/S20chef + /etc/rc5.d/S20chef + /etc/rc6.d/K20chef", + "stderr" => "" + }, + "not linked" => { + "stdout" => " Removing any system startup links for /etc/init.d/chef ...", + "stderr" => "" + }, + }, + "Debian/Squeeze and earlier" => { + "linked" => { + "stdout" => "update-rc.d: using dependency based boot sequencing", + "stderr" => "insserv: remove service /etc/init.d/../rc0.d/K20chef-client +insserv: remove service /etc/init.d/../rc1.d/K20chef-client +insserv: remove service /etc/init.d/../rc2.d/S20chef-client +insserv: remove service /etc/init.d/../rc3.d/S20chef-client +insserv: remove service /etc/init.d/../rc4.d/S20chef-client +insserv: remove service /etc/init.d/../rc5.d/S20chef-client +insserv: remove service /etc/init.d/../rc6.d/K20chef-client +insserv: dryrun, not creating .depend.boot, .depend.start, and .depend.stop" + }, + "not linked" => { + "stdout" => "update-rc.d: using dependency based boot sequencing", + "stderr" => "" + } + } + }.each do |model, streams| + describe "when update-rc.d shows the init script linked to rc*.d/" do + before do + @provider.stub!(:assert_update_rcd_available) + + @stdout = StringIO.new(streams["linked"]["stdout"]) + @stderr = StringIO.new(streams["linked"]["stderr"]) + @status = mock("Status", :exitstatus => 0, :stdout => @stdout) + @provider.stub!(:shell_out!).and_return(@status) + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + end + + it "says the service is enabled" do + @provider.service_currently_enabled?(@provider.get_priority).should be_true + end + + it "stores the 'enabled' state" do + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + @provider.load_current_resource.should equal(@current_resource) + @current_resource.enabled.should be_true + end + + it "stores the start/stop priorities of the service" do + @provider.load_current_resource + expected_priorities = {"6"=>[:stop, "20"], + "0"=>[:stop, "20"], + "1"=>[:stop, "20"], + "2"=>[:start, "20"], + "3"=>[:start, "20"], + "4"=>[:start, "20"], + "5"=>[:start, "20"]} + @provider.current_resource.priority.should == expected_priorities + end + end + + describe "when using squeeze/earlier and update-rc.d shows the init script isn't linked to rc*.d" do + before do + @provider.stub!(:assert_update_rcd_available) + @stdout = StringIO.new(streams["not linked"]["stdout"]) + @stderr = StringIO.new(streams["not linked"]["stderr"]) + @status = mock("Status", :exitstatus => 0, :stdout => @stdout) + @provider.stub!(:shell_out!).and_return(@status) + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + end + + it "says the service is disabled" do + @provider.service_currently_enabled?(@provider.get_priority).should be_false + end + + it "stores the 'disabled' state" do + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + @provider.load_current_resource.should equal(@current_resource) + @current_resource.enabled.should be_false + end + end + end + + describe "when update-rc.d shows the init script isn't linked to rc*.d" do + before do + @provider.stub!(:assert_update_rcd_available) + @status = mock("Status", :exitstatus => 0) + @stdout = StringIO.new(" Removing any system startup links for /etc/init.d/chef ...") + @stderr = StringIO.new + @status = mock("Status", :exitstatus => 0, :stdout => @stdout) + @provider.stub!(:shell_out!).and_return(@status) + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + end + + it "says the service is disabled" do + @provider.service_currently_enabled?(@provider.get_priority).should be_false + end + + it "stores the 'disabled' state" do + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + @provider.load_current_resource.should equal(@current_resource) + @current_resource.enabled.should be_false + end + end + + describe "when update-rc.d fails" do + before do + @status = mock("Status", :exitstatus => -1) + @provider.stub!(:popen4).and_return(@status) + end + + it "raises an error" do + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + end + + describe "when enabling a service without priority" do + it "should call update-rc.d 'service_name' defaults" do + @provider.should_receive(:run_command).with({:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove"}) + @provider.should_receive(:run_command).with({:command => "/usr/sbin/update-rc.d #{@new_resource.service_name} defaults"}) + @provider.enable_service() + end + end + + describe "when enabling a service with simple priority" do + before do + @new_resource.priority(75) + end + + it "should call update-rc.d 'service_name' defaults" do + @provider.should_receive(:run_command).with({:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove"}) + @provider.should_receive(:run_command).with({:command => "/usr/sbin/update-rc.d #{@new_resource.service_name} defaults 75 25"}) + @provider.enable_service() + end + end + + describe "when enabling a service with complex priorities" do + before do + @new_resource.priority(2 => [:start, 20], 3 => [:stop, 55]) + end + + it "should call update-rc.d 'service_name' defaults" do + @provider.should_receive(:run_command).with({:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove"}) + @provider.should_receive(:run_command).with({:command => "/usr/sbin/update-rc.d #{@new_resource.service_name} start 20 2 . stop 55 3 . "}) + @provider.enable_service() + end + end + + describe "when disabling a service without a priority" do + + it "should call update-rc.d -f 'service_name' remove + stop with a default priority" do + @provider.should_receive(:run_command).with({:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove"}) + @provider.should_receive(:run_command).with({:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} stop 80 2 3 4 5 ."}) + @provider.disable_service() + end + end + + describe "when disabling a service with simple priority" do + before do + @new_resource.priority(75) + end + + it "should call update-rc.d -f 'service_name' remove + stop with a specified priority" do + @provider.should_receive(:run_command).with({:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove"}) + @provider.should_receive(:run_command).with({:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} stop #{100 - @new_resource.priority} 2 3 4 5 ."}) + @provider.disable_service() + end + end +end diff --git a/spec/unit/provider/service/freebsd_service_spec.rb b/spec/unit/provider/service/freebsd_service_spec.rb new file mode 100644 index 0000000000..6dd06bde2c --- /dev/null +++ b/spec/unit/provider/service/freebsd_service_spec.rb @@ -0,0 +1,379 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org) +# Copyright:: Copyright (c) 2009 Bryan McLellan +# 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::Provider::Service::Freebsd do + before do + @node = Chef::Node.new + @node.automatic_attrs[:command] = {:ps => "ps -ax"} + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Service.new("apache22") + @new_resource.pattern("httpd") + @new_resource.supports({:status => false}) + + @current_resource = Chef::Resource::Service.new("apache22") + + @provider = Chef::Provider::Service::Freebsd.new(@new_resource,@run_context) + @provider.action = :start + @init_command = "/usr/local/etc/rc.d/apache22" + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + end + + describe "load_current_resource" do + before(:each) do + @stdout = StringIO.new(<<-PS_SAMPLE) +413 ?? Ss 0:02.51 /usr/sbin/syslogd -s +539 ?? Is 0:00.14 /usr/sbin/sshd +545 ?? Ss 0:17.53 sendmail: accepting connections (sendmail) +PS_SAMPLE + @status = mock(:stdout => @stdout, :exitstatus => 0) + @provider.stub!(:shell_out!).with(@node[:command][:ps]).and_return(@status) + + ::File.stub!(:exists?).and_return(false) + ::File.stub!(:exists?).with("/usr/local/etc/rc.d/#{@new_resource.service_name}").and_return(true) + @lines = mock("lines") + @lines.stub!(:each).and_yield("sshd_enable=\"YES\""). + and_yield("#{@new_resource.name}_enable=\"YES\"") + ::File.stub!(:open).and_return(@lines) + + @rc_with_name = StringIO.new(<<-RC_SAMPLE) +name="apache22" +rcvar=`set_rcvar` +RC_SAMPLE + ::File.stub!(:open).with("/usr/local/etc/rc.d/#{@new_resource.service_name}").and_return(@rc_with_name) + @provider.stub(:service_enable_variable_name).and_return nil + + end + + it "should create a current resource with the name of the new resource" do + Chef::Resource::Service.should_receive(:new).and_return(@current_resource) + @provider.load_current_resource + end + + it "should set the current resources service name to the new resources service name" do + @provider.load_current_resource + @current_resource.service_name.should == @new_resource.service_name + end + + it "should not raise an exception if the rcscript have a name variable" do + @provider.load_current_resource + lambda { @provider.service_enable_variable_name }.should_not raise_error(Chef::Exceptions::Service) + end + + describe "when the service supports status" do + before do + @new_resource.supports({:status => true}) + end + + it "should run '/etc/init.d/service_name status'" do + @provider.should_receive(:shell_out).with("/usr/local/etc/rc.d/#{@current_resource.service_name} status").and_return(@status) + @provider.load_current_resource + end + + it "should set running to true if the the status command returns 0" do + @provider.should_receive(:shell_out).with("/usr/local/etc/rc.d/#{@current_resource.service_name} status").and_return(@status) + @current_resource.should_receive(:running).with(true) + @provider.load_current_resource + end + + it "should set running to false if the status command returns anything except 0" do + @provider.should_receive(:shell_out).with("/usr/local/etc/rc.d/#{@current_resource.service_name} status").and_raise(Mixlib::ShellOut::ShellCommandFailed) + @current_resource.should_receive(:running).with(false) + @provider.load_current_resource + # @provider.current_resource.running.should be_false + end + end + + describe "when a status command has been specified" do + before do + @new_resource.status_command("/bin/chefhasmonkeypants status") + end + + it "should run the services status command if one has been specified" do + @provider.should_receive(:shell_out).with("/bin/chefhasmonkeypants status").and_return(@status) + @provider.load_current_resource + end + + end + + it "should raise error if the node has a nil ps attribute and no other means to get status" do + @node.automatic_attrs[:command] = {:ps => nil} + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + + it "should raise error if the node has an empty ps attribute and no other means to get status" do + @node.automatic_attrs[:command] = {:ps => ""} + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + + describe "when executing assertions" do + it "should verify that /etc/rc.conf exists" do + ::File.should_receive(:exists?).with("/etc/rc.conf") + @provider.stub!(:service_enable_variable_name).and_return("#{@current_resource.service_name}_enable") + @provider.load_current_resource + end + + context "and the init script is not found" do + [ "start", "reload", "restart", "enable" ].each do |action| + it "should raise an exception when the action is #{action}" do + ::File.stub!(:exists?).and_return(false) + @provider.load_current_resource + @provider.define_resource_requirements + @provider.instance_variable_get("@rcd_script_found").should be_false + @provider.action = action + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + end + + [ "stop", "disable" ].each do |action| + it "should not raise an error when the action is #{action}" do + @provider.action = action + lambda { @provider.process_resource_requirements }.should_not raise_error + end + end + end + + it "update state when current resource enabled state could not be determined" do + ::File.should_receive(:exists?).with("/etc/rc.conf").and_return false + @provider.load_current_resource + @provider.instance_variable_get("@enabled_state_found").should be_false + end + + it "update state when current resource enabled state could be determined" do + ::File.stub!(:exist?).with("/usr/local/etc/rc.d/#{@new_resource.service_name}").and_return(true) + ::File.should_receive(:exists?).with("/etc/rc.conf").and_return true + @provider.load_current_resource + @provider.instance_variable_get("@enabled_state_found").should be_false + @provider.instance_variable_get("@rcd_script_found").should be_true + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service, + "Could not find the service name in /usr/local/etc/rc.d/#{@current_resource.service_name} and rcvar") + end + + it "should throw an exception if service line is missing from rc.d script" do + pending "not implemented" do + false.should be_true + end + end + + end + + describe "when we have a 'ps' attribute" do + before do + @node.automatic_attrs[:command] = {:ps => "ps -ax"} + end + + it "should shell_out! the node's ps command" do + @provider.should_receive(:shell_out!).with(@node[:command][:ps]).and_return(@status) + @provider.load_current_resource + end + + it "should read stdout of the ps command" do + @provider.stub!(:shell_out!).and_return(@status) + @stdout.should_receive(:each_line).and_return(true) + @provider.load_current_resource + end + + it "should set running to true if the regex matches the output" do + @stdout.stub!(:each_line).and_yield("555 ?? Ss 0:05.16 /usr/sbin/cron -s"). + and_yield(" 9881 ?? Ss 0:06.67 /usr/local/sbin/httpd -DNOHTTPACCEPT") + @provider.load_current_resource + @current_resource.running.should be_true + end + + it "should set running to false if the regex doesn't match" do + @provider.stub!(:shell_out!).and_return(@status) + @provider.load_current_resource + @current_resource.running.should be_false + end + + it "should raise an exception if ps fails" do + @provider.stub!(:shell_out!).and_raise(Mixlib::ShellOut::ShellCommandFailed) + @provider.load_current_resource + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + end + + it "should return the current resource" do + @provider.load_current_resource.should eql(@current_resource) + end + + describe "when starting the service" do + it "should call the start command if one is specified" do + @new_resource.start_command("/etc/rc.d/chef startyousillysally") + @provider.should_receive(:shell_out!).with("/etc/rc.d/chef startyousillysally") + @provider.load_current_resource + @provider.start_service() + end + + it "should call '/usr/local/etc/rc.d/service_name faststart' if no start command is specified" do + @provider.should_receive(:shell_out!).with("/usr/local/etc/rc.d/#{@new_resource.service_name} faststart") + @provider.load_current_resource + @provider.start_service() + end + end + + describe Chef::Provider::Service::Init, "stop_service" do + it "should call the stop command if one is specified" do + @new_resource.stop_command("/etc/init.d/chef itoldyoutostop") + @provider.should_receive(:shell_out!).with("/etc/init.d/chef itoldyoutostop") + @provider.load_current_resource + @provider.stop_service() + end + + it "should call '/usr/local/etc/rc.d/service_name faststop' if no stop command is specified" do + @provider.should_receive(:shell_out!).with("/usr/local/etc/rc.d/#{@new_resource.service_name} faststop") + @provider.load_current_resource + @provider.stop_service() + end + end + + describe "when restarting a service" do + it "should call 'restart' on the service_name if the resource supports it" do + @new_resource.supports({:restart => true}) + @provider.should_receive(:shell_out!).with("/usr/local/etc/rc.d/#{@new_resource.service_name} fastrestart") + @provider.load_current_resource + @provider.restart_service() + end + + it "should call the restart_command if one has been specified" do + @new_resource.restart_command("/etc/init.d/chef restartinafire") + @provider.should_receive(:shell_out!).with("/etc/init.d/chef restartinafire") + @provider.load_current_resource + @provider.restart_service() + end + end + + describe "when the rcscript does not have a name variable" do + before do + @rc_without_name = StringIO.new(<<-RC_SAMPLE) +rcvar=`set_rcvar` +RC_SAMPLE + ::File.stub!(:open).with("/usr/local/etc/rc.d/#{@current_resource.service_name}").and_return(@rc_with_noname) + @provider.current_resource = @current_resource + end + + describe "when rcvar returns foobar_enable" do + before do + @rcvar_stdout = <<RCVAR_SAMPLE +# apache22 +# +# #{@current_resource.service_name}_enable="YES" +# (default: "") +RCVAR_SAMPLE + @status = mock(:stdout => @rcvar_stdout, :exitstatus => 0) + @provider.stub!(:shell_out!).with("/usr/local/etc/rc.d/#{@current_resource.service_name} rcvar").and_return(@status) + end + + it "should get the service name from rcvar if the rcscript does not have a name variable" do + @provider.load_current_resource + @provider.unstub!(:service_enable_variable_name) + @provider.service_enable_variable_name.should == "#{@current_resource.service_name}_enable" + end + + it "should not raise an exception if the rcscript does not have a name variable" do + @provider.load_current_resource + lambda { @provider.service_enable_variable_name }.should_not raise_error(Chef::Exceptions::Service) + end + end + + describe "when rcvar does not return foobar_enable" do + before do + @rcvar_stdout = <<RCVAR_SAMPLE +# service_with_noname +# +RCVAR_SAMPLE + @status = mock(:stdout => @rcvar_stdout, :exitstatus => 0) + @provider.stub!(:shell_out!).with("/usr/local/etc/rc.d/#{@current_resource.service_name} rcvar").and_return(@status) + end + + [ "start", "reload", "restart", "enable" ].each do |action| + it "should raise an exception when the action is #{action}" do + @provider.action = action + @provider.load_current_resource + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + end + + [ "stop", "disable" ].each do |action| + it "should not raise an error when the action is #{action}" do + ::File.stub!(:exist?).with("/usr/local/etc/rc.d/#{@new_resource.service_name}").and_return(true) + @provider.action = action + @provider.load_current_resource + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should_not raise_error(Chef::Exceptions::Service) + end + end + end + end + end + + describe Chef::Provider::Service::Freebsd, "enable_service" do + before do + @provider.current_resource = @current_resource + @provider.stub!(:service_enable_variable_name).and_return("#{@current_resource.service_name}_enable") + end + + it "should enable the service if it is not enabled" do + @current_resource.stub!(:enabled).and_return(false) + @provider.should_receive(:read_rc_conf).and_return([ "foo", "#{@current_resource.service_name}_enable=\"NO\"", "bar" ]) + @provider.should_receive(:write_rc_conf).with(["foo", "bar", "#{@current_resource.service_name}_enable=\"YES\""]) + @provider.enable_service() + end + + it "should enable the service if it is not enabled and not already specified in the rc.conf file" do + @current_resource.stub!(:enabled).and_return(false) + @provider.should_receive(:read_rc_conf).and_return([ "foo", "bar" ]) + @provider.should_receive(:write_rc_conf).with(["foo", "bar", "#{@current_resource.service_name}_enable=\"YES\""]) + @provider.enable_service() + end + + it "should not enable the service if it is already enabled" do + @current_resource.stub!(:enabled).and_return(true) + @provider.should_not_receive(:write_rc_conf) + @provider.enable_service + end + end + + describe Chef::Provider::Service::Freebsd, "disable_service" do + before do + @provider.current_resource = @current_resource + @provider.stub!(:service_enable_variable_name).and_return("#{@current_resource.service_name}_enable") + end + + it "should should disable the service if it is not disabled" do + @current_resource.stub!(:enabled).and_return(true) + @provider.should_receive(:read_rc_conf).and_return([ "foo", "#{@current_resource.service_name}_enable=\"YES\"", "bar" ]) + @provider.should_receive(:write_rc_conf).with(["foo", "bar", "#{@current_resource.service_name}_enable=\"NO\""]) + @provider.disable_service() + end + + it "should not disable the service if it is already disabled" do + @current_resource.stub!(:enabled).and_return(false) + @provider.should_not_receive(:write_rc_conf) + @provider.disable_service() + end + end +end diff --git a/spec/unit/provider/service/gentoo_service_spec.rb b/spec/unit/provider/service/gentoo_service_spec.rb new file mode 100644 index 0000000000..8d4ada043b --- /dev/null +++ b/spec/unit/provider/service/gentoo_service_spec.rb @@ -0,0 +1,144 @@ +# +# Author:: Lee Jensen (<ljensen@engineyard.com>) +# Author:: AJ Christensen (<aj@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' + +describe Chef::Provider::Service::Gentoo do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Service.new("chef") + @current_resource = Chef::Resource::Service.new("chef") + + @provider = Chef::Provider::Service::Gentoo.new(@new_resource, @run_context) + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + @status = mock("Status", :exitstatus => 0, :stdout => @stdout) + @provider.stub!(:shell_out).and_return(@status) + File.stub!(:exists?).with("/etc/init.d/chef").and_return(true) + File.stub!(:exists?).with("/sbin/rc-update").and_return(true) + File.stub!(:exists?).with("/etc/runlevels/default/chef").and_return(false) + File.stub!(:readable?).with("/etc/runlevels/default/chef").and_return(false) + end + # new test: found_enabled state + # + describe "load_current_resource" do + it "should raise Chef::Exceptions::Service if /sbin/rc-update does not exist" do + File.should_receive(:exists?).with("/sbin/rc-update").and_return(false) + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + + it "should track when service file is not found in /etc/runlevels" do + @provider.load_current_resource + @provider.instance_variable_get("@found_script").should be_false + end + + it "should track when service file is found in /etc/runlevels/**/" do + Dir.stub!(:glob).with("/etc/runlevels/**/chef").and_return(["/etc/runlevels/default/chef"]) + @provider.load_current_resource + @provider.instance_variable_get("@found_script").should be_true + end + + describe "when detecting the service enable state" do + describe "and the glob returns a default service script file" do + before do + Dir.stub!(:glob).with("/etc/runlevels/**/chef").and_return(["/etc/runlevels/default/chef"]) + end + + describe "and the file exists and is readable" do + before do + File.stub!(:exists?).with("/etc/runlevels/default/chef").and_return(true) + File.stub!(:readable?).with("/etc/runlevels/default/chef").and_return(true) + end + it "should set enabled to true" do + @provider.load_current_resource + @current_resource.enabled.should be_true + end + end + + describe "and the file exists but is not readable" do + before do + File.stub!(:exists?).with("/etc/runlevels/default/chef").and_return(true) + File.stub!(:readable?).with("/etc/runlevels/default/chef").and_return(false) + end + + it "should set enabled to false" do + @provider.load_current_resource + @current_resource.enabled.should be_false + end + end + + describe "and the file does not exist" do + before do + File.stub!(:exists?).with("/etc/runlevels/default/chef").and_return(false) + File.stub!(:readable?).with("/etc/runlevels/default/chef").and_return("foobarbaz") + end + + it "should set enabled to false" do + @provider.load_current_resource + @current_resource.enabled.should be_false + end + + end + end + + end + + it "should return the current_resource" do + @provider.load_current_resource.should == @current_resource + end + + it "should support the status command automatically" do + @provider.load_current_resource + @new_resource.supports[:status].should be_true + end + + it "should support the restart command automatically" do + @provider.load_current_resource + @new_resource.supports[:restart].should be_true + end + + it "should not support the reload command automatically" do + @provider.load_current_resource + @new_resource.supports[:reload].should_not be_true + end + + end + + describe "action_methods" do + before(:each) { @provider.stub!(:load_current_resource).and_return(@current_resource) } + + describe Chef::Provider::Service::Gentoo, "enable_service" do + it "should call rc-update add *service* default" do + @provider.should_receive(:run_command).with({:command => "/sbin/rc-update add chef default"}) + @provider.enable_service() + end + end + + describe Chef::Provider::Service::Gentoo, "disable_service" do + it "should call rc-update del *service* default" do + @provider.should_receive(:run_command).with({:command => "/sbin/rc-update del chef default"}) + @provider.disable_service() + end + end + end + +end diff --git a/spec/unit/provider/service/init_service_spec.rb b/spec/unit/provider/service/init_service_spec.rb new file mode 100644 index 0000000000..77b22c8cf4 --- /dev/null +++ b/spec/unit/provider/service/init_service_spec.rb @@ -0,0 +1,212 @@ +# +# Author:: AJ Christensen (<aj@hjksolutions.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' + +describe Chef::Provider::Service::Init, "load_current_resource" do + before(:each) do + @node = Chef::Node.new + @node.automatic_attrs[:command] = {:ps => "ps -ef"} + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Service.new("chef") + + @current_resource = Chef::Resource::Service.new("chef") + + @provider = Chef::Provider::Service::Init.new(@new_resource, @run_context) + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + + @stdout = StringIO.new(<<-PS) +aj 7842 5057 0 21:26 pts/2 00:00:06 vi init.rb +aj 7903 5016 0 21:26 pts/5 00:00:00 /bin/bash +aj 8119 6041 0 21:34 pts/3 00:00:03 vi init_service_spec.rb +PS + @status = mock("Status", :exitstatus => 0, :stdout => @stdout) + @provider.stub!(:shell_out!).and_return(@status) + end + + it "should create a current resource with the name of the new resource" do + @provider.load_current_resource + @provider.current_resource.should equal(@current_resource) + end + + it "should set the current resources service name to the new resources service name" do + @provider.load_current_resource + @current_resource.service_name.should == 'chef' + end + + describe "when the service supports status" do + before do + @new_resource.supports({:status => true}) + end + + it "should run '/etc/init.d/service_name status'" do + @provider.should_receive(:shell_out).with("/etc/init.d/#{@current_resource.service_name} status").and_return(@status) + @provider.load_current_resource + end + + it "should set running to true if the the status command returns 0" do + @provider.stub!(:shell_out).with("/etc/init.d/#{@current_resource.service_name} status").and_return(@status) + @provider.load_current_resource + @current_resource.running.should be_true + end + + it "should set running to false if the status command returns anything except 0" do + @status.stub!(:exitstatus).and_return(1) + @provider.stub!(:shell_out).with("/etc/init.d/#{@current_resource.service_name} status").and_return(@status) + @provider.load_current_resource + @current_resource.running.should be_false + end + + it "should set running to false if the status command raises" do + @provider.stub!(:shell_out).and_raise(Mixlib::ShellOut::ShellCommandFailed) + @provider.load_current_resource + @current_resource.running.should be_false + end + end + + describe "when a status command has been specified" do + before do + @new_resource.stub!(:status_command).and_return("/etc/init.d/chefhasmonkeypants status") + end + + it "should run the services status command if one has been specified" do + @provider.should_receive(:shell_out).with("/etc/init.d/chefhasmonkeypants status").and_return(@status) + @provider.load_current_resource + end + + end + + describe "when the node has not specified a ps command" do + + it "should raise an error if the node has a nil ps attribute" do + @node.automatic_attrs[:command] = {:ps => nil} + @provider.load_current_resource + @provider.action = :start + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + + it "should raise an error if the node has an empty ps attribute" do + @node.automatic_attrs[:command] = {:ps => ""} + @provider.load_current_resource + @provider.action = :start + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + + end + + + describe "when we have a 'ps' attribute" do + it "should shell_out! the node's ps command" do + @provider.should_receive(:shell_out!).and_return(@status) + @provider.load_current_resource + end + + it "should set running to true if the regex matches the output" do + @stdout = StringIO.new(<<-RUNNING_PS) +aj 7842 5057 0 21:26 pts/2 00:00:06 chef +aj 7842 5057 0 21:26 pts/2 00:00:06 poos +RUNNING_PS + @status.stub!(:stdout).and_return(@stdout) + @provider.load_current_resource + @current_resource.running.should be_true + end + + it "should set running to false if the regex doesn't match" do + @provider.stub!(:shell_out!).and_return(@status) + @provider.load_current_resource + @current_resource.running.should be_false + end + + it "should raise an exception if ps fails" do + @provider.stub!(:shell_out!).and_raise(Mixlib::ShellOut::ShellCommandFailed) + @provider.load_current_resource + @provider.action = :start + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + end + + it "should return the current resource" do + @provider.load_current_resource.should eql(@current_resource) + end + + describe "when starting the service" do + it "should call the start command if one is specified" do + @new_resource.start_command("/etc/init.d/chef startyousillysally") + @provider.should_receive(:shell_out!).with("/etc/init.d/chef startyousillysally") + @provider.start_service() + end + + it "should call '/etc/init.d/service_name start' if no start command is specified" do + @provider.should_receive(:shell_out!).with("/etc/init.d/#{@new_resource.service_name} start") + @provider.start_service() + end + end + + describe Chef::Provider::Service::Init, "stop_service" do + it "should call the stop command if one is specified" do + @new_resource.stop_command("/etc/init.d/chef itoldyoutostop") + @provider.should_receive(:shell_out!).with("/etc/init.d/chef itoldyoutostop") + @provider.stop_service() + end + + it "should call '/etc/init.d/service_name stop' if no stop command is specified" do + @provider.should_receive(:shell_out!).with("/etc/init.d/#{@new_resource.service_name} stop") + @provider.stop_service() + end + end + + describe "when restarting a service" do + it "should call 'restart' on the service_name if the resource supports it" do + @new_resource.supports({:restart => true}) + @provider.should_receive(:shell_out!).with("/etc/init.d/#{@new_resource.service_name} restart") + @provider.restart_service() + end + + it "should call the restart_command if one has been specified" do + @new_resource.restart_command("/etc/init.d/chef restartinafire") + @provider.should_receive(:shell_out!).with("/etc/init.d/#{@new_resource.service_name} restartinafire") + @provider.restart_service() + end + + it "should just call stop, then start when the resource doesn't support restart and no restart_command is specified" do + @provider.should_receive(:stop_service) + @provider.should_receive(:sleep).with(1) + @provider.should_receive(:start_service) + @provider.restart_service() + end + end + + describe "when reloading a service" do + it "should call 'reload' on the service if it supports it" do + @new_resource.supports({:reload => true}) + @provider.should_receive(:shell_out!).with("/etc/init.d/chef reload") + @provider.reload_service() + end + + it "should should run the user specified reload command if one is specified and the service doesn't support reload" do + @new_resource.reload_command("/etc/init.d/chef lollerpants") + @provider.should_receive(:shell_out!).with("/etc/init.d/chef lollerpants") + @provider.reload_service() + end + end +end diff --git a/spec/unit/provider/service/insserv_service_spec.rb b/spec/unit/provider/service/insserv_service_spec.rb new file mode 100644 index 0000000000..c823d511b5 --- /dev/null +++ b/spec/unit/provider/service/insserv_service_spec.rb @@ -0,0 +1,76 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2011 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::Provider::Service::Insserv do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @node.automatic_attrs[:command] = {:ps => "ps -ax"} + + @new_resource = Chef::Resource::Service.new("initgrediant") + @current_resource = Chef::Resource::Service.new("initgrediant") + + @provider = Chef::Provider::Service::Insserv.new(@new_resource, @run_context) + @status = mock("Process::Status mock", :exitstatus => 0, :stdout => "") + @provider.stub!(:shell_out!).and_return(@status) + end + + describe "load_current_resource" do + describe "when startup links exist" do + before do + Dir.stub!(:glob).with("/etc/rc**/S*initgrediant").and_return(["/etc/rc5.d/S18initgrediant", "/etc/rc2.d/S18initgrediant", "/etc/rc4.d/S18initgrediant", "/etc/rc3.d/S18initgrediant"]) + end + + it "sets the current enabled status to true" do + @provider.load_current_resource + @provider.current_resource.enabled.should be_true + end + end + + describe "when startup links do not exist" do + before do + Dir.stub!(:glob).with("/etc/rc**/S*initgrediant").and_return([]) + end + + it "sets the current enabled status to false" do + @provider.load_current_resource + @provider.current_resource.enabled.should be_false + end + end + + end + + describe "enable_service" do + it "should call insserv and create the default links" do + @provider.should_receive(:run_command).with({:command=>"/sbin/insserv -r -f #{@new_resource.service_name}"}) + @provider.should_receive(:run_command).with({:command=>"/sbin/insserv -d -f #{@new_resource.service_name}"}) + @provider.enable_service + end + end + + describe "disable_service" do + it "should call insserv and remove the links" do + @provider.should_receive(:run_command).with({:command=>"/sbin/insserv -r -f #{@new_resource.service_name}"}) + @provider.disable_service + end + end +end + diff --git a/spec/unit/provider/service/invokercd_service_spec.rb b/spec/unit/provider/service/invokercd_service_spec.rb new file mode 100644 index 0000000000..ace2ad24e3 --- /dev/null +++ b/spec/unit/provider/service/invokercd_service_spec.rb @@ -0,0 +1,212 @@ +# +# Author:: AJ Christensen (<aj@hjksolutions.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' + +describe Chef::Provider::Service::Invokercd, "load_current_resource" do + before(:each) do + @node = Chef::Node.new + @node.automatic_attrs[:command] = {:ps => "ps -ef"} + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Service.new("chef") + + @current_resource = Chef::Resource::Service.new("chef") + + @provider = Chef::Provider::Service::Invokercd.new(@new_resource, @run_context) + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + + @stdout = StringIO.new(<<-PS) +aj 7842 5057 0 21:26 pts/2 00:00:06 vi init.rb +aj 7903 5016 0 21:26 pts/5 00:00:00 /bin/bash +aj 8119 6041 0 21:34 pts/3 00:00:03 vi init_service_spec.rb +PS + @status = mock("Status", :exitstatus => 0, :stdout => @stdout) + @provider.stub!(:shell_out!).and_return(@status) + end + + it "should create a current resource with the name of the new resource" do + @provider.load_current_resource + @provider.current_resource.should equal(@current_resource) + end + + it "should set the current resources service name to the new resources service name" do + @provider.load_current_resource + @current_resource.service_name.should == 'chef' + end + + describe "when the service supports status" do + before do + @new_resource.supports({:status => true}) + end + + it "should run '/usr/sbin/invoke-rc.d service_name status'" do + @provider.should_receive(:shell_out).with("/usr/sbin/invoke-rc.d #{@current_resource.service_name} status").and_return(@status) + @provider.load_current_resource + end + + it "should set running to true if the the status command returns 0" do + @provider.stub!(:shell_out).with("/usr/sbin/invoke-rc.d #{@current_resource.service_name} status").and_return(@status) + @provider.load_current_resource + @current_resource.running.should be_true + end + + it "should set running to false if the status command returns anything except 0" do + @status.stub!(:exitstatus).and_return(1) + @provider.stub!(:shell_out).with("/usr/sbin/invoke-rc.d #{@current_resource.service_name} status").and_return(@status) + @provider.load_current_resource + @current_resource.running.should be_false + end + + it "should set running to false if the status command raises" do + @provider.stub!(:shell_out).with("/usr/sbin/invoke-rc.d #{@current_resource.service_name} status").and_raise(Mixlib::ShellOut::ShellCommandFailed) + @provider.load_current_resource + @current_resource.running.should be_false + end + end + + describe "when a status command has been specified" do + before do + @new_resource.stub!(:status_command).and_return("/usr/sbin/invoke-rc.d chefhasmonkeypants status") + end + + it "should run the services status command if one has been specified" do + @provider.should_receive(:shell_out).with("/usr/sbin/invoke-rc.d chefhasmonkeypants status").and_return(@status) + @provider.load_current_resource + end + + end + + describe "when the node has not specified a ps command" do + it "should raise error if the node has a nil ps attribute and no other means to get status" do + @node.automatic_attrs[:command] = {:ps => nil} + @provider.action = :start + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + + it "should raise error if the node has an empty ps attribute and no other means to get status" do + @node.automatic_attrs[:command] = {:ps => ""} + @provider.action = :start + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + + end + + + describe "when we have a 'ps' attribute" do + it "should shell_out! the node's ps command" do + @status = mock("Status", :exitstatus => 0, :stdout => @stdout) + @provider.should_receive(:shell_out!).with(@node[:command][:ps]).and_return(@status) + @provider.load_current_resource + end + + it "should set running to true if the regex matches the output" do + @stdout = StringIO.new(<<-RUNNING_PS) +aj 7842 5057 0 21:26 pts/2 00:00:06 chef +aj 7842 5057 0 21:26 pts/2 00:00:06 poos +RUNNING_PS + @status = mock("Status", :exitstatus => 0, :stdout => @stdout) + @provider.should_receive(:shell_out!).and_return(@status) + @provider.load_current_resource + @current_resource.running.should be_true + end + + it "should set running to false if the regex doesn't match" do + @status = mock("Status", :exitstatus => 0, :stdout => @stdout) + @provider.should_receive(:shell_out!).and_return(@status) + @provider.load_current_resource + @current_resource.running.should be_false + end + + it "should raise an exception if ps fails" do + @provider.stub!(:shell_out!).and_raise(Mixlib::ShellOut::ShellCommandFailed) + @provider.action = :start + @provider.load_current_resource + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + end + + it "should return the current resource" do + @provider.load_current_resource.should eql(@current_resource) + end + + describe "when starting the service" do + it "should call the start command if one is specified" do + @new_resource.start_command("/usr/sbin/invoke-rc.d chef startyousillysally") + @provider.should_receive(:shell_out!).with("/usr/sbin/invoke-rc.d chef startyousillysally") + @provider.start_service() + end + + it "should call '/usr/sbin/invoke-rc.d service_name start' if no start command is specified" do + @provider.should_receive(:shell_out!).with("/usr/sbin/invoke-rc.d #{@new_resource.service_name} start") + @provider.start_service() + end + end + + describe Chef::Provider::Service::Invokercd, "stop_service" do + it "should call the stop command if one is specified" do + @new_resource.stop_command("/usr/sbin/invoke-rc.d chef itoldyoutostop") + @provider.should_receive(:shell_out!).with("/usr/sbin/invoke-rc.d chef itoldyoutostop") + @provider.stop_service() + end + + it "should call '/usr/sbin/invoke-rc.d service_name stop' if no stop command is specified" do + @provider.should_receive(:shell_out!).with("/usr/sbin/invoke-rc.d #{@new_resource.service_name} stop") + @provider.stop_service() + end + end + + describe "when restarting a service" do + it "should call 'restart' on the service_name if the resource supports it" do + @new_resource.supports({:restart => true}) + @provider.should_receive(:shell_out!).with("/usr/sbin/invoke-rc.d #{@new_resource.service_name} restart") + @provider.restart_service() + end + + it "should call the restart_command if one has been specified" do + @new_resource.restart_command("/usr/sbin/invoke-rc.d chef restartinafire") + @provider.should_receive(:shell_out!).with("/usr/sbin/invoke-rc.d #{@new_resource.service_name} restartinafire") + @provider.restart_service() + end + + it "should just call stop, then start when the resource doesn't support restart and no restart_command is specified" do + @provider.should_receive(:stop_service) + @provider.should_receive(:sleep).with(1) + @provider.should_receive(:start_service) + @provider.restart_service() + end + end + + describe "when reloading a service" do + it "should call 'reload' on the service if it supports it" do + @new_resource.supports({:reload => true}) + @provider.should_receive(:shell_out!).with("/usr/sbin/invoke-rc.d chef reload") + @provider.reload_service() + end + + it "should should run the user specified reload command if one is specified and the service doesn't support reload" do + @new_resource.reload_command("/usr/sbin/invoke-rc.d chef lollerpants") + @provider.should_receive(:shell_out!).with("/usr/sbin/invoke-rc.d chef lollerpants") + @provider.reload_service() + end + end +end diff --git a/spec/unit/provider/service/macosx_spec.rb b/spec/unit/provider/service/macosx_spec.rb new file mode 100644 index 0000000000..9c3ec340a2 --- /dev/null +++ b/spec/unit/provider/service/macosx_spec.rb @@ -0,0 +1,229 @@ +# +# Author:: Igor Afonov <afonov@gmail.com> +# Copyright:: Copyright (c) 2011 Igor Afonov +# 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::Provider::Service::Macosx do + let(:node) { Chef::Node.new } + let(:events) {Chef::EventDispatch::Dispatcher.new} + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:provider) { described_class.new(new_resource, run_context) } + let(:stdout) { StringIO.new } + + before do + Dir.stub!(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist"], []) + provider.stub!(:shell_out!). + with("launchctl list", {:group => 1001, :user => 101}). + and_return(mock("ouput", :stdout => stdout)) + + File.stub!(:stat).and_return(mock("stat", :gid => 1001, :uid => 101)) + end + + ["redis-server", "io.redis.redis-server"].each do |service_name| + context "when service name is given as #{service_name}" do + let(:new_resource) { Chef::Resource::Service.new(service_name) } + let!(:current_resource) { Chef::Resource::Service.new(service_name) } + + describe "#load_current_resource" do + context "when launchctl returns pid in service list" do + let(:stdout) { StringIO.new <<-SVC_LIST } +12761 - 0x100114220.old.machinit.thing +7777 - io.redis.redis-server +- - com.lol.stopped-thing +SVC_LIST + + before do + provider.load_current_resource + end + + it "sets resource running state to true" do + provider.current_resource.running.should be_true + end + + it "sets resouce enabled state to true" do + provider.current_resource.enabled.should be_true + end + end + + describe "running unsupported actions" do + before do + Dir.stub!(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist"], []) + end + it "should throw an exception when enable action is attempted" do + lambda {provider.run_action(:enable)}.should raise_error(Chef::Exceptions::UnsupportedAction) + end + it "should throw an exception when reload action is attempted" do + lambda {provider.run_action(:reload)}.should raise_error(Chef::Exceptions::UnsupportedAction) + end + it "should throw an exception when disable action is attempted" do + lambda {provider.run_action(:disable)}.should raise_error(Chef::Exceptions::UnsupportedAction) + end + end + context "when launchctl returns empty service pid" do + let(:stdout) { StringIO.new <<-SVC_LIST } +12761 - 0x100114220.old.machinit.thing +- - io.redis.redis-server +- - com.lol.stopped-thing +SVC_LIST + + before do + provider.load_current_resource + end + + it "sets resource running state to false" do + provider.current_resource.running.should be_false + end + + it "sets resouce enabled state to true" do + provider.current_resource.enabled.should be_true + end + end + + context "when launchctl doesn't return service entry at all" do + let(:stdout) { StringIO.new <<-SVC_LIST } +12761 - 0x100114220.old.machinit.thing +- - com.lol.stopped-thing +SVC_LIST + + it "sets service running state to false" do + provider.load_current_resource + provider.current_resource.running.should be_false + end + + context "and plist for service is not available" do + before do + Dir.stub!(:glob).and_return([]) + provider.load_current_resource + end + + it "sets resouce enabled state to false" do + provider.current_resource.enabled.should be_false + end + end + + context "and plist for service is available" do + before do + Dir.stub!(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist"], []) + provider.load_current_resource + end + + it "sets resouce enabled state to true" do + provider.current_resource.enabled.should be_true + end + end + + describe "and several plists match service name" do + it "throws exception" do + Dir.stub!(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist", + "/Users/wtf/something.plist"]) + provider.load_current_resource + provider.define_resource_requirements + lambda { provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + end + end + end + describe "#start_service" do + before do + Chef::Resource::Service.stub!(:new).and_return(current_resource) + provider.load_current_resource + current_resource.stub!(:running).and_return(false) + end + + it "calls the start command if one is specified and service is not running" do + new_resource.stub!(:start_command).and_return("cowsay dirty") + + provider.should_receive(:shell_out!).with("cowsay dirty") + provider.start_service + end + + it "shows warning message if service is already running" do + current_resource.stub!(:running).and_return(true) + Chef::Log.should_receive(:debug).with("service[#{service_name}] already running, not starting") + + provider.start_service + end + + it "starts service via launchctl if service found" do + provider.should_receive(:shell_out!). + with("launchctl load -w '/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist'", + :group => 1001, :user => 101). + and_return(0) + + provider.start_service + end + end + + describe "#stop_service" do + before do + Chef::Resource::Service.stub!(:new).and_return(current_resource) + + provider.load_current_resource + current_resource.stub!(:running).and_return(true) + end + + it "calls the stop command if one is specified and service is running" do + new_resource.stub!(:stop_command).and_return("kill -9 123") + + provider.should_receive(:shell_out!).with("kill -9 123") + provider.stop_service + end + + it "shows warning message if service is not running" do + current_resource.stub!(:running).and_return(false) + Chef::Log.should_receive(:debug).with("service[#{service_name}] not running, not stopping") + + provider.stop_service + end + + it "stops the service via launchctl if service found" do + provider.should_receive(:shell_out!). + with("launchctl unload '/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist'", + :group => 1001, :user => 101). + and_return(0) + + provider.stop_service + end + end + + describe "#restart_service" do + before do + Chef::Resource::Service.stub!(:new).and_return(current_resource) + + provider.load_current_resource + current_resource.stub!(:running).and_return(true) + provider.stub!(:sleep) + end + + it "issues a command if given" do + new_resource.stub!(:restart_command).and_return("reload that thing") + + provider.should_receive(:shell_out!).with("reload that thing") + provider.restart_service + end + + it "stops and then starts service" do + provider.should_receive(:stop_service) + provider.should_receive(:start_service); + + provider.restart_service + end + end + end + end +end diff --git a/spec/unit/provider/service/redhat_spec.rb b/spec/unit/provider/service/redhat_spec.rb new file mode 100644 index 0000000000..dd874a4f05 --- /dev/null +++ b/spec/unit/provider/service/redhat_spec.rb @@ -0,0 +1,156 @@ +# +# Author:: AJ Christensen (<aj@hjksolutions.com>) +# Copyright:: Copyright (c) 2008 HJK Solutions, LLC +# 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 File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "spec_helper")) +require 'ostruct' + +shared_examples_for "define_resource_requirements_common" do + it "should raise an error if /sbin/chkconfig does not exist" do + File.stub!(:exists?).with("/sbin/chkconfig").and_return(false) + @provider.stub!(:shell_out).with("/sbin/service chef status").and_raise(Errno::ENOENT) + @provider.stub!(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_raise(Errno::ENOENT) + @provider.load_current_resource + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + + it "should not raise an error if the service exists but is not added to any runlevels" do + status = mock("Status", :exitstatus => 0, :stdout => "" , :stderr => "") + @provider.should_receive(:shell_out).with("/sbin/service chef status").and_return(status) + chkconfig = mock("Chkconfig", :exitstatus => 0, :stdout => "", :stderr => "service chef supports chkconfig, but is not referenced in any runlevel (run 'chkconfig --add chef')") + @provider.should_receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) + @provider.load_current_resource + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should_not raise_error + end +end + +describe "Chef::Provider::Service::Redhat" do + + before(:each) do + @node = Chef::Node.new + @node.automatic_attrs[:command] = {:ps => 'foo'} + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Service.new("chef") + + @current_resource = Chef::Resource::Service.new("chef") + + @provider = Chef::Provider::Service::Redhat.new(@new_resource, @run_context) + @provider.action = :start + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + File.stub!(:exists?).with("/sbin/chkconfig").and_return(true) + end + + describe "while not in why run mode" do + before(:each) do + Chef::Config[:why_run] = false + end + + describe "load current resource" do + it "sets the current enabled status to true if the service is enabled for any run level" do + status = mock("Status", :exitstatus => 0, :stdout => "" , :stderr => "") + @provider.should_receive(:shell_out).with("/sbin/service chef status").and_return(status) + chkconfig = mock("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:off 2:off 3:off 4:off 5:on 6:off", :stderr => "") + @provider.should_receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) + @provider.instance_variable_get("@service_missing").should be_false + @provider.load_current_resource + @current_resource.enabled.should be_true + end + + it "sets the current enabled status to false if the regex does not match" do + status = mock("Status", :exitstatus => 0, :stdout => "" , :stderr => "") + @provider.should_receive(:shell_out).with("/sbin/service chef status").and_return(status) + chkconfig = mock("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:off 2:off 3:off 4:off 5:off 6:off", :stderr => "") + @provider.should_receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) + @provider.instance_variable_get("@service_missing").should be_false + @provider.load_current_resource.should eql(@current_resource) + @current_resource.enabled.should be_false + end + end + + describe "define resource requirements" do + it_should_behave_like "define_resource_requirements_common" + + context "when the service does not exist" do + before do + status = mock("Status", :exitstatus => 1, :stdout => "", :stderr => "chef: unrecognized service") + @provider.should_receive(:shell_out).with("/sbin/service chef status").and_return(status) + chkconfig = mock("Chkconfig", :existatus=> 1, :stdout => "", :stderr => "error reading information on service chef: No such file or directory") + @provider.should_receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) + @provider.load_current_resource + @provider.define_resource_requirements + end + + [ "start", "reload", "restart", "enable" ].each do |action| + it "should raise an error when the action is #{action}" do + @provider.action = action + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + end + + [ "stop", "disable" ].each do |action| + it "should not raise an error when the action is #{action}" do + @provider.action = action + lambda { @provider.process_resource_requirements }.should_not raise_error + end + end + end + end + end + + describe "while in why run mode" do + before(:each) do + Chef::Config[:why_run] = true + end + + after do + Chef::Config[:why_run] = false + end + + describe "define resource requirements" do + it_should_behave_like "define_resource_requirements_common" + + it "should not raise an error if the service does not exist" do + status = mock("Status", :exitstatus => 1, :stdout => "", :stderr => "chef: unrecognized service") + @provider.should_receive(:shell_out).with("/sbin/service chef status").and_return(status) + chkconfig = mock("Chkconfig", :existatus=> 1, :stdout => "", :stderr => "error reading information on service chef: No such file or directory") + @provider.should_receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) + @provider.load_current_resource + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should_not raise_error + end + end + end + + describe "enable_service" do + it "should call chkconfig to add 'service_name'" do + @provider.should_receive(:shell_out!).with("/sbin/chkconfig #{@new_resource.service_name} on") + @provider.enable_service + end + end + + describe "disable_service" do + it "should call chkconfig to del 'service_name'" do + @provider.should_receive(:shell_out!).with("/sbin/chkconfig #{@new_resource.service_name} off") + @provider.disable_service + end + end + +end diff --git a/spec/unit/provider/service/simple_service_spec.rb b/spec/unit/provider/service/simple_service_spec.rb new file mode 100644 index 0000000000..cc0173e246 --- /dev/null +++ b/spec/unit/provider/service/simple_service_spec.rb @@ -0,0 +1,171 @@ +# +# Author:: Mathieu Sauve-Frankel <msf@kisoku.net> +# Copyright:: Copyright (c) 2009, Mathieu Sauve Frankel +# 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::Provider::Service::Simple, "load_current_resource" do + before(:each) do + @node = Chef::Node.new + @node.automatic_attrs[:command] = {:ps => "ps -ef"} + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Service.new("chef") + @current_resource = Chef::Resource::Service.new("chef") + + @provider = Chef::Provider::Service::Simple.new(@new_resource, @run_context) + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + + @stdout = StringIO.new(<<-NOMOCKINGSTRINGSPLZ) +aj 7842 5057 0 21:26 pts/2 00:00:06 vi init.rb +aj 7903 5016 0 21:26 pts/5 00:00:00 /bin/bash +aj 8119 6041 0 21:34 pts/3 00:00:03 vi simple_service_spec.rb +NOMOCKINGSTRINGSPLZ + @status = mock("Status", :exitstatus => 0, :stdout => @stdout) + @provider.stub!(:shell_out!).and_return(@status) + end + + it "should create a current resource with the name of the new resource" do + Chef::Resource::Service.should_receive(:new).and_return(@current_resource) + @provider.load_current_resource + end + + it "should set the current resources service name to the new resources service name" do + @current_resource.should_receive(:service_name).with(@new_resource.service_name) + @provider.load_current_resource + end + + it "should raise error if the node has a nil ps attribute and no other means to get status" do + @node.automatic_attrs[:command] = {:ps => nil} + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + + it "should raise error if the node has an empty ps attribute and no other means to get status" do + @node.automatic_attrs[:command] = {:ps => ""} + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + + describe "when we have a 'ps' attribute" do + it "should shell_out! the node's ps command" do + @provider.should_receive(:shell_out!).with(@node[:command][:ps]).and_return(@status) + @provider.load_current_resource + end + + it "should read stdout of the ps command" do + @provider.stub!(:shell_out!).and_return(@status) + @stdout.should_receive(:each_line).and_return(true) + @provider.load_current_resource + end + + it "should set running to true if the regex matches the output" do + @stdout = StringIO.new(<<-NOMOCKINGSTRINGSPLZ) +aj 7842 5057 0 21:26 pts/2 00:00:06 chef +aj 7842 5057 0 21:26 pts/2 00:00:06 poos +NOMOCKINGSTRINGSPLZ + @status = mock("Status", :exitstatus => 0, :stdout => @stdout) + @provider.stub!(:shell_out!).and_return(@status) + @provider.load_current_resource + @current_resource.running.should be_true + end + + it "should set running to false if the regex doesn't match" do + @provider.stub!(:shell_out!).and_return(@status) + @provider.load_current_resource + @current_resource.running.should be_false + end + + it "should raise an exception if ps fails" do + @provider.stub!(:shell_out!).and_raise(Mixlib::ShellOut::ShellCommandFailed) + @provider.action = :start + @provider.load_current_resource + @provider.define_resource_requirements + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + end + + it "should return the current resource" do + @provider.load_current_resource.should eql(@current_resource) + end + + + + describe "when starting the service" do + it "should call the start command if one is specified" do + @new_resource.stub!(:start_command).and_return("#{@new_resource.start_command}") + @provider.should_receive(:shell_out!).with("#{@new_resource.start_command}") + @provider.start_service() + end + + it "should raise an exception if no start command is specified" do + @provider.define_resource_requirements + @provider.action = :start + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + end + + describe "when stopping a service" do + it "should call the stop command if one is specified" do + @new_resource.stop_command("/etc/init.d/themadness stop") + @provider.should_receive(:shell_out!).with("/etc/init.d/themadness stop") + @provider.stop_service() + end + + it "should raise an exception if no stop command is specified" do + @provider.define_resource_requirements + @provider.action = :stop + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + end + + describe Chef::Provider::Service::Simple, "restart_service" do + it "should call the restart command if one has been specified" do + @new_resource.restart_command("/etc/init.d/foo restart") + @provider.should_receive(:shell_out!).with("/etc/init.d/foo restart") + @provider.restart_service() + end + + it "should raise an exception if the resource doesn't support restart, no restart command is provided, and no stop command is provided" do + @provider.define_resource_requirements + @provider.action = :restart + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::Service) + end + + it "should just call stop, then start when the resource doesn't support restart and no restart_command is specified" do + @provider.should_receive(:stop_service) + @provider.should_receive(:sleep).with(1) + @provider.should_receive(:start_service) + @provider.restart_service() + end + end + + describe Chef::Provider::Service::Simple, "reload_service" do + it "should raise an exception if reload is requested but no command is specified" do + @provider.define_resource_requirements + @provider.action = :reload + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "should should run the user specified reload command if one is specified" do + @new_resource.reload_command("kill -9 1") + @provider.should_receive(:shell_out!).with("kill -9 1") + @provider.reload_service() + end + end +end diff --git a/spec/unit/provider/service/solaris_smf_service_spec.rb b/spec/unit/provider/service/solaris_smf_service_spec.rb new file mode 100644 index 0000000000..3ea2902755 --- /dev/null +++ b/spec/unit/provider/service/solaris_smf_service_spec.rb @@ -0,0 +1,140 @@ +# +# Author:: Toomas Pelberg (<toomasp@gmx.net>) +# Copyright:: Copyright (c) 2010 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::Provider::Service::Solaris do + before(:each) do + @node =Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Service.new('chef') + + @current_resource = Chef::Resource::Service.new('chef') + + @provider = Chef::Provider::Service::Solaris.new(@new_resource, @run_context) + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + + @stdin = StringIO.new + @stdout = StringIO.new + @stderr = StringIO.new + @pid = 2342 + @stdout_string = "state disabled" + @stdout.stub!(:gets).and_return(@stdout_string) + end + + it "should raise an error if /bin/svcs does not exist" do + File.should_receive(:exists?).with("/bin/svcs").and_return(false) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Service) + end + + describe "on a host with /bin/svcs" do + + before do + File.stub!(:exists?).with('/bin/svcs').and_return(true) + end + + describe "when discovering the current service state" do + it "should create a current resource with the name of the new resource" do + @provider.stub!(:popen4).with("/bin/svcs -l chef").and_return(@status) + Chef::Resource::Service.should_receive(:new).and_return(@current_resource) + @provider.load_current_resource + end + + + it "should return the current resource" do + @provider.stub!(:popen4).with("/bin/svcs -l chef").and_return(@status) + @provider.load_current_resource.should eql(@current_resource) + end + + it "should popen4 '/bin/svcs -l service_name'" do + @provider.should_receive(:popen4).with("/bin/svcs -l chef").and_return(@status) + @provider.load_current_resource + end + + it "should mark service as not running" do + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @current_resource.should_receive(:running).with(false) + @provider.load_current_resource + end + + it "should mark service as running" do + @stdout.stub!(:each).and_yield("state online") + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @current_resource.should_receive(:running).with(true) + @provider.load_current_resource + end + end + + describe "when enabling the service" do + before(:each) do + #@provider = Chef::Provider::Service::Solaris.new(@node, @new_resource) + @provider.current_resource = @current_resource + @current_resource.enabled(true) + end + + it "should call svcadm enable chef" do + @provider.should_receive(:run_command).with({:command => "/usr/sbin/svcadm enable chef"}) + @provider.should_receive(:service_status).and_return(@current_resource) + @provider.enable_service.should be_true + end + + it "should call svcadm enable chef for start_service" do + @provider.should_receive(:run_command).with({:command => "/usr/sbin/svcadm enable chef"}) + @provider.should_receive(:service_status).and_return(@current_resource) + @provider.start_service.should be_true + end + + end + + + describe "when disabling the service" do + before(:each) do + @provider.current_resource = @current_resource + @current_resource.enabled(false) + end + + it "should call svcadm disable chef" do + @provider.should_receive(:run_command).with({:command => "/usr/sbin/svcadm disable chef"}) + @provider.should_receive(:service_status).and_return(@current_resource) + @provider.disable_service.should be_false + end + + it "should call svcadm disable chef for stop_service" do + @provider.should_receive(:run_command).with({:command => "/usr/sbin/svcadm disable chef"}) + @provider.should_receive(:service_status).and_return(@current_resource) + @provider.stop_service.should be_false + end + + end + + describe "when reloading the service" do + before(:each) do + @status = mock("Process::Status", :exitstatus => 0) + @provider.current_resource = @current_resource + end + + it "should call svcadm refresh chef" do + @provider.should_receive(:run_command).with({:command => "/usr/sbin/svcadm refresh chef"}).and_return(@status) + @provider.reload_service.should be_true + end + + end + end +end diff --git a/spec/unit/provider/service/systemd_service_spec.rb b/spec/unit/provider/service/systemd_service_spec.rb new file mode 100644 index 0000000000..dbdedecd40 --- /dev/null +++ b/spec/unit/provider/service/systemd_service_spec.rb @@ -0,0 +1,239 @@ +# +# Author:: Stephen Haynes (<sh@nomitor.com>) +# Copyright:: Copyright (c) 2011 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::Provider::Service::Systemd do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Service.new('rsyslog.service') + @provider = Chef::Provider::Service::Systemd.new(@new_resource, @run_context) + end + + describe "load_current_resource" do + before(:each) do + @current_resource = Chef::Resource::Service.new('rsyslog.service') + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + + @provider.stub!(:is_active?).and_return(false) + @provider.stub!(:is_enabled?).and_return(false) + end + + it "should create a current resource with the name of the new resource" do + Chef::Resource::Service.should_receive(:new).and_return(@current_resource) + @provider.load_current_resource + end + + it "should set the current resources service name to the new resources service name" do + @current_resource.should_receive(:service_name).with(@new_resource.service_name) + @provider.load_current_resource + end + + it "should check if the service is running" do + @provider.should_receive(:is_active?) + @provider.load_current_resource + end + + it "should set running to true if the service is running" do + @provider.stub!(:is_active?).and_return(true) + @current_resource.should_receive(:running).with(true) + @provider.load_current_resource + end + + it "should set running to false if the service is not running" do + @provider.stub!(:is_active?).and_return(false) + @current_resource.should_receive(:running).with(false) + @provider.load_current_resource + end + + describe "when a status command has been specified" do + before do + @new_resource.stub!(:status_command).and_return("/bin/chefhasmonkeypants status") + end + + it "should run the services status command if one has been specified" do + @provider.stub!(:run_command_with_systems_locale).with({:command => "/bin/chefhasmonkeypants status"}).and_return(0) + @current_resource.should_receive(:running).with(true) + @provider.load_current_resource + end + + it "should run the services status command if one has been specified and properly set status check state" do + @provider.stub!(:run_command_with_systems_locale).with({:command => "/bin/chefhasmonkeypants status"}).and_return(0) + @provider.load_current_resource + @provider.instance_variable_get("@status_check_success").should be_true + end + + it "should set running to false if it catches a Chef::Exceptions::Exec when using a status command" do + @provider.stub!(:run_command_with_systems_locale).and_raise(Chef::Exceptions::Exec) + @current_resource.should_receive(:running).with(false) + @provider.load_current_resource + end + + it "should update state to indicate status check failed when an exception is thrown using a status command" do + @provider.stub!(:run_command_with_systems_locale).and_raise(Chef::Exceptions::Exec) + @provider.load_current_resource + @provider.instance_variable_get("@status_check_success").should be_false + end + end + + it "should check if the service is enabled" do + @provider.should_receive(:is_enabled?) + @provider.load_current_resource + end + + it "should set enabled to true if the service is enabled" do + @provider.stub!(:is_enabled?).and_return(true) + @current_resource.should_receive(:enabled).with(true) + @provider.load_current_resource + end + + it "should set enabled to false if the service is not enabled" do + @provider.stub!(:is_enabled?).and_return(false) + @current_resource.should_receive(:enabled).with(false) + @provider.load_current_resource + end + + it "should return the current resource" do + @provider.load_current_resource.should eql(@current_resource) + end + end + + describe "start and stop service" do + before(:each) do + @current_resource = Chef::Resource::Service.new('rsyslog.service') + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + @provider.current_resource = @current_resource + end + + it "should call the start command if one is specified" do + @new_resource.stub!(:start_command).and_return("/sbin/rsyslog startyousillysally") + @provider.should_receive(:shell_out!).with("/sbin/rsyslog startyousillysally") + @provider.start_service + end + + it "should call '/bin/systemctl start service_name' if no start command is specified" do + @provider.should_receive(:run_command_with_systems_locale).with({:command => "/bin/systemctl start #{@new_resource.service_name}"}).and_return(0) + @provider.start_service + end + + it "should not call '/bin/systemctl start service_name' if it is already running" do + @current_resource.stub!(:running).and_return(true) + @provider.should_not_receive(:run_command_with_systems_locale).with({:command => "/bin/systemctl start #{@new_resource.service_name}"}).and_return(0) + @provider.start_service + end + + it "should call the restart command if one is specified" do + @current_resource.stub!(:running).and_return(true) + @new_resource.stub!(:restart_command).and_return("/sbin/rsyslog restartyousillysally") + @provider.should_receive(:shell_out!).with("/sbin/rsyslog restartyousillysally") + @provider.restart_service + end + + it "should call '/bin/systemctl restart service_name' if no restart command is specified" do + @current_resource.stub!(:running).and_return(true) + @provider.should_receive(:run_command_with_systems_locale).with({:command => "/bin/systemctl restart #{@new_resource.service_name}"}).and_return(0) + @provider.restart_service + end + + it "should call the reload command if one is specified" do + @current_resource.stub!(:running).and_return(true) + @new_resource.stub!(:reload_command).and_return("/sbin/rsyslog reloadyousillysally") + @provider.should_receive(:shell_out!).with("/sbin/rsyslog reloadyousillysally") + @provider.reload_service + end + + it "should call '/bin/systemctl reload service_name' if no reload command is specified" do + @current_resource.stub!(:running).and_return(true) + @provider.should_receive(:run_command_with_systems_locale).with({:command => "/bin/systemctl reload #{@new_resource.service_name}"}).and_return(0) + @provider.reload_service + end + + it "should call the stop command if one is specified" do + @current_resource.stub!(:running).and_return(true) + @new_resource.stub!(:stop_command).and_return("/sbin/rsyslog stopyousillysally") + @provider.should_receive(:shell_out!).with("/sbin/rsyslog stopyousillysally") + @provider.stop_service + end + + it "should call '/bin/systemctl stop service_name' if no stop command is specified" do + @current_resource.stub!(:running).and_return(true) + @provider.should_receive(:run_command_with_systems_locale).with({:command => "/bin/systemctl stop #{@new_resource.service_name}"}).and_return(0) + @provider.stop_service + end + + it "should not call '/bin/systemctl stop service_name' if it is already stopped" do + @current_resource.stub!(:running).and_return(false) + @provider.should_not_receive(:run_command_with_systems_locale).with({:command => "/bin/systemctl stop #{@new_resource.service_name}"}).and_return(0) + @provider.stop_service + end + end + + describe "enable and disable service" do + before(:each) do + @current_resource = Chef::Resource::Service.new('rsyslog.service') + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + @provider.current_resource = @current_resource + end + + it "should call '/bin/systemctl enable service_name' to enable the service" do + @provider.should_receive(:run_command_with_systems_locale).with({:command => "/bin/systemctl enable #{@new_resource.service_name}"}).and_return(0) + @provider.enable_service + end + + it "should call '/bin/systemctl disable service_name' to disable the service" do + @provider.should_receive(:run_command_with_systems_locale).with({:command => "/bin/systemctl disable #{@new_resource.service_name}"}).and_return(0) + @provider.disable_service + end + end + + describe "is_active?" do + before(:each) do + @current_resource = Chef::Resource::Service.new('rsyslog.service') + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + end + + it "should return true if '/bin/systemctl is-active service_name' returns 0" do + @provider.should_receive(:run_command_with_systems_locale).with({:command => '/bin/systemctl is-active rsyslog.service', :ignore_failure => true}).and_return(0) + @provider.is_active?.should be_true + end + + it "should return false if '/bin/systemctl is-active service_name' returns anything except 0" do + @provider.should_receive(:run_command_with_systems_locale).with({:command => '/bin/systemctl is-active rsyslog.service', :ignore_failure => true}).and_return(1) + @provider.is_active?.should be_false + end + end + + describe "is_enabled?" do + before(:each) do + @current_resource = Chef::Resource::Service.new('rsyslog.service') + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + end + + it "should return true if '/bin/systemctl is-enabled service_name' returns 0" do + @provider.should_receive(:run_command_with_systems_locale).with({:command => '/bin/systemctl is-enabled rsyslog.service', :ignore_failure => true}).and_return(0) + @provider.is_enabled?.should be_true + end + + it "should return false if '/bin/systemctl is-enabled service_name' returns anything except 0" do + @provider.should_receive(:run_command_with_systems_locale).with({:command => '/bin/systemctl is-enabled rsyslog.service', :ignore_failure => true}).and_return(1) + @provider.is_enabled?.should be_false + end + end +end diff --git a/spec/unit/provider/service/upstart_service_spec.rb b/spec/unit/provider/service/upstart_service_spec.rb new file mode 100644 index 0000000000..2fc49c7aa2 --- /dev/null +++ b/spec/unit/provider/service/upstart_service_spec.rb @@ -0,0 +1,314 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org) +# Copyright:: Copyright (c) 2010 Bryan McLellan +# 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::Provider::Service::Upstart do + before(:each) do + @node =Chef::Node.new + @node.name('upstarter') + @node.automatic_attrs[:platform] = 'ubuntu' + @node.automatic_attrs[:platform_version] = '9.10' + + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Service.new("rsyslog") + @provider = Chef::Provider::Service::Upstart.new(@new_resource, @run_context) + end + + describe "when first created" do + before do + @platform = nil + end + + it "should return /etc/event.d as the upstart job directory when running on Ubuntu 9.04" do + @node.automatic_attrs[:platform_version] = '9.04' + #Chef::Platform.stub!(:find_platform_and_version).and_return([ "ubuntu", "9.04" ]) + @provider = Chef::Provider::Service::Upstart.new(@new_resource, @run_context) + @provider.instance_variable_get(:@upstart_job_dir).should == "/etc/event.d" + @provider.instance_variable_get(:@upstart_conf_suffix).should == "" + end + + it "should return /etc/init as the upstart job directory when running on Ubuntu 9.10" do + @node.automatic_attrs[:platform_version] = '9.10' + @provider = Chef::Provider::Service::Upstart.new(@new_resource, @run_context) + @provider.instance_variable_get(:@upstart_job_dir).should == "/etc/init" + @provider.instance_variable_get(:@upstart_conf_suffix).should == ".conf" + end + + it "should return /etc/init as the upstart job directory by default" do + @node.automatic_attrs[:platform_version] = '9000' + @provider = Chef::Provider::Service::Upstart.new(@new_resource, @run_context) + @provider.instance_variable_get(:@upstart_job_dir).should == "/etc/init" + @provider.instance_variable_get(:@upstart_conf_suffix).should == ".conf" + end + end + + describe "load_current_resource" do + before(:each) do + @node.automatic_attrs[:command] = {:ps => "ps -ax"} + + @current_resource = Chef::Resource::Service.new("rsyslog") + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + + @status = mock("Status", :exitstatus => 0) + @provider.stub!(:popen4).and_return(@status) + @stdin = StringIO.new + @stdout = StringIO.new + @stderr = StringIO.new + @pid = mock("PID") + + ::File.stub!(:exists?).and_return(true) + ::File.stub!(:open).and_return(true) + end + + it "should create a current resource with the name of the new resource" do + Chef::Resource::Service.should_receive(:new).and_return(@current_resource) + @provider.load_current_resource + end + + it "should set the current resources service name to the new resources service name" do + @current_resource.should_receive(:service_name).with(@new_resource.service_name) + @provider.load_current_resource + end + + it "should run '/sbin/status rsyslog'" do + @provider.should_receive(:popen4).with("/sbin/status rsyslog").and_return(@status) + @provider.load_current_resource + end + + describe "when the status command uses the new format" do + before do + end + + it "should set running to true if the the status command returns 0" do + @stdout = StringIO.new("rsyslog start/running") + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.load_current_resource + @current_resource.running.should be_true + end + + it "should set running to false if the status command returns anything except 0" do + @stdout = StringIO.new("rsyslog stop/waiting") + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.load_current_resource + @current_resource.running.should be_false + end + end + + describe "when the status command uses the old format" do + it "should set running to true if the the status command returns 0" do + @stdout = StringIO.new("rsyslog (start) running, process 32225") + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.load_current_resource + @current_resource.running.should be_true + end + + it "should set running to false if the status command returns anything except 0" do + @stdout = StringIO.new("rsyslog (stop) waiting") + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.load_current_resource + @current_resource.running.should be_false + end + end + + it "should set running to false if it catches a Chef::Exceptions::Exec" do + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_raise(Chef::Exceptions::Exec) + @current_resource.should_receive(:running).with(false) + @provider.load_current_resource + end + + it "should set enabled to true when it finds 'starts on'" do + @lines = mock("start on filesystem", :gets => "start on filesystem") + ::File.stub!(:open).and_yield(@lines) + @current_resource.should_receive(:running).with(false) + @provider.load_current_resource + end + + it "should set enabled to false when it finds '#starts on'" do + @lines = mock("start on filesystem", :gets => "#start on filesystem") + ::File.stub!(:open).and_yield(@lines) + @current_resource.should_receive(:running).with(false) + @provider.load_current_resource + end + + it "should assume disable when no job configuration file is found" do + ::File.stub!(:exists?).and_return(false) + @current_resource.should_receive(:running).with(false) + @provider.load_current_resource + end + + + it "should track state when the upstart configuration file fails to load" do + File.should_receive(:exists?).and_return false + @provider.load_current_resource + @provider.instance_variable_get("@config_file_found").should == false + end + + describe "when a status command has been specified" do + before do + @new_resource.stub!(:status_command).and_return("/bin/chefhasmonkeypants status") + end + + it "should run the services status command if one has been specified" do + @provider.stub!(:run_command_with_systems_locale).with({:command => "/bin/chefhasmonkeypants status"}).and_return(0) + @current_resource.should_receive(:running).with(true) + @provider.load_current_resource + end + + it "should track state when the user-provided status command fails" do + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_raise(Chef::Exceptions::Exec) + @provider.load_current_resource + @provider.instance_variable_get("@command_success").should == false + end + + it "should set running to false if it catches a Chef::Exceptions::Exec when using a status command" do + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_raise(Chef::Exceptions::Exec) + @current_resource.should_receive(:running).with(false) + @provider.load_current_resource + end + end + + it "should track state when we fail to obtain service status via upstart_state" do + @provider.should_receive(:upstart_state).and_raise Chef::Exceptions::Exec + @provider.load_current_resource + @provider.instance_variable_get("@command_success").should == false + end + + it "should return the current resource" do + @provider.load_current_resource.should eql(@current_resource) + end + + + end + + describe "enable and disable service" do + before(:each) do + @current_resource = Chef::Resource::Service.new('rsyslog') + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + @provider.current_resource = @current_resource + Chef::Util::FileEdit.stub!(:new) + end + + it "should enable the service if it is not enabled" do + @file = Object.new + Chef::Util::FileEdit.stub!(:new).and_return(@file) + @current_resource.stub!(:enabled).and_return(false) + @file.should_receive(:search_file_replace) + @file.should_receive(:write_file) + @provider.enable_service() + end + + it "should disable the service if it is enabled" do + @file = Object.new + Chef::Util::FileEdit.stub!(:new).and_return(@file) + @current_resource.stub!(:enabled).and_return(true) + @file.should_receive(:search_file_replace) + @file.should_receive(:write_file) + @provider.disable_service() + end + + end + + describe "start and stop service" do + before(:each) do + @current_resource = Chef::Resource::Service.new('rsyslog') + + Chef::Resource::Service.stub!(:new).and_return(@current_resource) + @provider.current_resource = @current_resource + end + + it "should call the start command if one is specified" do + @new_resource.stub!(:start_command).and_return("/sbin/rsyslog startyousillysally") + @provider.should_receive(:shell_out!).with("/sbin/rsyslog startyousillysally") + @provider.start_service() + end + + it "should call '/sbin/start service_name' if no start command is specified" do + @provider.should_receive(:run_command_with_systems_locale).with({:command => "/sbin/start #{@new_resource.service_name}"}).and_return(0) + @provider.start_service() + end + + it "should not call '/sbin/start service_name' if it is already running" do + @current_resource.stub!(:running).and_return(true) + @provider.should_not_receive(:run_command_with_systems_locale).with({:command => "/sbin/start #{@new_resource.service_name}"}).and_return(0) + @provider.start_service() + end + + it "should pass parameters to the start command if they are provided" do + @new_resource = Chef::Resource::Service.new("rsyslog") + @new_resource.parameters({ "OSD_ID" => "2" }) + @provider = Chef::Provider::Service::Upstart.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + @provider.should_receive(:run_command_with_systems_locale).with({:command => "/sbin/start rsyslog OSD_ID=2"}).and_return(0) + @provider.start_service() + end + + it "should call the restart command if one is specified" do + @current_resource.stub!(:running).and_return(true) + @new_resource.stub!(:restart_command).and_return("/sbin/rsyslog restartyousillysally") + @provider.should_receive(:shell_out!).with("/sbin/rsyslog restartyousillysally") + @provider.restart_service() + end + + it "should call '/sbin/restart service_name' if no restart command is specified" do + @current_resource.stub!(:running).and_return(true) + @provider.should_receive(:run_command_with_systems_locale).with({:command => "/sbin/restart #{@new_resource.service_name}"}).and_return(0) + @provider.restart_service() + end + + it "should call '/sbin/start service_name' if restart_service is called for a stopped service" do + @current_resource.stub!(:running).and_return(false) + @provider.should_receive(:run_command_with_systems_locale).with({:command => "/sbin/start #{@new_resource.service_name}"}).and_return(0) + @provider.restart_service() + end + + it "should call the reload command if one is specified" do + @current_resource.stub!(:running).and_return(true) + @new_resource.stub!(:reload_command).and_return("/sbin/rsyslog reloadyousillysally") + @provider.should_receive(:shell_out!).with("/sbin/rsyslog reloadyousillysally") + @provider.reload_service() + end + + it "should call '/sbin/reload service_name' if no reload command is specified" do + @current_resource.stub!(:running).and_return(true) + @provider.should_receive(:run_command_with_systems_locale).with({:command => "/sbin/reload #{@new_resource.service_name}"}).and_return(0) + @provider.reload_service() + end + + it "should call the stop command if one is specified" do + @current_resource.stub!(:running).and_return(true) + @new_resource.stub!(:stop_command).and_return("/sbin/rsyslog stopyousillysally") + @provider.should_receive(:shell_out!).with("/sbin/rsyslog stopyousillysally") + @provider.stop_service() + end + + it "should call '/sbin/stop service_name' if no stop command is specified" do + @current_resource.stub!(:running).and_return(true) + @provider.should_receive(:run_command_with_systems_locale).with({:command => "/sbin/stop #{@new_resource.service_name}"}).and_return(0) + @provider.stop_service() + end + + it "should not call '/sbin/stop service_name' if it is already stopped" do + @current_resource.stub!(:running).and_return(false) + @provider.should_not_receive(:run_command_with_systems_locale).with({:command => "/sbin/stop #{@new_resource.service_name}"}).and_return(0) + @provider.stop_service() + end + end +end diff --git a/spec/unit/provider/service/windows_spec.rb b/spec/unit/provider/service/windows_spec.rb new file mode 100644 index 0000000000..a68e798d36 --- /dev/null +++ b/spec/unit/provider/service/windows_spec.rb @@ -0,0 +1,235 @@ +# +# Author:: Nuo Yan <nuo@opscode.com> +# Author:: Seth Chisamore <schisamo@opscode.com> +# Copyright:: Copyright (c) 2010-2011 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::Provider::Service::Windows, "load_current_resource" do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Service.new("chef") + @provider = Chef::Provider::Service::Windows.new(@new_resource, @run_context) + Object.send(:remove_const, 'Win32') if defined?(Win32) + Win32 = Module.new + Win32::Service = Class.new + Win32::Service::AUTO_START = 0x00000002 + Win32::Service::DISABLED = 0x00000004 + Win32::Service.stub!(:status).with(@new_resource.service_name).and_return( + mock("StatusStruct", :current_state => "running")) + Win32::Service.stub!(:config_info).with(@new_resource.service_name).and_return( + mock("ConfigStruct", :start_type => "auto start")) + Win32::Service.stub!(:exists?).and_return(true) + end + + it "should set the current resources service name to the new resources service name" do + @provider.load_current_resource + @provider.current_resource.service_name.should == 'chef' + end + + it "should return the current resource" do + @provider.load_current_resource.should equal(@provider.current_resource) + end + + it "should set the current resources status" do + @provider.load_current_resource + @provider.current_resource.running.should be_true + end + + it "should set the current resources start type" do + @provider.load_current_resource + @provider.current_resource.enabled.should be_true + end + + describe Chef::Provider::Service::Windows, "start_service" do + before(:each) do + Win32::Service.stub!(:status).with(@new_resource.service_name).and_return( + mock("StatusStruct", :current_state => "stopped"), + mock("StatusStruct", :current_state => "running")) + end + + it "should call the start command if one is specified" do + @new_resource.start_command "sc start chef" + @provider.should_receive(:shell_out!).with("#{@new_resource.start_command}").and_return("Starting custom service") + @provider.start_service + @new_resource.updated_by_last_action?.should be_true + end + + it "should use the built-in command if no start command is specified" do + Win32::Service.should_receive(:start).with(@new_resource.service_name) + @provider.start_service + @new_resource.updated_by_last_action?.should be_true + end + + it "should do nothing if the service does not exist" do + Win32::Service.stub!(:exists?).with(@new_resource.service_name).and_return(false) + Win32::Service.should_not_receive(:start).with(@new_resource.service_name) + @provider.start_service + @new_resource.updated_by_last_action?.should be_false + end + + it "should do nothing if the service is running" do + Win32::Service.stub!(:status).with(@new_resource.service_name).and_return( + mock("StatusStruct", :current_state => "running")) + @provider.load_current_resource + Win32::Service.should_not_receive(:start).with(@new_resource.service_name) + @provider.start_service + @new_resource.updated_by_last_action?.should be_false + end + end + + describe Chef::Provider::Service::Windows, "stop_service" do + + before(:each) do + Win32::Service.stub!(:status).with(@new_resource.service_name).and_return( + mock("StatusStruct", :current_state => "running"), + mock("StatusStruct", :current_state => "stopped")) + end + + it "should call the stop command if one is specified" do + @new_resource.stop_command "sc stop chef" + @provider.should_receive(:shell_out!).with("#{@new_resource.stop_command}").and_return("Stopping custom service") + @provider.stop_service + @new_resource.updated_by_last_action?.should be_true + end + + it "should use the built-in command if no stop command is specified" do + Win32::Service.should_receive(:stop).with(@new_resource.service_name) + @provider.stop_service + @new_resource.updated_by_last_action?.should be_true + end + + it "should do nothing if the service does not exist" do + Win32::Service.stub!(:exists?).with(@new_resource.service_name).and_return(false) + Win32::Service.should_not_receive(:stop).with(@new_resource.service_name) + @provider.stop_service + @new_resource.updated_by_last_action?.should be_false + end + + it "should do nothing if the service is stopped" do + Win32::Service.stub!(:status).with(@new_resource.service_name).and_return( + mock("StatusStruct", :current_state => "stopped")) + @provider.load_current_resource + Win32::Service.should_not_receive(:stop).with(@new_resource.service_name) + @provider.stop_service + @new_resource.updated_by_last_action?.should be_false + end + end + + describe Chef::Provider::Service::Windows, "restart_service" do + + it "should call the restart command if one is specified" do + @new_resource.restart_command "sc restart" + @provider.should_receive(:shell_out!).with("#{@new_resource.restart_command}") + @provider.restart_service + @new_resource.updated_by_last_action?.should be_true + end + + it "should stop then start the service if it is running" do + Win32::Service.stub!(:status).with(@new_resource.service_name).and_return( + mock("StatusStruct", :current_state => "running"), + mock("StatusStruct", :current_state => "stopped"), + mock("StatusStruct", :current_state => "stopped"), + mock("StatusStruct", :current_state => "running")) + Win32::Service.should_receive(:stop).with(@new_resource.service_name) + Win32::Service.should_receive(:start).with(@new_resource.service_name) + @provider.restart_service + @new_resource.updated_by_last_action?.should be_true + end + + it "should just start the service if it is stopped" do + Win32::Service.stub!(:status).with(@new_resource.service_name).and_return( + mock("StatusStruct", :current_state => "stopped"), + mock("StatusStruct", :current_state => "stopped"), + mock("StatusStruct", :current_state => "running")) + Win32::Service.should_receive(:start).with(@new_resource.service_name) + @provider.restart_service + @new_resource.updated_by_last_action?.should be_true + end + + it "should do nothing if the service does not exist" do + Win32::Service.stub!(:exists?).with(@new_resource.service_name).and_return(false) + Win32::Service.should_not_receive(:stop).with(@new_resource.service_name) + Win32::Service.should_not_receive(:start).with(@new_resource.service_name) + @provider.restart_service + @new_resource.updated_by_last_action?.should be_false + end + + end + + describe Chef::Provider::Service::Windows, "enable_service" do + + before(:each) do + Win32::Service.stub!(:config_info).with(@new_resource.service_name).and_return( + mock("ConfigStruct", :start_type => "disabled")) + end + + it "should enable service" do + Win32::Service.should_receive(:configure).with(:service_name => @new_resource.service_name, :start_type => Win32::Service::AUTO_START) + @provider.enable_service + @new_resource.updated_by_last_action?.should be_true + end + + it "should do nothing if the service does not exist" do + Win32::Service.stub!(:exists?).with(@new_resource.service_name).and_return(false) + Win32::Service.should_not_receive(:configure) + @provider.enable_service + @new_resource.updated_by_last_action?.should be_false + end + + it "should do nothing if the service is enabled" do + Win32::Service.stub!(:config_info).with(@new_resource.service_name).and_return( + mock("ConfigStruct", :start_type => "auto start")) + Win32::Service.should_not_receive(:configure) + @provider.enable_service + @new_resource.updated_by_last_action?.should be_false + end + end + + describe Chef::Provider::Service::Windows, "disable_service" do + + before(:each) do + Win32::Service.stub!(:config_info).with(@new_resource.service_name).and_return( + mock("ConfigStruct", :start_type => "auto start")) + end + + it "should disable service" do + Win32::Service.should_receive(:configure).with(:service_name => @new_resource.service_name, :start_type => Win32::Service::DISABLED) + @provider.disable_service + @new_resource.updated_by_last_action?.should be_true + end + + it "should do nothing if the service does not exist" do + Win32::Service.stub!(:exists?).with(@new_resource.service_name).and_return(false) + Win32::Service.should_not_receive(:configure) + @provider.disable_service + @new_resource.updated_by_last_action?.should be_false + end + + it "should do nothing if the service is disabled" do + Win32::Service.stub!(:config_info).with(@new_resource.service_name).and_return( + mock("ConfigStruct", :start_type => "disabled")) + @provider.load_current_resource + Win32::Service.should_not_receive(:configure) + @provider.disable_service + @new_resource.updated_by_last_action?.should be_false + end + + end +end diff --git a/spec/unit/provider/service_spec.rb b/spec/unit/provider/service_spec.rb new file mode 100644 index 0000000000..d25d7cf735 --- /dev/null +++ b/spec/unit/provider/service_spec.rb @@ -0,0 +1,169 @@ +# +# Author:: AJ Christensen (<aj@hjksolutions.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' + +describe Chef::Provider::Service do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Service.new("chef") + @current_resource = Chef::Resource::Service.new("chef") + + @provider = Chef::Provider::Service.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + @provider.stub!(:load_current_resource) + end + + describe "when enabling the service" do + it "should enable the service if disabled and set the resource as updated" do + @current_resource.enabled(false) + @provider.should_receive(:enable_service).and_return(true) + @provider.action_enable + @provider.converge + @provider.new_resource.should be_updated + end + + it "should not enable the service if already enabled" do + @current_resource.enabled(true) + @provider.should_not_receive(:enable_service) + @provider.action_enable + @provider.converge + @provider.new_resource.should_not be_updated + end + end + + + describe "when disabling the service" do + it "should disable the service if enabled and set the resource as updated" do + @current_resource.stub!(:enabled).and_return(true) + @provider.should_receive(:disable_service).and_return(true) + @provider.run_action(:disable) + @provider.new_resource.should be_updated + end + + it "should not disable the service if already disabled" do + @current_resource.stub!(:enabled).and_return(false) + @provider.should_not_receive(:disable_service).and_return(true) + @provider.run_action(:disable) + @provider.new_resource.should_not be_updated + end + end + + describe "action_start" do + it "should start the service if it isn't running and set the resource as updated" do + @current_resource.running(false) + @provider.should_receive(:start_service).with.and_return(true) + @provider.run_action(:start) + @provider.new_resource.should be_updated + end + + it "should not start the service if already running" do + @current_resource.running(true) + @provider.should_not_receive(:start_service) + @provider.run_action(:start) + @provider.new_resource.should_not be_updated + end + end + + describe "action_stop" do + it "should stop the service if it is running and set the resource as updated" do + @current_resource.stub!(:running).and_return(true) + @provider.should_receive(:stop_service).and_return(true) + @provider.run_action(:stop) + @provider.new_resource.should be_updated + end + + it "should not stop the service if it's already stopped" do + @current_resource.stub!(:running).and_return(false) + @provider.should_not_receive(:stop_service).and_return(true) + @provider.run_action(:stop) + @provider.new_resource.should_not be_updated + end + end + + describe "action_restart" do + before do + @current_resource.supports(:restart => true) + end + + it "should restart the service if it's supported and set the resource as updated" do + @provider.should_receive(:restart_service).and_return(true) + @provider.run_action(:restart) + @provider.new_resource.should be_updated + end + + it "should restart the service even if it isn't running and set the resource as updated" do + @current_resource.stub!(:running).and_return(false) + @provider.should_receive(:restart_service).and_return(true) + @provider.run_action(:restart) + @provider.new_resource.should be_updated + end + end + + describe "action_reload" do + before do + @new_resource.supports(:reload => true) + end + + it "should raise an exception if reload isn't supported" do + @new_resource.supports(:reload => false) + @new_resource.stub!(:reload_command).and_return(false) + lambda { @provider.run_action(:reload) }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "should reload the service if it is running and set the resource as updated" do + @current_resource.stub!(:running).and_return(true) + @provider.should_receive(:reload_service).and_return(true) + @provider.run_action(:reload) + @provider.new_resource.should be_updated + end + + it "should not reload the service if it's stopped" do + @current_resource.stub!(:running).and_return(false) + @provider.should_not_receive(:reload_service).and_return(true) + @provider.run_action(:stop) + @provider.new_resource.should_not be_updated + end + end + + it "delegates enable_service to subclasses" do + lambda { @provider.enable_service }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "delegates disable_service to subclasses" do + lambda { @provider.disable_service }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "delegates start_service to subclasses" do + lambda { @provider.start_service }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "delegates stop_service to subclasses" do + lambda { @provider.stop_service }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "delegates restart_service to subclasses" do + lambda { @provider.restart_service }.should raise_error(Chef::Exceptions::UnsupportedAction) + end + + it "delegates reload_service to subclasses" do + lambda { @provider.reload_service }.should raise_error(Chef::Exceptions::UnsupportedAction) + end +end diff --git a/spec/unit/provider/subversion_spec.rb b/spec/unit/provider/subversion_spec.rb new file mode 100644 index 0000000000..e441428d6c --- /dev/null +++ b/spec/unit/provider/subversion_spec.rb @@ -0,0 +1,281 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.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' + +describe Chef::Provider::Subversion do + + before do + @resource = Chef::Resource::Subversion.new("my app") + @resource.repository "http://svn.example.org/trunk/" + @resource.destination "/my/deploy/dir" + @resource.revision "12345" + @resource.svn_arguments(false) + @resource.svn_info_args(false) + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @provider = Chef::Provider::Subversion.new(@resource, @run_context) + end + + it "converts resource attributes to options for run_command and popen4" do + @provider.run_options.should == {} + @resource.user 'deployninja' + @provider.run_options.should == {:user => "deployninja"} + end + + context "determining the revision of the currently deployed code" do + + before do + @stdout = mock("stdout") + @stderr = mock("stderr") + @exitstatus = mock("exitstatus") + end + + it "sets the revision to nil if there isn't any deployed code yet" do + ::File.should_receive(:exist?).with("/my/deploy/dir/.svn").and_return(false) + @provider.find_current_revision.should be_nil + end + + it "determines the current revision if there's a checkout with svn data available" do + example_svn_info = "Path: .\n" + + "URL: http://svn.example.org/trunk/myapp\n" + + "Repository Root: http://svn.example.org\n" + + "Repository UUID: d62ff500-7bbc-012c-85f1-0026b0e37c24\n" + + "Revision: 11739\nNode Kind: directory\n" + + "Schedule: normal\n" + + "Last Changed Author: codeninja\n" + + "Last Changed Rev: 11410\n" + # Last Changed Rev is preferred to Revision + "Last Changed Date: 2009-03-25 06:09:56 -0600 (Wed, 25 Mar 2009)\n\n" + ::File.should_receive(:exist?).with("/my/deploy/dir/.svn").and_return(true) + ::File.should_receive(:directory?).with("/my/deploy/dir").and_return(true) + ::Dir.should_receive(:chdir).with("/my/deploy/dir").and_yield + @stdout.stub!(:string).and_return(example_svn_info) + @stderr.stub!(:string).and_return("") + @exitstatus.stub!(:exitstatus).and_return(0) + expected_command = ["svn info", {:cwd=>"/my/deploy/dir"}] + @provider.should_receive(:popen4).with(*expected_command). + and_yield("no-pid", "no-stdin", @stdout,@stderr). + and_return(@exitstatus) + @provider.find_current_revision.should eql("11410") + end + + it "gives nil as the current revision if the deploy dir isn't a SVN working copy" do + example_svn_info = "svn: '/tmp/deploydir' is not a working copy\n" + ::File.should_receive(:exist?).with("/my/deploy/dir/.svn").and_return(true) + ::File.should_receive(:directory?).with("/my/deploy/dir").and_return(true) + ::Dir.should_receive(:chdir).with("/my/deploy/dir").and_yield + @stdout.stub!(:string).and_return(example_svn_info) + @stderr.stub!(:string).and_return("") + @exitstatus.stub!(:exitstatus).and_return(1) + @provider.should_receive(:popen4).and_yield("no-pid", "no-stdin", @stdout,@stderr). + and_return(@exitstatus) + @provider.find_current_revision.should be_nil + end + + it "finds the current revision when loading the current resource state" do + # note: the test is kinda janky, but it provides regression coverage for CHEF-2092 + @resource.instance_variable_set(:@action, :sync) + @provider.should_receive(:find_current_revision).and_return("12345") + @provider.load_current_resource + @provider.current_resource.revision.should == "12345" + end + end + + it "creates the current_resource object and sets its revision to the current deployment's revision as long as we're not exporting" do + @provider.stub!(:find_current_revision).and_return("11410") + @provider.new_resource.instance_variable_set :@action, [:checkout] + @provider.load_current_resource + @provider.current_resource.name.should eql(@resource.name) + @provider.current_resource.revision.should eql("11410") + end + + context "resolving revisions to an integer" do + + before do + @stdout = mock("stdout") + @stderr = mock("stderr") + @resource.svn_info_args "--no-auth-cache" + end + + it "returns the revision number as is if it's already an integer" do + @provider.revision_int.should eql("12345") + end + + it "queries the server and resolves the revision if it's not an integer (i.e. 'HEAD')" do + example_svn_info = "Path: .\n" + + "URL: http://svn.example.org/trunk/myapp\n" + + "Repository Root: http://svn.example.org\n" + + "Repository UUID: d62ff500-7bbc-012c-85f1-0026b0e37c24\n" + + "Revision: 11739\nNode Kind: directory\n" + + "Schedule: normal\n" + + "Last Changed Author: codeninja\n" + + "Last Changed Rev: 11410\n" + # Last Changed Rev is preferred to Revision + "Last Changed Date: 2009-03-25 06:09:56 -0600 (Wed, 25 Mar 2009)\n\n" + exitstatus = mock("exitstatus") + exitstatus.stub!(:exitstatus).and_return(0) + @resource.revision "HEAD" + @stdout.stub!(:string).and_return(example_svn_info) + @stderr.stub!(:string).and_return("") + expected_command = ["svn info http://svn.example.org/trunk/ --no-auth-cache -rHEAD", {:cwd=>Dir.tmpdir}] + @provider.should_receive(:popen4).with(*expected_command). + and_yield("no-pid","no-stdin",@stdout,@stderr). + and_return(exitstatus) + @provider.revision_int.should eql("11410") + end + + it "returns a helpful message if data from `svn info` can't be parsed" do + example_svn_info = "some random text from an error message\n" + exitstatus = mock("exitstatus") + exitstatus.stub!(:exitstatus).and_return(0) + @resource.revision "HEAD" + @stdout.stub!(:string).and_return(example_svn_info) + @stderr.stub!(:string).and_return("") + @provider.should_receive(:popen4).and_yield("no-pid","no-stdin",@stdout,@stderr). + and_return(exitstatus) + lambda {@provider.revision_int}.should raise_error(RuntimeError, "Could not parse `svn info` data: some random text from an error message") + + end + + it "responds to :revision_slug as an alias for revision_sha" do + @provider.should respond_to(:revision_slug) + end + + end + + it "generates a checkout command with default options" do + @provider.checkout_command.should eql("svn checkout -q -r12345 http://svn.example.org/trunk/ /my/deploy/dir") + end + + it "generates a checkout command with authentication" do + @resource.svn_username "deployNinja" + @resource.svn_password "vanish!" + @provider.checkout_command.should eql("svn checkout -q --username deployNinja --password vanish! " + + "-r12345 http://svn.example.org/trunk/ /my/deploy/dir") + end + + it "generates a checkout command with arbitrary options" do + @resource.svn_arguments "--no-auth-cache" + @provider.checkout_command.should eql("svn checkout --no-auth-cache -q -r12345 "+ + "http://svn.example.org/trunk/ /my/deploy/dir") + end + + it "generates a sync command with default options" do + @provider.sync_command.should eql("svn update -q -r12345 /my/deploy/dir") + end + + it "generates an export command with default options" do + @provider.export_command.should eql("svn export --force -q -r12345 http://svn.example.org/trunk/ /my/deploy/dir") + end + + it "doesn't try to find the current revision when loading the resource if running an export" do + @provider.new_resource.instance_variable_set :@action, [:export] + @provider.should_not_receive(:find_current_revision) + @provider.load_current_resource + end + + it "doesn't try to find the current revision when loading the resource if running a force export" do + @provider.new_resource.instance_variable_set :@action, [:force_export] + @provider.should_not_receive(:find_current_revision) + @provider.load_current_resource + end + + it "runs an export with the --force option" do + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + expected_cmd = "svn export --force -q -r12345 http://svn.example.org/trunk/ /my/deploy/dir" + @provider.should_receive(:run_command).with(:command => expected_cmd) + @provider.run_action(:force_export) + @resource.should be_updated + end + + it "runs the checkout command for action_checkout" do + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + expected_cmd = "svn checkout -q -r12345 http://svn.example.org/trunk/ /my/deploy/dir" + @provider.should_receive(:run_command).with(:command => expected_cmd) + @provider.run_action(:checkout) + @resource.should be_updated + end + + it "raises an error if the svn checkout command would fail because the enclosing directory doesn't exist" do + lambda {@provider.run_action(:sync)}.should raise_error(Chef::Exceptions::MissingParentDirectory) + end + + it "should not checkout if the destination exists or is a non empty directory" do + ::File.stub!(:exist?).with("/my/deploy/dir/.svn").and_return(false) + ::File.stub!(:exist?).with("/my/deploy/dir").and_return(true) + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + ::Dir.stub!(:entries).with("/my/deploy/dir").and_return(['.','..','foo','bar']) + @provider.should_not_receive(:checkout_command) + @provider.run_action(:checkout) + @resource.should_not be_updated + end + + it "runs commands with the user and group specified in the resource" do + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + @resource.user "whois" + @resource.group "thisis" + expected_cmd = "svn checkout -q -r12345 http://svn.example.org/trunk/ /my/deploy/dir" + @provider.should_receive(:run_command).with(:command => expected_cmd, :user => "whois", :group => "thisis") + @provider.run_action(:checkout) + @resource.should be_updated + end + + it "does a checkout for action_sync if there's no deploy dir" do + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + ::File.should_receive(:exist?).with("/my/deploy/dir/.svn").twice.and_return(false) + @provider.should_receive(:action_checkout) + @provider.run_action(:sync) + end + + it "does a checkout for action_sync if the deploy dir exists but is empty" do + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + ::File.should_receive(:exist?).with("/my/deploy/dir/.svn").twice.and_return(false) + @provider.should_receive(:action_checkout) + @provider.run_action(:sync) + end + + it "runs the sync_command on action_sync if the deploy dir exists and isn't empty" do + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + ::File.should_receive(:exist?).with("/my/deploy/dir/.svn").and_return(true) + @provider.stub!(:find_current_revision).and_return("11410") + @provider.stub!(:current_revision_matches_target_revision?).and_return(false) + expected_cmd = "svn update -q -r12345 /my/deploy/dir" + @provider.should_receive(:run_command).with(:command => expected_cmd) + @provider.run_action(:sync) + @resource.should be_updated + end + + it "does not fetch any updates if the remote revision matches the current revision" do + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + ::File.should_receive(:exist?).with("/my/deploy/dir/.svn").and_return(true) + @provider.stub!(:find_current_revision).and_return('12345') + @provider.stub!(:current_revision_matches_target_revision?).and_return(true) + @provider.run_action(:sync) + @resource.should_not be_updated + end + + it "runs the export_command on action_export" do + ::File.stub!(:directory?).with("/my/deploy").and_return(true) + expected_cmd = "svn export --force -q -r12345 http://svn.example.org/trunk/ /my/deploy/dir" + @provider.should_receive(:run_command).with(:command => expected_cmd) + @provider.run_action(:export) + @resource.should be_updated + end + +end diff --git a/spec/unit/provider/template_spec.rb b/spec/unit/provider/template_spec.rb new file mode 100644 index 0000000000..6747876d61 --- /dev/null +++ b/spec/unit/provider/template_spec.rb @@ -0,0 +1,198 @@ +# +# 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 'stringio' +require 'spec_helper' +require 'etc' +require 'ostruct' + +describe Chef::Provider::Template do + before(:each) do + @cookbook_repo = File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks")) + Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest, @cookbook_repo) } + + @node = Chef::Node.new + cl = Chef::CookbookLoader.new(@cookbook_repo) + cl.load_cookbooks + @cookbook_collection = Chef::CookbookCollection.new(cl) + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, @cookbook_collection, @events) + + @rendered_file_location = Dir.tmpdir + '/openldap_stuff.conf' + + @resource = Chef::Resource::Template.new(@rendered_file_location) + @resource.cookbook_name = 'openldap' + + @provider = Chef::Provider::Template.new(@resource, @run_context) + @current_resource = @resource.dup + @provider.current_resource = @current_resource + @access_controls = mock("access controls") + @provider.stub!(:access_controls).and_return(@access_controls) + passwd_struct = if windows? + Struct::Passwd.new("root", "x", 0, 0, "/root", "/bin/bash") + else + Struct::Passwd.new("root", "x", 0, 0, "root", "/root", "/bin/bash") + end + group_struct = OpenStruct.new(:name => "root", :passwd => "x", :gid => 0) + Etc.stub!(:getpwuid).and_return(passwd_struct) + Etc.stub!(:getgrgid).and_return(group_struct) + end + + describe "when creating the template" do + + before do + + end + after do + FileUtils.rm(@rendered_file_location) if ::File.exist?(@rendered_file_location) + end + + it "finds the template file in the coobook cache if it isn't local" do + @provider.template_location.should == CHEF_SPEC_DATA + '/cookbooks/openldap/templates/default/openldap_stuff.conf.erb' + end + + it "finds the template file locally if it is local" do + @resource.local(true) + @resource.source('/tmp/its_on_disk.erb') + @provider.template_location.should == '/tmp/its_on_disk.erb' + end + + it "stops executing when the local template source can't be found" do + @access_controls.stub!(:requires_changes?).and_return(false) + @resource.source "invalid.erb" + @resource.local true + lambda { @provider.run_action(:create) } .should raise_error Chef::Mixin::WhyRun::ResourceRequirements::Assertion::AssertionFailure + end + + it "should use the cookbook name if defined in the template resource" do + @resource.cookbook_name = 'apache2' + @resource.cookbook('openldap') + @resource.source "test.erb" + @provider.template_location.should == CHEF_SPEC_DATA + '/cookbooks/openldap/templates/default/test.erb' + end + + describe "when the target file does not exist" do + it "creates the template with the rendered content" do + @access_controls.stub!(:requires_changes?).and_return(true) + @access_controls.should_receive(:set_all!) + @node.normal[:slappiness] = "a warm gun" + @provider.should_receive(:backup) + @provider.run_action(:create) + IO.read(@rendered_file_location).should == "slappiness is a warm gun" + @resource.should be_updated_by_last_action + end + + it "should set the file access control as specified in the resource" do + @access_controls.stub!(:requires_changes?).and_return(false) + @access_controls.should_receive(:set_all!) + @resource.owner("adam") + @resource.group("wheel") + @resource.mode(00644) + @provider.run_action(:create) + @resource.should be_updated_by_last_action + end + + it "creates the template with the rendered content for the create if missing action" do + @access_controls.stub!(:requires_changes?).and_return(true) + @access_controls.should_receive(:set_all!) + @node.normal[:slappiness] = "happiness" + @provider.should_receive(:backup) + @provider.run_action(:create_if_missing) + IO.read(@rendered_file_location).should == "slappiness is happiness" + @resource.should be_updated_by_last_action + end + end + + describe "when the target file has the wrong content" do + before do + File.open(@rendered_file_location, "w+") { |f| f.print "blargh" } + end + + it "overwrites the file with the updated content when the create action is run" do + @node.normal[:slappiness] = "a warm gun" + @access_controls.stub!(:requires_changes?).and_return(false) + @access_controls.should_receive(:set_all!) + @provider.should_receive(:backup) + @provider.run_action(:create) + IO.read(@rendered_file_location).should == "slappiness is a warm gun" + @resource.should be_updated_by_last_action + end + + it "should set the file access control as specified in the resource" do + @access_controls.stub!(:requires_changes?).and_return(true) + @access_controls.should_receive(:set_all!) + @resource.owner("adam") + @resource.group("wheel") + @resource.mode(00644) + @provider.should_receive(:backup) + @provider.run_action(:create) + @resource.should be_updated_by_last_action + end + + it "doesn't overwrite the file when the create if missing action is run" do + @access_controls.stub!(:requires_changes?).and_return(false) + @access_controls.should_not_receive(:set_all!) + @node.normal[:slappiness] = "a warm gun" + @provider.should_not_receive(:backup) + @provider.run_action(:create_if_missing) + IO.read(@rendered_file_location).should == "blargh" + @resource.should_not be_updated_by_last_action + end + end + + describe "when the target has the correct content" do + before do + Chef::ChecksumCache.instance.reset! + File.open(@rendered_file_location, "w") { |f| f.print "slappiness is a warm gun" } + @current_resource.checksum('4ff94a87794ed9aefe88e734df5a66fc8727a179e9496cbd88e3b5ec762a5ee9') + @access_controls = mock("access controls") + @provider.stub!(:access_controls).and_return(@access_controls) + end + + it "does not backup the original or overwrite it" do + @node.normal[:slappiness] = "a warm gun" + @access_controls.stub!(:requires_changes?).and_return(false) + @provider.should_not_receive(:backup) + FileUtils.should_not_receive(:mv) + @provider.run_action(:create) + @resource.should_not be_updated_by_last_action + end + + it "does not backup the original or overwrite it on create if missing" do + @node.normal[:slappiness] = "a warm gun" + @access_controls.stub!(:requires_changes?).and_return(false) + @provider.should_not_receive(:backup) + FileUtils.should_not_receive(:mv) + @provider.run_action(:create) + @resource.should_not be_updated_by_last_action + end + + it "sets the file access controls if they have diverged" do + @provider.stub!(:backup).and_return(true) + @access_controls.stub!(:requires_changes?).and_return(true) + @access_controls.should_receive(:set_all!) + @resource.owner("adam") + @resource.group("wheel") + @resource.mode(00644) + @provider.should_receive(:backup) + @provider.run_action(:create) + @resource.should be_updated_by_last_action + end + end + + end +end diff --git a/spec/unit/provider/user/dscl_spec.rb b/spec/unit/provider/user/dscl_spec.rb new file mode 100644 index 0000000000..3894cd61b4 --- /dev/null +++ b/spec/unit/provider/user/dscl_spec.rb @@ -0,0 +1,480 @@ +# +# Author:: Dreamcat4 (<dreamcat4@gmail.com>) +# Copyright:: Copyright (c) 2009 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. +# + +ShellCmdResult = Struct.new(:stdout, :stderr, :exitstatus) + +require 'spec_helper' +require 'ostruct' + +describe Chef::Provider::User::Dscl do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::User.new("toor") + @provider = Chef::Provider::User::Dscl.new(@new_resource, @run_context) + end + + describe "when shelling out to dscl" do + it "should run dscl with the supplied cmd /Path args" do + shell_return = ShellCmdResult.new('stdout', 'err', 0) + @provider.should_receive(:shell_out).with("dscl . -cmd /Path args").and_return(shell_return) + @provider.safe_dscl("cmd /Path args").should == 'stdout' + end + + it "returns an empty string from delete commands" do + shell_return = ShellCmdResult.new('out', 'err', 23) + @provider.should_receive(:shell_out).with("dscl . -delete /Path args").and_return(shell_return) + @provider.safe_dscl("delete /Path args").should == "" + end + + it "should raise an exception for any other command" do + shell_return = ShellCmdResult.new('out', 'err', 23) + @provider.should_receive(:shell_out).with('dscl . -cmd /Path arguments').and_return(shell_return) + lambda { @provider.safe_dscl("cmd /Path arguments") }.should raise_error(Chef::Exceptions::DsclCommandFailed) + end + + it "raises an exception when dscl reports 'no such key'" do + shell_return = ShellCmdResult.new("No such key: ", 'err', 23) + @provider.should_receive(:shell_out).with('dscl . -cmd /Path args').and_return(shell_return) + lambda { @provider.safe_dscl("cmd /Path args") }.should raise_error(Chef::Exceptions::DsclCommandFailed) + end + + it "raises an exception when dscl reports 'eDSRecordNotFound'" do + shell_return = ShellCmdResult.new("<dscl_cmd> DS Error: -14136 (eDSRecordNotFound)", 'err', -14136) + @provider.should_receive(:shell_out).with('dscl . -cmd /Path args').and_return(shell_return) + lambda { @provider.safe_dscl("cmd /Path args") }.should raise_error(Chef::Exceptions::DsclCommandFailed) + end + end + + describe "get_free_uid" do + before do + @provider.stub!(:safe_dscl).and_return("\nwheel 200\nstaff 201\n") + end + + it "should run safe_dscl with list /Users uid" do + @provider.should_receive(:safe_dscl).with("list /Users uid") + @provider.get_free_uid + end + + it "should return the first unused uid number on or above 200" do + @provider.get_free_uid.should == 202 + end + + it "should raise an exception when the search limit is exhausted" do + search_limit = 1 + lambda { @provider.get_free_uid(search_limit) }.should raise_error(RuntimeError) + end + end + + describe "uid_used?" do + before do + @provider.stub!(:safe_dscl).and_return("\naj 500\n") + end + + it "should run safe_dscl with list /Users uid" do + @provider.should_receive(:safe_dscl).with("list /Users uid") + @provider.uid_used?(500) + end + + it "should return true for a used uid number" do + @provider.uid_used?(500).should be_true + end + + it "should return false for an unused uid number" do + @provider.uid_used?(501).should be_false + end + + it "should return false if not given any valid uid number" do + @provider.uid_used?(nil).should be_false + end + end + + describe "when determining the uid to set" do + it "raises RequestedUIDUnavailable if the requested uid is already in use" do + @provider.stub!(:uid_used?).and_return(true) + @provider.should_receive(:get_free_uid).and_return(501) + lambda { @provider.set_uid }.should raise_error(Chef::Exceptions::RequestedUIDUnavailable) + end + + it "finds a valid, unused uid when none is specified" do + @provider.should_receive(:safe_dscl).with("list /Users uid").and_return('') + @provider.should_receive(:safe_dscl).with("create /Users/toor UniqueID 501") + @provider.should_receive(:get_free_uid).and_return(501) + @provider.set_uid + @new_resource.uid.should == 501 + end + + it "sets the uid specified in the resource" do + @new_resource.uid(1000) + @provider.should_receive(:safe_dscl).with("create /Users/toor UniqueID 1000").and_return(true) + @provider.should_receive(:safe_dscl).with("list /Users uid").and_return('') + @provider.set_uid + end + end + + describe "when modifying the home directory" do + before do + @new_resource.supports({ :manage_home => true }) + @new_resource.home('/Users/toor') + + @current_resource = @new_resource.dup + @provider.current_resource = @current_resource + end + + it "deletes the home directory when resource#home is nil" do + @new_resource.instance_variable_set(:@home, nil) + @provider.should_receive(:safe_dscl).with("delete /Users/toor NFSHomeDirectory").and_return(true) + @provider.modify_home + end + + + it "raises InvalidHomeDirectory when the resource's home directory doesn't look right" do + @new_resource.home('epic-fail') + lambda { @provider.modify_home }.should raise_error(Chef::Exceptions::InvalidHomeDirectory) + end + + it "moves the users home to the new location if it exists and the target location is different" do + @new_resource.supports(:manage_home => true) + + current_home = CHEF_SPEC_DATA + '/old_home_dir' + current_home_files = [current_home + '/my-dot-emacs', current_home + '/my-dot-vim'] + @current_resource.home(current_home) + @new_resource.gid(23) + ::File.stub!(:exists?).with('/old/home/toor').and_return(true) + ::File.stub!(:exists?).with('/Users/toor').and_return(true) + + FileUtils.should_receive(:mkdir_p).with('/Users/toor').and_return(true) + FileUtils.should_receive(:rmdir).with(current_home) + ::Dir.should_receive(:glob).with("#{CHEF_SPEC_DATA}/old_home_dir/*",::File::FNM_DOTMATCH).and_return(current_home_files) + FileUtils.should_receive(:mv).with(current_home_files, "/Users/toor", :force => true) + FileUtils.should_receive(:chown_R).with('toor','23','/Users/toor') + + @provider.should_receive(:safe_dscl).with("create /Users/toor NFSHomeDirectory '/Users/toor'") + @provider.modify_home + end + + it "should raise an exception when the systems user template dir (skel) cannot be found" do + ::File.stub!(:exists?).and_return(false,false,false) + lambda { @provider.modify_home }.should raise_error(Chef::Exceptions::User) + end + + it "should run ditto to copy any missing files from skel to the new home dir" do + ::File.should_receive(:exists?).with("/System/Library/User\ Template/English.lproj").and_return(true) + FileUtils.should_receive(:chown_R).with('toor', '', '/Users/toor') + @provider.should_receive(:shell_out!).with("ditto '/System/Library/User Template/English.lproj' '/Users/toor'") + @provider.ditto_home + end + + it "creates the user's NFSHomeDirectory and home directory" do + @provider.should_receive(:safe_dscl).with("create /Users/toor NFSHomeDirectory '/Users/toor'").and_return(true) + @provider.should_receive(:ditto_home) + @provider.modify_home + end + end + + describe "osx_shadow_hash?" do + it "should return true when the string is a shadow hash" do + @provider.osx_shadow_hash?("0"*8*155).should eql(true) + end + + it "should return false otherwise" do + @provider.osx_shadow_hash?("any other string").should eql(false) + end + end + + describe "when detecting the format of a password" do + it "detects a OS X salted sha1" do + @provider.osx_salted_sha1?("0"*48).should eql(true) + @provider.osx_salted_sha1?("any other string").should eql(false) + end + end + + describe "guid" do + it "should run safe_dscl with read /Users/user GeneratedUID to get the users GUID" do + expected_uuid = "b398449e-cee0-45e0-80f8-b0b5b1bfdeaa" + @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(expected_uuid + "\n") + @provider.guid.should == expected_uuid + end + end + + describe "shadow_hash_set?" do + + it "should run safe_dscl with read /Users/user to see if the AuthenticationAuthority key exists" do + @provider.should_receive(:safe_dscl).with("read /Users/toor") + @provider.shadow_hash_set? + end + + describe "when the user account has an AuthenticationAuthority key" do + it "uses the shadow hash when there is a ShadowHash field in the AuthenticationAuthority key" do + @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: ;ShadowHash;\n") + @provider.shadow_hash_set?.should be_true + end + + it "does not use the shadow hash when there is no ShadowHash field in the AuthenticationAuthority key" do + @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: \n") + @provider.shadow_hash_set?.should be_false + end + + end + + describe "with no AuthenticationAuthority key in the user account" do + it "does not use the shadow hash" do + @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("") + @provider.shadow_hash_set?.should eql(false) + end + end + end + + describe "when setting or modifying the user password" do + before do + @new_resource.password("password") + @output = StringIO.new + end + + describe "when using a salted sha1 for the password" do + before do + @new_resource.password("F"*48) + end + + it "should write a shadow hash file with the expected salted sha1" do + uuid = "B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA" + File.should_receive(:open).with('/var/db/shadow/hash/B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA', "w", 384).and_yield(@output) + @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(uuid) + @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: ;ShadowHash;\n") + expected_salted_sha1 = @new_resource.password + expected_shadow_hash = "00000000"*155 + expected_shadow_hash[168] = expected_salted_sha1 + @provider.modify_password + @output.string.strip.should == expected_shadow_hash + end + end + + describe "when given a shadow hash file for the password" do + it "should write the shadow hash file directly to /var/db/shadow/hash/GUID" do + shadow_hash = '0123456789ABCDE0123456789ABCDEF' * 40 + raise 'oops' unless shadow_hash.size == 1240 + @new_resource.password shadow_hash + uuid = "B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA" + File.should_receive(:open).with('/var/db/shadow/hash/B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA', "w", 384).and_yield(@output) + @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(uuid) + @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: ;ShadowHash;\n") + @provider.modify_password + @output.string.strip.should == shadow_hash + end + end + + describe "when given a string for the password" do + it "should output a salted sha1 and shadow hash file from the specified password" do + uuid = "B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA" + File.should_receive(:open).with('/var/db/shadow/hash/B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA', "w", 384).and_yield(@output) + @new_resource.password("password") + OpenSSL::Random.stub!(:random_bytes).and_return("\377\377\377\377\377\377\377\377") + expected_salted_sha1 = "F"*8+"SHA1-"*8 + expected_shadow_hash = "00000000"*155 + expected_shadow_hash[168] = expected_salted_sha1 + @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(uuid) + @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: ;ShadowHash;\n") + @provider.modify_password + @output.string.strip.should match(/^0{168}(FFFFFFFF1C1AA7935D4E1190AFEC92343F31F7671FBF126D)0{1071}$/) + end + end + + it "should write the output directly to the shadow hash file at /var/db/shadow/hash/GUID" do + shadow_file = StringIO.new + uuid = "B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA" + File.should_receive(:open).with("/var/db/shadow/hash/B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA",'w',0600).and_yield(shadow_file) + @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(uuid) + @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: ;ShadowHash;\n") + @provider.modify_password + shadow_file.string.should match(/^0{168}[0-9A-F]{48}0{1071}$/) + end + + it "should run safe_dscl append /Users/user AuthenticationAuthority ;ShadowHash; when no shadow hash set" do + shadow_file = StringIO.new + uuid = "B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA" + File.should_receive(:open).with("/var/db/shadow/hash/B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA",'w',0600).and_yield(shadow_file) + @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(uuid) + @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority:\n") + @provider.should_receive(:safe_dscl).with("append /Users/toor AuthenticationAuthority ';ShadowHash;'") + @provider.modify_password + shadow_file.string.should match(/^0{168}[0-9A-F]{48}0{1071}$/) + end + end + + describe "load_current_resource" do + it "should raise an error if the required binary /usr/bin/dscl doesn't exist" do + ::File.should_receive(:exists?).with("/usr/bin/dscl").and_return(false) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::User) + end + + it "shouldn't raise an error if /usr/bin/dscl exists" do + ::File.stub!(:exists?).and_return(true) + lambda { @provider.load_current_resource }.should_not raise_error(Chef::Exceptions::User) + end + end + + describe "when the user does not yet exist and chef is creating it" do + context "with a numeric gid" do + before do + @new_resource.comment "#mockssuck" + @new_resource.gid 1001 + end + + it "creates the user, comment field, sets uid, gid, configures the home directory, sets the shell, and sets the password" do + @provider.should_receive :dscl_create_user + @provider.should_receive :dscl_create_comment + @provider.should_receive :set_uid + @provider.should_receive :dscl_set_gid + @provider.should_receive :modify_home + @provider.should_receive :dscl_set_shell + @provider.should_receive :modify_password + @provider.create_user + end + + it "creates the user and sets the comment field" do + @provider.should_receive(:safe_dscl).with("create /Users/toor").and_return(true) + @provider.dscl_create_user + end + + it "sets the comment field" do + @provider.should_receive(:safe_dscl).with("create /Users/toor RealName '#mockssuck'").and_return(true) + @provider.dscl_create_comment + end + + it "should run safe_dscl with create /Users/user PrimaryGroupID to set the users primary group" do + @provider.should_receive(:safe_dscl).with("create /Users/toor PrimaryGroupID '1001'").and_return(true) + @provider.dscl_set_gid + end + + it "should run safe_dscl with create /Users/user UserShell to set the users login shell" do + @provider.should_receive(:safe_dscl).with("create /Users/toor UserShell '/usr/bin/false'").and_return(true) + @provider.dscl_set_shell + end + end + + context "with a non-numeric gid" do + before do + @new_resource.comment "#mockssuck" + @new_resource.gid "newgroup" + end + + it "should map the group name to a numeric ID when the group exists" do + @provider.should_receive(:safe_dscl).with("read /Groups/newgroup PrimaryGroupID").ordered.and_return("PrimaryGroupID: 1001\n") + @provider.should_receive(:safe_dscl).with("create /Users/toor PrimaryGroupID '1001'").ordered.and_return(true) + @provider.dscl_set_gid + end + + it "should raise an exception when the group does not exist" do + shell_return = ShellCmdResult.new("<dscl_cmd> DS Error: -14136 (eDSRecordNotFound)", 'err', -14136) + @provider.should_receive(:shell_out).with('dscl . -read /Groups/newgroup PrimaryGroupID').and_return(shell_return) + lambda { @provider.dscl_set_gid }.should raise_error(Chef::Exceptions::GroupIDNotFound) + end + end + end + + describe "when the user exists and chef is managing it" do + before do + @current_resource = @new_resource.dup + @provider.current_resource = @current_resource + + # These are all different from @current_resource + @new_resource.username "mud" + @new_resource.uid 2342 + @new_resource.gid 2342 + @new_resource.home '/Users/death' + @new_resource.password 'goaway' + end + + it "sets the user, comment field, uid, gid, moves the home directory, sets the shell, and sets the password" do + @provider.should_receive :dscl_create_user + @provider.should_receive :dscl_create_comment + @provider.should_receive :set_uid + @provider.should_receive :dscl_set_gid + @provider.should_receive :modify_home + @provider.should_receive :dscl_set_shell + @provider.should_receive :modify_password + @provider.create_user + end + end + + describe "when changing the gid" do + before do + @current_resource = @new_resource.dup + @provider.current_resource = @current_resource + + # This is different from @current_resource + @new_resource.gid 2342 + end + + it "sets the gid" do + @provider.should_receive :dscl_set_gid + @provider.manage_user + end + end + + describe "when the user exists and chef is removing it" do + it "removes the user's home directory when the resource is configured to manage home" do + @new_resource.supports({ :manage_home => true }) + @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("NFSHomeDirectory: /Users/fuuuuuuuuuuuuu") + @provider.should_receive(:safe_dscl).with("delete /Users/toor") + FileUtils.should_receive(:rm_rf).with("/Users/fuuuuuuuuuuuuu") + @provider.remove_user + end + + it "removes the user from any group memberships" do + Etc.stub(:group).and_yield(OpenStruct.new(:name => 'ragefisters', :mem => 'toor')) + @provider.should_receive(:safe_dscl).with("delete /Users/toor") + @provider.should_receive(:safe_dscl).with("delete /Groups/ragefisters GroupMembership 'toor'") + @provider.remove_user + end + end + + describe "when discovering if a user is locked" do + + it "determines the user is not locked when dscl shows an AuthenticationAuthority without a DisabledUser field" do + @provider.should_receive(:safe_dscl).with("read /Users/toor") + @provider.should_not be_locked + end + + it "determines the user is locked when dscl shows an AuthenticationAuthority with a DisabledUser field" do + @provider.should_receive(:safe_dscl).with('read /Users/toor').and_return("\nAuthenticationAuthority: ;DisabledUser;\n") + @provider.should be_locked + end + + it "determines the user is not locked when dscl shows no AuthenticationAuthority" do + @provider.should_receive(:safe_dscl).with('read /Users/toor').and_return("\n") + @provider.should_not be_locked + end + end + + describe "when locking the user" do + it "should run safe_dscl with append /Users/user AuthenticationAuthority ;DisabledUser; to lock the user account" do + @provider.should_receive(:safe_dscl).with("append /Users/toor AuthenticationAuthority ';DisabledUser;'") + @provider.lock_user + end + end + + describe "when unlocking the user" do + it "removes DisabledUser from the authentication string" do + @provider.should_receive(:safe_dscl).with("read /Users/toor AuthenticationAuthority").and_return("\nAuthenticationAuthority: ;ShadowHash; ;DisabledUser;\n") + @provider.should_receive(:safe_dscl).with("create /Users/toor AuthenticationAuthority ';ShadowHash;'") + @provider.unlock_user + end + end +end diff --git a/spec/unit/provider/user/pw_spec.rb b/spec/unit/provider/user/pw_spec.rb new file mode 100644 index 0000000000..b7503ea15f --- /dev/null +++ b/spec/unit/provider/user/pw_spec.rb @@ -0,0 +1,235 @@ +# +# Author:: Stephen Haynes (<sh@nomitor.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' + +describe Chef::Provider::User::Pw do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::User.new("adam") + @new_resource.comment "Adam Jacob" + @new_resource.uid 1000 + @new_resource.gid 1000 + @new_resource.home "/home/adam" + @new_resource.shell "/usr/bin/zsh" + @new_resource.password "abracadabra" + + @new_resource.supports :manage_home => true + + @current_resource = Chef::Resource::User.new("adam") + @current_resource.comment "Adam Jacob" + @current_resource.uid 1000 + @current_resource.gid 1000 + @current_resource.home "/home/adam" + @current_resource.shell "/usr/bin/zsh" + @current_resource.password "abracadabra" + + @provider = Chef::Provider::User::Pw.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + end + + describe "setting options to the pw command" do + field_list = { + 'comment' => "-c", + 'home' => "-d", + 'gid' => "-g", + 'uid' => "-u", + 'shell' => "-s" + } + field_list.each do |attribute, option| + it "should check for differences in #{attribute} between the new and current resources" do + @current_resource.should_receive(attribute) + @new_resource.should_receive(attribute) + @provider.set_options + end + + it "should set the option for #{attribute} if the new resources #{attribute} is not null" do + @new_resource.stub!(attribute).and_return("hola") + @provider.set_options.should eql(" #{@new_resource.username} #{option} '#{@new_resource.send(attribute)}' -m") + end + + it "should set the option for #{attribute} if the new resources #{attribute} is not null, without homedir management" do + @new_resource.stub!(:supports).and_return({:manage_home => false}) + @new_resource.stub!(attribute).and_return("hola") + @provider.set_options.should eql(" #{@new_resource.username} #{option} '#{@new_resource.send(attribute)}'") + end + end + + it "should combine all the possible options" do + match_string = " adam" + field_list.sort{ |a,b| a[0] <=> b[0] }.each do |attribute, option| + @new_resource.stub!(attribute).and_return("hola") + match_string << " #{option} 'hola'" + end + match_string << " -m" + @provider.set_options.should eql(match_string) + end + end + + describe "create_user" do + before(:each) do + @provider.stub!(:run_command).and_return(true) + @provider.stub!(:modify_password).and_return(true) + end + + it "should run pw useradd with the return of set_options" do + @provider.should_receive(:run_command).with({ :command => "pw useradd adam -m" }).and_return(true) + @provider.create_user + end + + it "should modify the password" do + @provider.should_receive(:modify_password).and_return(true) + @provider.create_user + end + end + + describe "manage_user" do + before(:each) do + @provider.stub!(:run_command).and_return(true) + @provider.stub!(:modify_password).and_return(true) + end + + it "should run pw usermod with the return of set_options" do + @provider.should_receive(:run_command).with({ :command => "pw usermod adam -m" }).and_return(true) + @provider.manage_user + end + + it "should modify the password" do + @provider.should_receive(:modify_password).and_return(true) + @provider.create_user + end + end + + describe "remove_user" do + it "should run pw userdel with the new resources user name" do + @new_resource.supports :manage_home => false + @provider.should_receive(:run_command).with({ :command => "pw userdel #{@new_resource.username}" }).and_return(true) + @provider.remove_user + end + + it "should run pw userdel with the new resources user name and -r if manage_home is true" do + @provider.should_receive(:run_command).with({ :command => "pw userdel #{@new_resource.username} -r"}).and_return(true) + @provider.remove_user + end + end + + describe "determining if the user is locked" do + it "should return true if user is locked" do + @current_resource.stub!(:password).and_return("*LOCKED*abracadabra") + @provider.check_lock.should eql(true) + end + + it "should return false if user is not locked" do + @current_resource.stub!(:password).and_return("abracadabra") + @provider.check_lock.should eql(false) + end + end + + describe "when locking the user" do + it "should run pw lock with the new resources username" do + @provider.should_receive(:run_command).with({ :command => "pw lock #{@new_resource.username}"}) + @provider.lock_user + end + end + + describe "when unlocking the user" do + it "should run pw unlock with the new resources username" do + @provider.should_receive(:run_command).with({ :command => "pw unlock #{@new_resource.username}"}) + @provider.unlock_user + end + end + + describe "when modifying the password" do + before(:each) do + @status = mock("Status", :exitstatus => 0) + @provider.stub!(:popen4).and_return(@status) + @pid, @stdin, @stdout, @stderr = nil, nil, nil, nil + end + + it "should check for differences in password between the new and current resources" do + @current_resource.should_receive(:password) + @new_resource.should_receive(:password) + @provider.modify_password + end + + describe "and the passwords are identical" do + before(:each) do + @new_resource.stub!(:password).and_return("abracadabra") + @current_resource.stub!(:password).and_return("abracadabra") + end + + it "logs an appropriate message" do + Chef::Log.should_receive(:debug).with("user[adam] no change needed to password") + @provider.modify_password + end + end + + describe "and the passwords are different" do + before(:each) do + @new_resource.stub!(:password).and_return("abracadabra") + @current_resource.stub!(:password).and_return("sesame") + end + + it "should log an appropriate message" do + Chef::Log.should_receive(:debug).with("user[adam] updating password") + @provider.modify_password + end + + it "should run pw usermod with the username and the option -H 0" do + @provider.should_receive(:popen4).with("pw usermod adam -H 0", :waitlast => true).and_return(@status) + @provider.modify_password + end + + it "should send the new password to the stdin of pw usermod" do + @stdin = StringIO.new + @provider.stub!(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.modify_password + @stdin.string.should == "abracadabra\n" + end + + it "should raise an exception if pw usermod fails" do + @status.should_receive(:exitstatus).and_return(1) + lambda { @provider.modify_password }.should raise_error(Chef::Exceptions::User) + end + + it "should not raise an exception if pw usermod succeeds" do + @status.should_receive(:exitstatus).and_return(0) + lambda { @provider.modify_password }.should_not raise_error(Chef::Exceptions::User) + end + end + end + + describe "when loading the current state" do + before do + @provider.new_resource = Chef::Resource::User.new("adam") + end + + it "should raise an error if the required binary /usr/sbin/pw doesn't exist" do + File.should_receive(:exists?).with("/usr/sbin/pw").and_return(false) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::User) + end + + it "shouldn't raise an error if /usr/sbin/pw exists" do + File.stub!(:exists?).and_return(true) + lambda { @provider.load_current_resource }.should_not raise_error(Chef::Exceptions::User) + end + end +end diff --git a/spec/unit/provider/user/useradd_spec.rb b/spec/unit/provider/user/useradd_spec.rb new file mode 100644 index 0000000000..ea6caf6e0a --- /dev/null +++ b/spec/unit/provider/user/useradd_spec.rb @@ -0,0 +1,386 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2008, 2010 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::Provider::User::Useradd do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::User.new("adam", @run_context) + @new_resource.comment "Adam Jacob" + @new_resource.uid 1000 + @new_resource.gid 1000 + @new_resource.home "/home/adam" + @new_resource.shell "/usr/bin/zsh" + @new_resource.password "abracadabra" + @new_resource.system false + @new_resource.manage_home false + @new_resource.non_unique false + @current_resource = Chef::Resource::User.new("adam", @run_context) + @current_resource.comment "Adam Jacob" + @current_resource.uid 1000 + @current_resource.gid 1000 + @current_resource.home "/home/adam" + @current_resource.shell "/usr/bin/zsh" + @current_resource.password "abracadabra" + @current_resource.system false + @current_resource.manage_home false + @current_resource.non_unique false + @current_resource.supports({:manage_home => false, :non_unique => false}) + @provider = Chef::Provider::User::Useradd.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + end + + describe "when setting option" do + field_list = { + 'comment' => "-c", + 'gid' => "-g", + 'uid' => "-u", + 'shell' => "-s", + 'password' => "-p" + } + + field_list.each do |attribute, option| + it "should check for differences in #{attribute} between the new and current resources" do + @current_resource.should_receive(attribute) + @new_resource.should_receive(attribute) + @provider.universal_options + end + + it "should set the option for #{attribute} if the new resources #{attribute} is not nil" do + @new_resource.stub!(attribute).and_return("hola") + @provider.universal_options.should eql(" #{option} 'hola'") + end + + it "should set the option for #{attribute} if the new resources #{attribute} is not nil, without homedir management" do + @new_resource.stub!(:supports).and_return({:manage_home => false, + :non_unique => false}) + @new_resource.stub!(attribute).and_return("hola") + @provider.universal_options.should eql(" #{option} 'hola'") + end + + it "should set the option for #{attribute} if the new resources #{attribute} is not nil, without homedir management (using real attributes)" do + @new_resource.stub!(:manage_home).and_return(false) + @new_resource.stub!(:non_unique).and_return(false) + @new_resource.stub!(attribute).and_return("hola") + @provider.universal_options.should eql(" #{option} 'hola'") + end + end + + it "should combine all the possible options" do + match_string = "" + field_list.sort{ |a,b| a[0] <=> b[0] }.each do |attribute, option| + @new_resource.stub!(attribute).and_return("hola") + match_string << " #{option} 'hola'" + end + @provider.universal_options.should eql(match_string) + end + + describe "when we want to create a system user" do + before do + @new_resource.manage_home(true) + @new_resource.non_unique(false) + end + + it "should set useradd -r" do + @new_resource.system(true) + @provider.useradd_options.should == " -r" + end + end + + describe "when the resource has a different home directory and supports home directory management" do + before do + @new_resource.stub!(:home).and_return("/wowaweea") + @new_resource.stub!(:supports).and_return({:manage_home => true, + :non_unique => false}) + end + + it "should set -m -d /homedir" do + @provider.universal_options.should == " -m -d '/wowaweea'" + @provider.useradd_options.should == "" + end + end + + describe "when the resource has a different home directory and supports home directory management (using real attributes)" do + before do + @new_resource.stub!(:home).and_return("/wowaweea") + @new_resource.stub!(:manage_home).and_return(true) + @new_resource.stub!(:non_unique).and_return(false) + end + + it "should set -m -d /homedir" do + @provider.universal_options.should eql(" -m -d '/wowaweea'") + @provider.useradd_options.should == "" + end + end + + describe "when the resource supports non_unique ids" do + before do + @new_resource.stub!(:supports).and_return({:manage_home => false, + :non_unique => true}) + end + + it "should set -m -o" do + @provider.universal_options.should eql(" -o") + end + end + + describe "when the resource supports non_unique ids (using real attributes)" do + before do + @new_resource.stub!(:manage_home).and_return(false) + @new_resource.stub!(:non_unique).and_return(true) + end + + it "should set -m -o" do + @provider.universal_options.should eql(" -o") + end + end + end + + describe "when creating a user" do + before(:each) do + @current_resource = Chef::Resource::User.new(@new_resource.name, @run_context) + @current_resource.username(@new_resource.username) + @provider.current_resource = @current_resource + @provider.new_resource.manage_home true + @provider.new_resource.home "/Users/mud" + @provider.new_resource.gid '23' + end + + it "runs useradd with the computed command options" do + command = "useradd -c 'Adam Jacob' -g '23' -p 'abracadabra' -s '/usr/bin/zsh' -u '1000' -m -d '/Users/mud' adam" + @provider.should_receive(:run_command).with({ :command => command }).and_return(true) + @provider.create_user + end + + describe "and home is not specified for new system user resource" do + + before do + @provider.new_resource.system true + # there is no public API to set attribute's value to nil + @provider.new_resource.instance_variable_set("@home", nil) + end + + it "should not include -m or -d in the command options" do + command = "useradd -c 'Adam Jacob' -g '23' -p 'abracadabra' -s '/usr/bin/zsh' -u '1000' -r adam" + @provider.should_receive(:run_command).with({ :command => command }).and_return(true) + @provider.create_user + end + + end + + end + + describe "when managing a user" do + before(:each) do + @provider.new_resource.manage_home true + @provider.new_resource.home "/Users/mud" + @provider.new_resource.gid '23' + end + + # CHEF-3423, -m must come before the username + it "runs usermod with the computed command options" do + @provider.should_receive(:run_command).with({ :command => "usermod -g '23' -m -d '/Users/mud' adam" }).and_return(true) + @provider.manage_user + end + + it "does not set the -r option to usermod" do + @new_resource.system(true) + @provider.should_receive(:run_command).with({ :command => "usermod -g '23' -m -d '/Users/mud' adam" }).and_return(true) + @provider.manage_user + end + + it "CHEF-3429: does not set -m if we aren't changing the home directory" do + @provider.should_receive(:updating_home?).and_return(false) + @provider.should_receive(:run_command).with({ :command => "usermod -g '23' adam" }).and_return(true) + @provider.manage_user + end + end + + describe "when removing a user" do + + it "should run userdel with the new resources user name" do + @provider.should_receive(:run_command).with({ :command => "userdel #{@new_resource.username}" }).and_return(true) + @provider.remove_user + end + + it "should run userdel with the new resources user name and -r if manage_home is true" do + @new_resource.stub!(:supports).and_return({ :manage_home => true, + :non_unique => false}) + @provider.should_receive(:run_command).with({ :command => "userdel -r #{@new_resource.username}"}).and_return(true) + @provider.remove_user + end + + it "should run userdel with the new resources user name if non_unique is true" do + @new_resource.stub!(:supports).and_return({ :manage_home => false, + :non_unique => true}) + @provider.should_receive(:run_command).with({ :command => "userdel #{@new_resource.username}"}).and_return(true) + @provider.remove_user + end + end + + describe "when checking the lock" do + before(:each) do + # @node = Chef::Node.new + # @new_resource = mock("Chef::Resource::User", + # :nil_object => true, + # :username => "adam" + # ) + @status = mock("Status", :exitstatus => 0) + #@provider = Chef::Provider::User::Useradd.new(@node, @new_resource) + @provider.stub!(:popen4).and_return(@status) + @stdin = mock("STDIN", :nil_object => true) + @stdout = mock("STDOUT", :nil_object => true) + @stdout.stub!(:gets).and_return("root P 09/02/2008 0 99999 7 -1") + @stderr = mock("STDERR", :nil_object => true) + @pid = mock("PID", :nil_object => true) + end + + it "should call passwd -S to check the lock status" do + @provider.should_receive(:popen4).with("passwd -S #{@new_resource.username}").and_return(@status) + @provider.check_lock + end + + it "should get the first line of passwd -S STDOUT" do + @provider.should_receive(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @stdout.should_receive(:gets).and_return("root P 09/02/2008 0 99999 7 -1") + @provider.check_lock + end + + it "should return false if status begins with P" do + @provider.should_receive(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.check_lock.should eql(false) + end + + it "should return false if status begins with N" do + @stdout.stub!(:gets).and_return("root N") + @provider.should_receive(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.check_lock.should eql(false) + end + + it "should return true if status begins with L" do + @stdout.stub!(:gets).and_return("root L") + @provider.should_receive(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.check_lock.should eql(true) + end + + it "should raise a Chef::Exceptions::User if passwd -S fails on anything other than redhat/centos" do + @node.automatic_attrs[:platform] = 'ubuntu' + @status.should_receive(:exitstatus).and_return(1) + lambda { @provider.check_lock }.should raise_error(Chef::Exceptions::User) + end + + ['redhat', 'centos'].each do |os| + it "should not raise a Chef::Exceptions::User if passwd -S exits with 1 on #{os} and the passwd package is version 0.73-1" do + @node.automatic_attrs[:platform] = os + @stdout.stub!(:gets).and_return("passwd-0.73-1\n") + @status.should_receive(:exitstatus).twice.and_return(1) + @provider.should_receive(:popen4).with("passwd -S #{@new_resource.username}") + @provider.should_receive(:popen4).with("rpm -q passwd").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + lambda { @provider.check_lock }.should_not raise_error(Chef::Exceptions::User) + end + + it "should raise a Chef::Exceptions::User if passwd -S exits with 1 on #{os} and the passwd package is not version 0.73-1" do + @node.automatic_attrs[:platform] = os + @stdout.stub!(:gets).and_return("passwd-0.73-2\n") + @status.should_receive(:exitstatus).twice.and_return(1) + @provider.should_receive(:popen4).with("passwd -S #{@new_resource.username}") + @provider.should_receive(:popen4).with("rpm -q passwd").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + lambda { @provider.check_lock }.should raise_error(Chef::Exceptions::User) + end + + it "should raise a Chef::Exceptions::User if passwd -S exits with something other than 0 or 1 on #{os}" do + @node.automatic_attrs[:platform] = os + @status.should_receive(:exitstatus).twice.and_return(2) + lambda { @provider.check_lock }.should raise_error(Chef::Exceptions::User) + end + end + end + + describe "when locking the user" do + it "should run usermod -L with the new resources username" do + @provider.should_receive(:run_command).with({ :command => "usermod -L #{@new_resource.username}"}) + @provider.lock_user + end + end + + describe "when unlocking the user" do + it "should run usermod -L with the new resources username" do + @provider.should_receive(:run_command).with({ :command => "usermod -U #{@new_resource.username}"}) + @provider.unlock_user + end + end + + describe "when checking if home needs updating" do + [ + { + "action" => "should return false if home matches", + "current_resource_home" => [ "/home/laurent" ], + "new_resource_home" => [ "/home/laurent" ], + "expected_result" => false + }, + { + "action" => "should return true if home doesn't match", + "current_resource_home" => [ "/home/laurent" ], + "new_resource_home" => [ "/something/else" ], + "expected_result" => true + }, + { + "action" => "should return false if home only differs by trailing slash", + "current_resource_home" => [ "/home/laurent" ], + "new_resource_home" => [ "/home/laurent/", "/home/laurent" ], + "expected_result" => false + }, + { + "action" => "should return false if home is an equivalent path", + "current_resource_home" => [ "/home/laurent" ], + "new_resource_home" => [ "/home/./laurent", "/home/laurent" ], + "expected_result" => false + }, + ].each do |home_check| + it home_check["action"] do + @provider.current_resource.home home_check["current_resource_home"].first + @current_home_mock = mock("Pathname") + @provider.new_resource.home home_check["new_resource_home"].first + @new_home_mock = mock("Pathname") + + Pathname.should_receive(:new).with(@current_resource.home).and_return(@current_home_mock) + @current_home_mock.should_receive(:cleanpath).and_return(home_check["current_resource_home"].last) + Pathname.should_receive(:new).with(@new_resource.home).and_return(@new_home_mock) + @new_home_mock.should_receive(:cleanpath).and_return(home_check["new_resource_home"].last) + + @provider.updating_home?.should == home_check["expected_result"] + end + end + it "should return true if the current home does not exist but a home is specified by the new resource" do + @new_resource = Chef::Resource::User.new("adam", @run_context) + @current_resource = Chef::Resource::User.new("adam", @run_context) + @provider = Chef::Provider::User::Useradd.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + @current_resource.home nil + @new_resource.home "/home/kitten" + + @provider.updating_home?.should == true + end + end +end diff --git a/spec/unit/provider/user/windows_spec.rb b/spec/unit/provider/user/windows_spec.rb new file mode 100644 index 0000000000..6ede11b28c --- /dev/null +++ b/spec/unit/provider/user/windows_spec.rb @@ -0,0 +1,178 @@ +# +# Author:: Doug MacEachern (<dougm@vmware.com>) +# Copyright:: Copyright (c) 2010 VMware, 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' + +class Chef + class Util + class Windows + class NetUser + end + end + end +end + +describe Chef::Provider::User::Windows do + before(:each) do + @node = Chef::Node.new + @new_resource = Chef::Resource::User.new("monkey") + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @current_resource = Chef::Resource::User.new("monkey") + + @net_user = mock("Chef::Util::Windows::NetUser") + Chef::Util::Windows::NetUser.stub!(:new).and_return(@net_user) + + @provider = Chef::Provider::User::Windows.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + end + + describe "when comparing the user's current attributes to the desired attributes" do + before do + @new_resource.comment "Adam Jacob" + @new_resource.uid 1000 + @new_resource.gid 1000 + @new_resource.home "/home/adam" + @new_resource.shell "/usr/bin/zsh" + @new_resource.password "abracadabra" + + @provider.current_resource = @new_resource.clone + end + describe "and the attributes match" do + it "doesn't set the comment field to be updated" do + @provider.set_options.should_not have_key(:full_name) + end + + it "doesn't set the home directory to be updated" do + @provider.set_options.should_not have_key(:home_dir) + end + + it "doesn't set the group id to be updated" do + @provider.set_options.should_not have_key(:primary_group_id) + end + + it "doesn't set the user id to be updated" do + @provider.set_options.should_not have_key(:user_id) + end + + it "doesn't set the shell to be updated" do + @provider.set_options.should_not have_key(:script_path) + end + + it "doesn't set the password to be updated" do + @provider.set_options.should_not have_key(:password) + end + + end + + describe "and the attributes do not match" do + before do + @current_resource = Chef::Resource::User.new("adam") + @current_resource.comment "Adam Jacob-foo" + @current_resource.uid 1111 + @current_resource.gid 1111 + @current_resource.home "/home/adam-foo" + @current_resource.shell "/usr/bin/tcsh" + @current_resource.password "foobarbaz" + @provider.current_resource = @current_resource + end + + it "marks the full_name field to be updated" do + @provider.set_options[:full_name].should == "Adam Jacob" + end + + it "marks the home_dir attribute to be updated" do + @provider.set_options[:home_dir].should == '/home/adam' + end + + it "marks the primary_group_id attribute to be updated" do + @provider.set_options[:primary_group_id].should == 1000 + end + + it "marks the user_id attribute to be updated" do + @provider.set_options[:user_id].should == 1000 + end + + it "marks the script_path attribute to be updated" do + @provider.set_options[:script_path].should == '/usr/bin/zsh' + end + + it "marks the password attribute to be updated" do + @provider.set_options[:password].should == 'abracadabra' + end + end + end + + describe "when creating the user" do + it "should call @net_user.add with the return of set_options" do + @provider.stub!(:set_options).and_return(:name=> "monkey") + @net_user.should_receive(:add).with(:name=> "monkey") + @provider.create_user + end + end + + describe "manage_user" do + before(:each) do + @provider.stub!(:set_options).and_return(:name=> "monkey") + end + + it "should call @net_user.update with the return of set_options" do + @net_user.should_receive(:update).with(:name=> "monkey") + @provider.manage_user + end + end + + describe "when removing the user" do + it "should call @net_user.delete" do + @net_user.should_receive(:delete) + @provider.remove_user + end + end + + describe "when checking if the user is locked" do + before(:each) do + @current_resource.password "abracadabra" + end + + it "should return true if user is locked" do + @net_user.stub!(:check_enabled).and_return(true) + @provider.check_lock.should eql(true) + end + + it "should return false if user is not locked" do + @net_user.stub!(:check_enabled).and_return(false) + @provider.check_lock.should eql(false) + end + end + + describe "locking the user" do + it "should call @net_user.disable_account" do + @net_user.stub!(:check_enabled).and_return(true) + @net_user.should_receive(:disable_account) + @provider.lock_user + end + end + + describe "unlocking the user" do + it "should call @net_user.enable_account" do + @net_user.stub!(:check_enabled).and_return(false) + @net_user.should_receive(:enable_account) + @provider.unlock_user + end + end +end diff --git a/spec/unit/provider/user_spec.rb b/spec/unit/provider/user_spec.rb new file mode 100644 index 0000000000..4066ffd7fe --- /dev/null +++ b/spec/unit/provider/user_spec.rb @@ -0,0 +1,477 @@ +# +# 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' + +EtcPwnamIsh = Struct.new(:name, :passwd, :uid, :gid, :gecos, :dir, :shell, :change, :uclass, :expire) +EtcGrnamIsh = Struct.new(:name, :passwd, :gid, :mem) + +describe Chef::Provider::User do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::User.new("adam") + @new_resource.comment "Adam Jacob" + @new_resource.uid 1000 + @new_resource.gid 1000 + @new_resource.home "/home/adam" + @new_resource.shell "/usr/bin/zsh" + + @current_resource = Chef::Resource::User.new("adam") + @current_resource.comment "Adam Jacob" + @current_resource.uid 1000 + @current_resource.gid 1000 + @current_resource.home "/home/adam" + @current_resource.shell "/usr/bin/zsh" + + @provider = Chef::Provider::User.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + end + + describe "when first created" do + it "assume the user exists by default" do + @provider.user_exists.should eql(true) + end + + it "does not know the locked state" do + @provider.locked.should eql(nil) + end + end + + describe "executing load_current_resource" do + before(:each) do + @node = Chef::Node.new + #@new_resource = mock("Chef::Resource::User", + # :null_object => true, + # :username => "adam", + # :comment => "Adam Jacob", + # :uid => 1000, + # :gid => 1000, + # :home => "/home/adam", + # :shell => "/usr/bin/zsh", + # :password => nil, + # :updated => nil + #) + Chef::Resource::User.stub!(:new).and_return(@current_resource) + @pw_user = EtcPwnamIsh.new + @pw_user.name = "adam" + @pw_user.gid = 1000 + @pw_user.uid = 1000 + @pw_user.gecos = "Adam Jacob" + @pw_user.dir = "/home/adam" + @pw_user.shell = "/usr/bin/zsh" + @pw_user.passwd = "*" + Etc.stub!(:getpwnam).and_return(@pw_user) + end + + it "should create a current resource with the same name as the new resource" do + @provider.load_current_resource + @provider.current_resource.name.should == 'adam' + end + + it "should set the username of the current resource to the username of the new resource" do + @provider.load_current_resource + @current_resource.username.should == @new_resource.username + end + + it "should look up the user in /etc/passwd with getpwnam" do + Etc.should_receive(:getpwnam).with(@new_resource.username).and_return(@pw_user) + @provider.load_current_resource + end + + it "should set user_exists to false if the user is not found with getpwnam" do + Etc.should_receive(:getpwnam).and_raise(ArgumentError) + @provider.load_current_resource + @provider.user_exists.should eql(false) + end + + # The mapping between the Chef::Resource::User and Getpwnam struct + user_attrib_map = { + :uid => :uid, + :gid => :gid, + :comment => :gecos, + :home => :dir, + :shell => :shell + } + user_attrib_map.each do |user_attrib, getpwnam_attrib| + it "should set the current resources #{user_attrib} based on getpwnam #{getpwnam_attrib}" do + @current_resource.should_receive(user_attrib).with(@pw_user.send(getpwnam_attrib)) + @provider.load_current_resource + end + end + + it "should attempt to convert the group gid if one has been supplied" do + @provider.should_receive(:convert_group_name) + @provider.load_current_resource + end + + it "shouldn't try and convert the group gid if none has been supplied" do + @new_resource.stub!(:gid).and_return(nil) + @provider.should_not_receive(:convert_group_name) + @provider.load_current_resource + end + + it "should return the current resource" do + @provider.load_current_resource.should eql(@current_resource) + end + + describe "and running assertions" do + def self.shadow_lib_unavail? + begin + require 'rubygems' + require 'shadow' + rescue LoadError => e + pending "ruby-shadow gem not installed for dynamic load test" + true + else + false + end + end + + before (:each) do + user = @pw_user.dup + user.name = "root" + user.passwd = "x" + @new_resource.password "some new password" + Etc.stub!(:getpwnam).and_return(user) + end + + unless shadow_lib_unavail? + context "and we have the ruby-shadow gem" do + pending "and we are not root (rerun this again as root)", :requires_unprivileged_user => true + + context "and we are root", :requires_root => true do + it "should pass assertions when ruby-shadow can be loaded" do + @provider.action = 'create' + original_method = @provider.method(:require) + @provider.should_receive(:require) { |*args| original_method.call(*args) } + passwd_info = Struct::PasswdEntry.new(:sp_namp => "adm ", :sp_pwdp => "$1$T0N0Q.lc$nyG6pFI3Dpqa5cxUz/57j0", :sp_lstchg => 14861, :sp_min => 0, :sp_max => 99999, + :sp_warn => 7, :sp_inact => -1, :sp_expire => -1, :sp_flag => -1) + Shadow::Passwd.should_receive(:getspnam).with("adam").and_return(passwd_info) + @provider.load_current_resource + @provider.define_resource_requirements + @provider.process_resource_requirements + end + end + end + end + + it "should fail assertions when ruby-shadow cannot be loaded" do + @provider.should_receive(:require).with("shadow") { raise LoadError } + @provider.load_current_resource + @provider.define_resource_requirements + lambda {@provider.process_resource_requirements}.should raise_error Chef::Exceptions::MissingLibrary + end + + end + end + + describe "compare_user" do + before(:each) do + # @node = Chef::Node.new + # @new_resource = mock("Chef::Resource::User", + # :null_object => true, + # :username => "adam", + # :comment => "Adam Jacob", + # :uid => 1000, + # :gid => 1000, + # :home => "/home/adam", + # :shell => "/usr/bin/zsh", + # :password => nil, + # :updated => nil + # ) + # @current_resource = mock("Chef::Resource::User", + # :null_object => true, + # :username => "adam", + # :comment => "Adam Jacob", + # :uid => 1000, + # :gid => 1000, + # :home => "/home/adam", + # :shell => "/usr/bin/zsh", + # :password => nil, + # :updated => nil + # ) + # @provider = Chef::Provider::User.new(@node, @new_resource) + # @provider.current_resource = @current_resource + end + + %w{uid gid comment home shell password}.each do |attribute| + it "should return true if #{attribute} doesn't match" do + @new_resource.should_receive(attribute).exactly(2).times.and_return(true) + @current_resource.should_receive(attribute).once.and_return(false) + @provider.compare_user.should eql(true) + end + end + + it "should return false if the objects are identical" do + @provider.compare_user.should eql(false) + end + end + + describe "action_create" do + before(:each) do + @provider.stub!(:load_current_resource) + # @current_resource = mock("Chef::Resource::User", + # :null_object => true, + # :username => "adam", + # :comment => "Adam Jacob", + # :uid => 1000, + # :gid => 1000, + # :home => "/home/adam", + # :shell => "/usr/bin/zsh", + # :password => nil, + # :updated => nil + # ) + # @provider = Chef::Provider::User.new(@node, @new_resource) + # @provider.current_resource = @current_resource + # @provider.user_exists = false + # @provider.stub!(:create_user).and_return(true) + # @provider.stub!(:manage_user).and_return(true) + end + + it "should call create_user if the user does not exist" do + @provider.user_exists = false + @provider.should_receive(:create_user).and_return(true) + @provider.action_create + @provider.converge + @new_resource.should be_updated + end + + it "should call manage_user if the user exists and has mismatched attributes" do + @provider.user_exists = true + @provider.stub!(:compare_user).and_return(true) + @provider.should_receive(:manage_user).and_return(true) + @provider.action_create + @provider.converge + end + + it "should set the the new_resources updated flag when it creates the user if we call manage_user" do + @provider.user_exists = true + @provider.stub!(:compare_user).and_return(true) + @provider.stub!(:manage_user).and_return(true) + @provider.action_create + @provider.converge + @new_resource.should be_updated + end + end + + describe "action_remove" do + before(:each) do + @provider.stub!(:load_current_resource) + end + + it "should not call remove_user if the user does not exist" do + @provider.user_exists = false + @provider.should_not_receive(:remove_user) + @provider.action_remove + @provider.converge + end + + it "should call remove_user if the user exists" do + @provider.user_exists = true + @provider.should_receive(:remove_user) + @provider.action_remove + @provider.converge + end + + it "should set the new_resources updated flag to true if the user is removed" do + @provider.user_exists = true + @provider.should_receive(:remove_user) + @provider.action_remove + @provider.converge + @new_resource.should be_updated + end + end + + describe "action_manage" do + before(:each) do + @provider.stub!(:load_current_resource) + # @node = Chef::Node.new + # @new_resource = mock("Chef::Resource::User", + # :null_object => true + # ) + # @current_resource = mock("Chef::Resource::User", + # :null_object => true + # ) + # @provider = Chef::Provider::User.new(@node, @new_resource) + # @provider.current_resource = @current_resource + # @provider.user_exists = true + # @provider.stub!(:manage_user).and_return(true) + end + + it "should run manage_user if the user exists and has mismatched attributes" do + @provider.should_receive(:compare_user).and_return(true) + @provider.should_receive(:manage_user).and_return(true) + @provider.action_manage + @provider.converge + end + + it "should set the new resources updated flag to true if manage_user is called" do + @provider.stub!(:compare_user).and_return(true) + @provider.stub!(:manage_user).and_return(true) + @provider.action_manage + @provider.converge + @new_resource.should be_updated + end + + it "should not run manage_user if the user does not exist" do + @provider.user_exists = false + @provider.should_not_receive(:manage_user) + @provider.action_manage + @provider.converge + end + + it "should not run manage_user if the user exists but has no differing attributes" do + @provider.should_receive(:compare_user).and_return(false) + @provider.should_not_receive(:manage_user) + @provider.action_manage + @provider.converge + end + end + + describe "action_modify" do + before(:each) do + @provider.stub!(:load_current_resource) + # @node = Chef::Node.new + # @new_resource = mock("Chef::Resource::User", + # :null_object => true + # ) + # @current_resource = mock("Chef::Resource::User", + # :null_object => true + # ) + # @provider = Chef::Provider::User.new(@node, @new_resource) + # @provider.current_resource = @current_resource + # @provider.user_exists = true + # @provider.stub!(:manage_user).and_return(true) + end + + it "should run manage_user if the user exists and has mismatched attributes" do + @provider.should_receive(:compare_user).and_return(true) + @provider.should_receive(:manage_user).and_return(true) + @provider.action_modify + @provider.converge + end + + it "should set the new resources updated flag to true if manage_user is called" do + @provider.stub!(:compare_user).and_return(true) + @provider.stub!(:manage_user).and_return(true) + @provider.action_modify + @provider.converge + @new_resource.should be_updated + end + + it "should not run manage_user if the user exists but has no differing attributes" do + @provider.should_receive(:compare_user).and_return(false) + @provider.should_not_receive(:manage_user) + @provider.action_modify + @provider.converge + end + + it "should raise a Chef::Exceptions::User if the user doesn't exist" do + @provider.user_exists = false + lambda { @provider.action = :modify; @provider.run_action }.should raise_error(Chef::Exceptions::User) + end + end + + + describe "action_lock" do + before(:each) do + @provider.stub!(:load_current_resource) + end + it "should lock the user if it exists and is unlocked" do + @provider.stub!(:check_lock).and_return(false) + @provider.should_receive(:lock_user).and_return(true) + @provider.action_lock + @provider.converge + end + + it "should set the new resources updated flag to true if lock_user is called" do + @provider.stub!(:check_lock).and_return(false) + @provider.should_receive(:lock_user) + @provider.action_lock + @provider.converge + @new_resource.should be_updated + end + + it "should raise a Chef::Exceptions::User if we try and lock a user that does not exist" do + @provider.user_exists = false + @provider.action = :lock + + lambda { @provider.run_action }.should raise_error(Chef::Exceptions::User) + end + end + + describe "action_unlock" do + before(:each) do + @provider.stub!(:load_current_resource) + # @node = Chef::Node.new + # @new_resource = mock("Chef::Resource::User", + # :null_object => true + # ) + # @current_resource = mock("Chef::Resource::User", + # :null_object => true + # ) + # @provider = Chef::Provider::User.new(@node, @new_resource) + # @provider.current_resource = @current_resource + # @provider.user_exists = true + # @provider.stub!(:check_lock).and_return(true) + # @provider.stub!(:unlock_user).and_return(true) + end + + it "should unlock the user if it exists and is locked" do + @provider.stub!(:check_lock).and_return(true) + @provider.should_receive(:unlock_user).and_return(true) + @provider.action_unlock + @provider.converge + @new_resource.should be_updated + end + + it "should raise a Chef::Exceptions::User if we try and unlock a user that does not exist" do + @provider.user_exists = false + @provider.action = :unlock + lambda { @provider.run_action }.should raise_error(Chef::Exceptions::User) + end + end + + describe "convert_group_name" do + before do + @new_resource.gid('999') + @group = EtcGrnamIsh.new('wheel', '*', 999, []) + end + + it "should lookup the group name locally" do + Etc.should_receive(:getgrnam).with("999").and_return(@group) + @provider.convert_group_name.should == 999 + end + + it "should raise an error if we can't translate the group name during resource assertions" do + Etc.should_receive(:getgrnam).and_raise(ArgumentError) + @provider.define_resource_requirements + @provider.convert_group_name + lambda { @provider.process_resource_requirements }.should raise_error(Chef::Exceptions::User) + end + + it "should set the new resources gid to the integerized version if available" do + Etc.should_receive(:getgrnam).with("999").and_return(@group) + @provider.convert_group_name + @new_resource.gid.should == 999 + end + end +end |