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 | |
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')
279 files changed, 48194 insertions, 0 deletions
diff --git a/spec/unit/api_client_spec.rb b/spec/unit/api_client_spec.rb new file mode 100644 index 0000000000..e01243152e --- /dev/null +++ b/spec/unit/api_client_spec.rb @@ -0,0 +1,163 @@ +# +# 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 'chef/api_client' +require 'tempfile' + +describe Chef::ApiClient do + before(:each) do + @client = Chef::ApiClient.new + end + + describe "initialize" do + it "should be a Chef::ApiClient" do + @client.should be_a_kind_of(Chef::ApiClient) + end + end + + describe "name" do + it "should let you set the name to a string" do + @client.name("ops_master").should == "ops_master" + end + + it "should return the current name" do + @client.name "ops_master" + @client.name.should == "ops_master" + end + + it "should not accept spaces" do + lambda { @client.name "ops master" }.should raise_error(ArgumentError) + end + + it "should throw an ArgumentError if you feed it anything but a string" do + lambda { @client.name Hash.new }.should raise_error(ArgumentError) + end + end + + describe "admin" do + it "should let you set the admin bit" do + @client.admin(true).should == true + end + + it "should return the current admin value" do + @client.admin true + @client.admin.should == true + end + + it "should default to false" do + @client.admin.should == false + end + + it "should throw an ArgumentError if you feed it anything but true or false" do + lambda { @client.name Hash.new }.should raise_error(ArgumentError) + end + end + + describe "public_key" do + it "should let you set the public key" do + @client.public_key("super public").should == "super public" + end + + it "should return the current public key" do + @client.public_key("super public") + @client.public_key.should == "super public" + end + + it "should throw an ArgumentError if you feed it something lame" do + lambda { @client.public_key Hash.new }.should raise_error(ArgumentError) + end + end + + describe "private_key" do + it "should let you set the private key" do + @client.private_key("super private").should == "super private" + end + + it "should return the private key" do + @client.private_key("super private") + @client.private_key.should == "super private" + end + + it "should throw an ArgumentError if you feed it something lame" do + lambda { @client.private_key Hash.new }.should raise_error(ArgumentError) + end + end + + describe "serialize" do + before(:each) do + @client.name("black") + @client.public_key("crowes") + @client.private_key("monkeypants") + @serial = @client.to_json + end + + it "should serialize to a json hash" do + @client.to_json.should match(/^\{.+\}$/) + end + + %w{ + name + public_key + }.each do |t| + it "should include '#{t}'" do + @serial.should =~ /"#{t}":"#{@client.send(t.to_sym)}"/ + end + end + + it "should include 'admin'" do + @serial.should =~ /"admin":false/ + end + + it "should not include the private key" do + @serial.should_not =~ /"private_key":/ + end + end + + describe "deserialize" do + before(:each) do + @client.name("black") + @client.public_key("crowes") + @client.private_key("monkeypants") + @client.admin(true) + @deserial = Chef::JSONCompat.from_json(@client.to_json) + end + + it "should deserialize to a Chef::ApiClient object" do + @deserial.should be_a_kind_of(Chef::ApiClient) + end + + %w{ + name + public_key + admin + }.each do |t| + it "should match '#{t}'" do + @deserial.send(t.to_sym).should == @client.send(t.to_sym) + end + end + + it "should not include the private key" do + @deserial.private_key.should == nil + end + + end +end + + diff --git a/spec/unit/application/agent_spec.rb b/spec/unit/application/agent_spec.rb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/spec/unit/application/agent_spec.rb diff --git a/spec/unit/application/client_spec.rb b/spec/unit/application/client_spec.rb new file mode 100644 index 0000000000..c5480c4adc --- /dev/null +++ b/spec/unit/application/client_spec.rb @@ -0,0 +1,136 @@ +# +# Author:: AJ Christensen (<aj@junglist.gen.nz>) +# 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::Application::Client, "reconfigure" do + before do + @original_config = Chef::Config.configuration + + @app = Chef::Application::Client.new + @app.stub!(:configure_opt_parser).and_return(true) + @app.stub!(:configure_chef).and_return(true) + @app.stub!(:configure_logging).and_return(true) + Chef::Config[:json_attribs] = nil + Chef::Config[:interval] = 10 + Chef::Config[:splay] = nil + + Chef::Config[:once] = false + end + + after do + Chef::Config.configuration.replace(@original_config) + end + + describe "when in daemonized mode and no interval has been set" do + before do + Chef::Config[:daemonize] = true + Chef::Config[:interval] = nil + end + + it "should set the interval to 1800" do + @app.reconfigure + Chef::Config.interval.should == 1800 + end + end + + describe "when configured to run once" do + before do + Chef::Config[:once] = true + Chef::Config[:daemonize] = false + Chef::Config[:splay] = 60 + Chef::Config[:interval] = 1800 + end + + it "ignores the splay" do + @app.reconfigure + Chef::Config.splay.should be_nil + end + + it "forces the interval to nil" do + @app.reconfigure + Chef::Config.interval.should be_nil + end + + end + + describe "when the json_attribs configuration option is specified" do + + describe "and the json_attribs matches a HTTP regex" do + before do + @json = StringIO.new({:a=>"b"}.to_json) + @json_tempfile = mock("Tempfile for remote JSON", :open => @json) + @rest = mock("Chef::REST", :get_rest => @json_tempfile) + + Chef::Config[:json_attribs] = "https://foo.com/foo.json" + Chef::REST.stub!(:new).with("https://foo.com/foo.json", nil, nil).and_return(@rest) + @app.stub!(:open).with("/etc/chef/dna.json").and_return(@json) + end + + it "should perform a RESTful GET on the supplied URL" do + @app.reconfigure + @app.chef_client_json.should == {"a" => "b"} + end + end + + describe "and the json_attribs does not match the HTTP regex" do + before do + Chef::Config[:json_attribs] = "/etc/chef/dna.json" + @json = StringIO.new({:a=>"b"}.to_json) + @app.stub!(:open).with("/etc/chef/dna.json").and_return(@json) + end + + it "should parse the json out of the file" do + @app.reconfigure + @app.chef_client_json.should == {"a" => "b"} + end + end + + describe "when parsing fails" do + before do + Chef::Config[:json_attribs] = "/etc/chef/dna.json" + @json = mock("Tempfile", :read => {:a=>"b"}.to_json) + @app.stub!(:open).with("/etc/chef/dna.json").and_return(@json) + Chef::JSONCompat.stub!(:from_json).with(@json.read).and_raise(JSON::ParserError) + Chef::Application.stub!(:fatal!).and_return(true) + end + + it "should hard fail the application" do + Chef::Application.should_receive(:fatal!).with("Could not parse the provided JSON file (/etc/chef/dna.json)!: JSON::ParserError", 2).and_return(true) + @app.reconfigure + end + end + end +end + +describe Chef::Application::Client, "setup_application" do + before do + @app = Chef::Application::Client.new + # this is all stuff the reconfigure method needs + @app.stub!(:configure_opt_parser).and_return(true) + @app.stub!(:configure_chef).and_return(true) + @app.stub!(:configure_logging).and_return(true) + end + + it "should change privileges" do + Chef::Daemon.should_receive(:change_privilege).and_return(true) + @app.setup_application + end + after do + Chef::Config[:solo] = false + end +end diff --git a/spec/unit/application/knife_spec.rb b/spec/unit/application/knife_spec.rb new file mode 100644 index 0000000000..78a65e7045 --- /dev/null +++ b/spec/unit/application/knife_spec.rb @@ -0,0 +1,152 @@ +# +# Author:: AJ Christensen (<aj@junglist.gen.nz>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'spec_helper' +require "#{CHEF_SPEC_DATA}/knife_subcommand/test_yourself" + +describe Chef::Application::Knife do + include SpecHelpers::Knife + + before(:all) do + class NoopKnifeCommand < Chef::Knife + def run + end + end + end + + before(:each) do + @knife = Chef::Application::Knife.new + @knife.stub!(:puts) + Chef::Knife.stub!(:list_commands) + end + + it "should exit 1 and print the options if no arguments are given at all" do + with_argv([]) do + lambda { @knife.run }.should raise_error(SystemExit) { |e| e.status.should == 1 } + end + end + + it "should exit 2 if run without a sub command" do + with_argv("--user", "adam") do + Chef::Log.should_receive(:error).with(/you need to pass a sub\-command/i) + lambda { @knife.run }.should raise_error(SystemExit) { |e| e.status.should == 2 } + end + end + + it "should run a sub command with the applications command line option prototype" do + with_argv(*%w{noop knife command with some args}) do + knife = mock(Chef::Knife) + Chef::Knife.should_receive(:run).with(ARGV, @knife.options).and_return(knife) + @knife.should_receive(:exit).with(0) + @knife.run + end + end + + describe "when given a path to the client key" do + it "expands a relative path relative to the CWD" do + relative_path = '.chef/client.pem' + Dir.stub!(:pwd).and_return(CHEF_SPEC_DATA) + with_argv(*%W{noop knife command -k #{relative_path}}) do + @knife.should_receive(:exit).with(0) + @knife.run + end + Chef::Config[:client_key].should == File.join(CHEF_SPEC_DATA, relative_path) + end + + it "expands a ~/home/path to the correct full path" do + home_path = '~/.chef/client.pem' + with_argv(*%W{noop knife command -k #{home_path}}) do + @knife.should_receive(:exit).with(0) + @knife.run + end + Chef::Config[:client_key].should == File.join(ENV['HOME'], '.chef/client.pem').gsub((File::ALT_SEPARATOR || '\\'), File::SEPARATOR) + end + + it "does not expand a full path" do + full_path = if windows? + 'C:/chef/client.pem' + else + '/etc/chef/client.pem' + end + with_argv(*%W{noop knife command -k #{full_path}}) do + @knife.should_receive(:exit).with(0) + @knife.run + end + Chef::Config[:client_key].should == full_path + end + + end + + describe "with environment configuration" do + before do + Chef::Config[:environment] = nil + end + + it "should default to no environment" do + with_argv(*%w{noop knife command}) do + @knife.should_receive(:exit).with(0) + @knife.run + end + Chef::Config[:environment].should == nil + end + + it "should load the environment from the config file" do + config_file = File.join(CHEF_SPEC_DATA,"environment-config.rb") + with_argv(*%W{noop knife command -c #{config_file}}) do + @knife.should_receive(:exit).with(0) + @knife.run + end + Chef::Config[:environment].should == 'production' + end + + it "should load the environment from the CLI options" do + with_argv(*%W{noop knife command -E development}) do + @knife.should_receive(:exit).with(0) + @knife.run + end + Chef::Config[:environment].should == 'development' + end + + it "should override the config file environment with the CLI environment" do + config_file = File.join(CHEF_SPEC_DATA,"environment-config.rb") + with_argv(*%W{noop knife command -c #{config_file} -E override}) do + @knife.should_receive(:exit).with(0) + @knife.run + end + Chef::Config[:environment].should == 'override' + end + + it "should override the config file environment with the CLI environment regardless of order" do + config_file = File.join(CHEF_SPEC_DATA,"environment-config.rb") + with_argv(*%W{noop knife command -E override -c #{config_file}}) do + @knife.should_receive(:exit).with(0) + @knife.run + end + Chef::Config[:environment].should == 'override' + end + + it "should run a sub command with the applications command line option prototype" do + with_argv(*%w{noop knife command with some args}) do + knife = mock(Chef::Knife) + Chef::Knife.should_receive(:run).with(ARGV, @knife.options).and_return(knife) + @knife.should_receive(:exit).with(0) + @knife.run + end + end + + end +end diff --git a/spec/unit/application/server_spec.rb b/spec/unit/application/server_spec.rb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/spec/unit/application/server_spec.rb diff --git a/spec/unit/application/solo_spec.rb b/spec/unit/application/solo_spec.rb new file mode 100644 index 0000000000..148fb3cf87 --- /dev/null +++ b/spec/unit/application/solo_spec.rb @@ -0,0 +1,167 @@ +# +# Author:: AJ Christensen (<aj@junglist.gen.nz>) +# 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::Application::Solo do + before do + @original_config = Chef::Config.configuration + + + @app = Chef::Application::Solo.new + @app.stub!(:configure_opt_parser).and_return(true) + @app.stub!(:configure_chef).and_return(true) + @app.stub!(:configure_logging).and_return(true) + Chef::Config[:recipe_url] = false + Chef::Config[:json_attribs] = false + Chef::Config[:splay] = nil + Chef::Config[:solo] = true + end + + after do + Chef::Config[:solo] = nil + Chef::Config.configuration.replace(@original_config) + Chef::Config[:solo] = false + end + + describe "configuring the application" do + it "should set solo mode to true" do + @app.reconfigure + Chef::Config[:solo].should be_true + end + + describe "when in daemonized mode and no interval has been set" do + before do + Chef::Config[:daemonize] = true + end + + it "should set the interval to 1800" do + Chef::Config[:interval] = nil + @app.reconfigure + Chef::Config[:interval].should == 1800 + end + end + + describe "when the json_attribs configuration option is specified" do + + describe "and the json_attribs matches a HTTP regex" do + before do + @json = StringIO.new({:a=>"b"}.to_json) + @json_tempfile = mock("Tempfile (mock)", :open => @json) + @rest = mock("Chef::REST", :get_rest => @json_tempfile) + + Chef::Config[:json_attribs] = "https://foo.com/foo.json" + Chef::REST.stub!(:new).with("https://foo.com/foo.json", nil, nil).and_return(@rest) + @app.stub!(:open).with("/etc/chef/dna.json").and_return(@json) + end + + it "should perform a RESTful GET on the supplied URL" do + @app.reconfigure + @app.chef_solo_json.should == {"a" => "b"} + end + end + + describe "and the json_attribs does not match the HTTP regex" do + before do + Chef::Config[:json_attribs] = "/etc/chef/dna.json" + @json = StringIO.new({:a=>"b"}.to_json) + @app.stub!(:open).with("/etc/chef/dna.json").and_return(@json) + end + + it "should parse the json out of the file" do + @app.reconfigure + @app.chef_solo_json.should == {"a" => "b"} + end + end + + describe "when parsing fails" do + before do + Chef::Config[:json_attribs] = "/etc/chef/dna.json" + @json = mock("Tempfile", :read => {:a=>"b"}.to_json) + @app.stub!(:open).with("/etc/chef/dna.json").and_return(@json) + Chef::JSONCompat.stub!(:from_json).with(@json.read).and_raise(JSON::ParserError) + Chef::Application.stub!(:fatal!).and_return(true) + end + + it "should hard fail the application" do + Chef::Application.should_receive(:fatal!).with("Could not parse the provided JSON file (/etc/chef/dna.json)!: JSON::ParserError", 2).and_return(true) + @app.reconfigure + end + end + end + + + + describe "when the recipe_url configuration option is specified" do + before do + Chef::Config[:cookbook_path] = "#{Dir.tmpdir}/chef-solo/cookbooks" + Chef::Config[:recipe_url] = "http://junglist.gen.nz/recipes.tgz" + FileUtils.stub!(:mkdir_p).and_return(true) + @tarfile = StringIO.new("remote_tarball_content") + @app.stub!(:open).with("http://junglist.gen.nz/recipes.tgz").and_yield(@tarfile) + + @target_file = StringIO.new + File.stub!(:open).with("#{Dir.tmpdir}/chef-solo/recipes.tgz", "wb").and_yield(@target_file) + + Chef::Mixin::Command.stub!(:run_command).and_return(true) + end + + it "should create the recipes path based on the parent of the cookbook path" do + FileUtils.should_receive(:mkdir_p).with("#{Dir.tmpdir}/chef-solo").and_return(true) + @app.reconfigure + end + + it "should download the recipes" do + @app.should_receive(:open).with("http://junglist.gen.nz/recipes.tgz").and_yield(@tarfile) + @app.reconfigure + end + + it "should write the recipes to the target path" do + @app.reconfigure + @target_file.string.should == "remote_tarball_content" + end + + it "should untar the target file to the parent of the cookbook path" do + Chef::Mixin::Command.should_receive(:run_command).with({:command => "tar zxvfC #{Dir.tmpdir}/chef-solo/recipes.tgz #{Dir.tmpdir}/chef-solo"}).and_return(true) + @app.reconfigure + end + end + end + + + describe "after the application has been configured" do + before do + Chef::Config[:solo] = true + + Chef::Daemon.stub!(:change_privilege) + @chef_client = mock("Chef::Client") + Chef::Client.stub!(:new).and_return(@chef_client) + @app = Chef::Application::Solo.new + # this is all stuff the reconfigure method needs + @app.stub!(:configure_opt_parser).and_return(true) + @app.stub!(:configure_chef).and_return(true) + @app.stub!(:configure_logging).and_return(true) + end + + it "should change privileges" do + Chef::Daemon.should_receive(:change_privilege).and_return(true) + @app.setup_application + end + end + +end + diff --git a/spec/unit/application_spec.rb b/spec/unit/application_spec.rb new file mode 100644 index 0000000000..c89bc27d4f --- /dev/null +++ b/spec/unit/application_spec.rb @@ -0,0 +1,239 @@ +# +# Author:: AJ Christensen (<aj@junglist.gen.nz>) +# Author:: Mark Mzyk (mmzyk@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::Application do + before do + Chef::Log.logger = Logger.new(StringIO.new) + @app = Chef::Application.new + Dir.stub!(:chdir).and_return(0) + @app.stub!(:reconfigure) + end + + describe "reconfigure" do + before do + @app = Chef::Application.new + @app.stub!(:configure_chef).and_return(true) + @app.stub!(:configure_logging).and_return(true) + end + + it "should configure chef" do + @app.should_receive(:configure_chef).and_return(true) + @app.reconfigure + end + + it "should configure logging" do + @app.should_receive(:configure_logging).and_return(true) + @app.reconfigure + end + + end + + describe Chef::Application do + before do + @app = Chef::Application.new + end + + describe "run" do + before do + @app.stub!(:setup_application).and_return(true) + @app.stub!(:run_application).and_return(true) + @app.stub!(:configure_chef).and_return(true) + @app.stub!(:configure_logging).and_return(true) + end + + it "should reconfigure the application before running" do + @app.should_receive(:reconfigure).and_return(true) + @app.run + end + + it "should setup the application before running it" do + @app.should_receive(:setup_application).and_return(true) + @app.run + end + + it "should run the actual application" do + @app.should_receive(:run_application).and_return(true) + @app.run + end + end + end + + describe "configure_chef" do + before do + @app = Chef::Application.new + #Chef::Config.stub!(:merge!).and_return(true) + @app.stub!(:parse_options).and_return(true) + end + + it "should parse the commandline options" do + @app.should_receive(:parse_options).and_return(true) + @app.config[:config_file] = "/etc/chef/default.rb" #have a config file set, to prevent triggering error block + @app.configure_chef + end + + describe "when a config_file is present" do + before do + Chef::Config.configuration.delete('rspec_ran') + + @config_file = Tempfile.new("rspec-chef-config") + @config_file.puts("rspec_ran('true')") + @config_file.close + + @app.config[:config_file] = "/etc/chef/default.rb" + end + + after do + @config_file.unlink + end + + it "should configure chef::config from a file" do + File.should_receive(:open).with("/etc/chef/default.rb").and_yield(@config_file) + Chef::Config.should_receive(:from_file).with(@config_file.path) + @app.configure_chef + end + + it "should merge the local config hash into chef::config" do + File.should_receive(:open).with("/etc/chef/default.rb").and_yield(@config_file) + @app.configure_chef + Chef::Config.rspec_ran.should == "true" + end + + end + + describe "when there is no config_file defined" do + before do + @app.config[:config_file] = nil + end + + it "should raise a fatal" do + Chef::Config.should_not_receive(:from_file).with("/etc/chef/default.rb") + Chef::Application.should_receive(:fatal!) + @app.configure_chef + end + end + + describe "when the config file is set and not found" do + before do + @app.config[:config_file] = "/etc/chef/notfound" + end + it "should use the passed in command line options and defaults" do + Chef::Config.should_receive(:merge!) + @app.configure_chef + end + end + + describe "when the config_file is an URL" do + before do + Chef::Config.configuration.delete('rspec_ran') + + @app.config[:config_file] = "http://example.com/foo.rb" + + @config_file = Tempfile.new("rspec-chef-config") + @config_file.puts("rspec_ran('true')") + @config_file.close + + + @cf = mock("cf") + #@cf.stub!(:path).and_return("/tmp/some/path") + #@cf.stub!(:nil?).and_return(false) + @rest = mock("rest") + #@rest.stub!(:get_rest).and_return(@rest) + #@rest.stub!(:open).and_yield(@cf) + Chef::REST.stub!(:new).and_return(@rest) + end + + after {@config_file.unlink} + + it "should configure chef::config from an URL" do + Chef::REST.should_receive(:new).with("", nil, nil).at_least(1).times.and_return(@rest) + @rest.should_receive(:fetch).with("http://example.com/foo.rb").and_yield(@config_file) + @app.configure_chef + Chef::Config.rspec_ran.should == "true" + end + end + end + + describe "configure_logging" do + before do + @app = Chef::Application.new + Chef::Log.stub!(:init) + Chef::Log.stub!(:level=) + end + + it "should initialise the chef logger" do + Chef::Log.should_receive(:init).with(Chef::Config[:log_location]).and_return(true) + @app.configure_logging + end + + it "should initialise the chef logger level" do + Chef::Log.should_receive(:level=).with(Chef::Config[:log_level]).and_return(true) + @app.configure_logging + end + + end + + describe "class method: fatal!" do + before do + STDERR.stub!(:puts).with("FATAL: blah").and_return(true) + Chef::Log.stub!(:fatal).with("blah").and_return(true) + Process.stub!(:exit).and_return(true) + end + + it "should log an error message to the logger" do + Chef::Log.should_receive(:fatal).with("blah").and_return(true) + Chef::Application.fatal! "blah" + end + + describe "when an exit code is supplied" do + it "should exit with the given exit code" do + Process.should_receive(:exit).with(-100).and_return(true) + Chef::Application.fatal! "blah", -100 + end + end + + describe "when an exit code is not supplied" do + it "should exit with the default exit code" do + Process.should_receive(:exit).with(-1).and_return(true) + Chef::Application.fatal! "blah" + end + end + + end + + describe "setup_application" do + before do + @app = Chef::Application.new + end + + it "should raise an error" do + lambda { @app.setup_application }.should raise_error(Chef::Exceptions::Application) + end + end + + describe "run_application" do + before do + @app = Chef::Application.new + end + + it "should raise an error" do + lambda { @app.run_application }.should raise_error(Chef::Exceptions::Application) + end + end +end diff --git a/spec/unit/checksum/storage/filesystem_spec.rb b/spec/unit/checksum/storage/filesystem_spec.rb new file mode 100644 index 0000000000..a39644202e --- /dev/null +++ b/spec/unit/checksum/storage/filesystem_spec.rb @@ -0,0 +1,69 @@ +# +# 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 'chef/checksum/storage/filesystem' + +describe Chef::Checksum::Storage::Filesystem do + + before do + Chef::Log.logger = Logger.new(StringIO.new) + + @now = Time.now + + Time.stub!(:now).and_return(@now) + + @checksum_of_the_file = "3fafecfb15585ede6b840158cbc2f399" + @storage = Chef::Checksum::Storage::Filesystem.new(Chef::Config.checksum_path, @checksum_of_the_file) + end + + it "has the path to the file in the checksum repo" do + @storage.file_location.should == "/var/chef/checksums/3f/3fafecfb15585ede6b840158cbc2f399" + end + + it "has the path the the file's subdirectory in the checksum repo" do + @storage.checksum_repo_directory.should == "/var/chef/checksums/3f" + end + + it "commits a file from a given location to the checksum repo location" do + File.should_receive(:rename).with("/tmp/arbitrary_file_location", @storage.file_location) + FileUtils.should_receive(:mkdir_p).with("/var/chef/checksums/3f") + + @storage.commit("/tmp/arbitrary_file_location") + end + + it "reverts committing a file" do + File.should_receive(:rename).with("/tmp/arbitrary_file_location", @storage.file_location) + FileUtils.should_receive(:mkdir_p).with("/var/chef/checksums/3f") + @storage.commit("/tmp/arbitrary_file_location") + + File.should_receive(:rename).with(@storage.file_location, "/tmp/arbitrary_file_location") + @storage.revert("/tmp/arbitrary_file_location") + end + + it "deletes the file" do + FileUtils.should_receive(:rm).with(@storage.file_location) + @storage.purge + end + + it "successfully purges even if its file has been deleted from the repo" do + FileUtils.should_receive(:rm).with(@storage.file_location).and_raise(Errno::ENOENT) + lambda {@storage.purge}.should_not raise_error + end + +end diff --git a/spec/unit/checksum_cache_spec.rb b/spec/unit/checksum_cache_spec.rb new file mode 100644 index 0000000000..78e76b6dfb --- /dev/null +++ b/spec/unit/checksum_cache_spec.rb @@ -0,0 +1,209 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# Copyright:: Copyright (c) 2009 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' + +describe Chef::ChecksumCache do + before(:each) do + Chef::Config[:cache_type] = "Memory" + Chef::Config[:cache_options] = { } + @cache = Chef::ChecksumCache.instance + @cache.reset! + end + + describe "loading the moneta backend" do + it "should build a Chef::ChecksumCache object" do + @cache.should be_a_kind_of(Chef::ChecksumCache) + end + + it "should set up a Moneta Cache adaptor" do + @cache.moneta.should be_a_kind_of(Moneta::Memory) + end + + it "should raise an exception if it cannot load the moneta adaptor" do + Chef::Log.should_receive(:fatal).with(/^Could not load Moneta back end/) + lambda { + c = Chef::ChecksumCache.instance.reset!('WTF') + }.should raise_error(LoadError) + end + end + + describe "when caching checksums of cookbook files and templates" do + + before do + @cache.reset!("Memory", {}) + end + + it "proxies the class method checksum_for_file to the instance" do + @cache.should_receive(:checksum_for_file).with("a_file_or_a_fail") + Chef::ChecksumCache.checksum_for_file("a_file_or_a_fail") + end + + it "returns a cached checksum value" do + @cache.moneta["chef-file-riseofthemachines"] = {"mtime" => "12345", "checksum" => "123abc"} + fstat = mock("File.stat('riseofthemachines')", :mtime => Time.at(12345)) + File.should_receive(:stat).with("riseofthemachines").and_return(fstat) + @cache.checksum_for_file("riseofthemachines").should == "123abc" + end + + it "gives nil for a cache miss" do + @cache.moneta["chef-file-riseofthemachines"] = {"mtime" => "12345", "checksum" => "123abc"} + fstat = mock("File.stat('riseofthemachines')", :mtime => Time.at(555555)) + @cache.lookup_checksum("chef-file-riseofthemachines", fstat).should be_nil + end + + it "treats a non-matching mtime as a cache miss" do + @cache.moneta["chef-file-riseofthemachines"] = {"mtime" => "12345", "checksum" => "123abc"} + fstat = mock("File.stat('riseofthemachines')", :mtime => Time.at(555555)) + @cache.lookup_checksum("chef-file-riseofthemachines", fstat).should be_nil + end + + it "computes a checksum of a file" do + fixture_file = CHEF_SPEC_DATA + "/checksum/random.txt" + expected = "09ee9c8cc70501763563bcf9c218d71b2fbf4186bf8e1e0da07f0f42c80a3394" + @cache.send(:checksum_file, fixture_file, Digest::SHA256.new).should == expected + end + + it "computes a checksum and stores it in the cache" do + fstat = mock("File.stat('riseofthemachines')", :mtime => Time.at(555555)) + @cache.should_receive(:checksum_file).with("riseofthemachines", an_instance_of(Digest::SHA256)).and_return("ohai2uChefz") + @cache.generate_checksum("chef-file-riseofthemachines", "riseofthemachines", fstat).should == "ohai2uChefz" + @cache.lookup_checksum("chef-file-riseofthemachines", fstat).should == "ohai2uChefz" + end + + it "returns a generated checksum if there is no cached value" do + fixture_file = CHEF_SPEC_DATA + "/checksum/random.txt" + expected = "09ee9c8cc70501763563bcf9c218d71b2fbf4186bf8e1e0da07f0f42c80a3394" + @cache.checksum_for_file(fixture_file).should == expected + end + + it "generates a key from a file name" do + file = "/this/is/a/test/random.rb" + @cache.generate_key(file).should == "chef-file--this-is-a-test-random-rb" + end + + it "generates a key from a file name and group" do + file = "/this/is/a/test/random.rb" + @cache.generate_key(file, "spec").should == "spec-file--this-is-a-test-random-rb" + end + + it "returns a cached checksum value using a user defined key" do + key = @cache.generate_key("riseofthemachines", "specs") + @cache.moneta[key] = {"mtime" => "12345", "checksum" => "123abc"} + fstat = mock("File.stat('riseofthemachines')", :mtime => Time.at(12345)) + File.should_receive(:stat).with("riseofthemachines").and_return(fstat) + @cache.checksum_for_file("riseofthemachines", key).should == "123abc" + end + + it "generates a checksum from a non-file IO object" do + io = StringIO.new("riseofthemachines\nriseofthechefs\n") + expected_md5 = '0e157ac1e2dd73191b76067fb6b4bceb' + @cache.generate_md5_checksum(io).should == expected_md5 + end + + end + + describe "when cleaning up after outdated checksums" do + + before do + Chef::ChecksumCache.reset_cache_validity + end + + it "initially has no valid cached checksums" do + Chef::ChecksumCache.valid_cached_checksums.should be_empty + end + + it "adds a checksum to the list of valid cached checksums when it's created" do + @cache.checksum_for_file(File.join(CHEF_SPEC_DATA, 'checksum', 'random.txt')) + Chef::ChecksumCache.valid_cached_checksums.should have(1).valid_checksum + end + + it "adds a checksum to the list of valid cached checksums when it's read" do + @cache.checksum_for_file(File.join(CHEF_SPEC_DATA, 'checksum', 'random.txt')) + Chef::ChecksumCache.reset_cache_validity + @cache.checksum_for_file(File.join(CHEF_SPEC_DATA, 'checksum', 'random.txt')) + Chef::ChecksumCache.valid_cached_checksums.should have(1).valid_checksum + end + + context "with an existing set of cached checksums" do + before do + Chef::Config[:cache_type] = "BasicFile" + Chef::Config[:cache_options] = {:path => File.join(CHEF_SPEC_DATA, "checksum_cache")} + + @expected_cached_checksums = ["chef-file--tmp-chef-rendered-template20100929-10863-600hhz-0", + "chef-file--tmp-chef-rendered-template20100929-10863-6m8zdk-0", + "chef-file--tmp-chef-rendered-template20100929-10863-ahd2gq-0", + "chef-file--tmp-chef-rendered-template20100929-10863-api8ux-0", + "chef-file--tmp-chef-rendered-template20100929-10863-b0r1m1-0", + "chef-file--tmp-chef-rendered-template20100929-10863-bfygsi-0", + "chef-file--tmp-chef-rendered-template20100929-10863-el14l6-0", + "chef-file--tmp-chef-rendered-template20100929-10863-ivrl3y-0", + "chef-file--tmp-chef-rendered-template20100929-10863-kkbs85-0", + "chef-file--tmp-chef-rendered-template20100929-10863-ory1ux-0", + "chef-file--tmp-chef-rendered-template20100929-10863-pgsq76-0", + "chef-file--tmp-chef-rendered-template20100929-10863-ra8uim-0", + "chef-file--tmp-chef-rendered-template20100929-10863-t7k1g-0", + "chef-file--tmp-chef-rendered-template20100929-10863-t8g0sv-0", + "chef-file--tmp-chef-rendered-template20100929-10863-ufy6g3-0", + "chef-file--tmp-chef-rendered-template20100929-10863-x2d6j9-0", + "chef-file--tmp-chef-rendered-template20100929-10863-xi0l6h-0"] + @expected_cached_checksums.sort! + end + + after do + Chef::Config[:cache_type] = "Memory" + Chef::Config[:cache_options] = { } + @cache = Chef::ChecksumCache.instance + @cache.reset! + end + + it "lists all of the cached checksums in the cache directory" do + Chef::ChecksumCache.all_cached_checksums.keys.sort.should == @expected_cached_checksums + end + + it "clears all of the checksums not marked valid from the checksums directory" do + valid_cksum_key = "chef-file--tmp-chef-rendered-template20100929-10863-ivrl3y-0" + valid_cksum_file = File.join(CHEF_SPEC_DATA, "checksum_cache", valid_cksum_key) + @expected_cached_checksums.delete(valid_cksum_key) + + Chef::ChecksumCache.valid_cached_checksums << valid_cksum_key + + Chef::ChecksumCache.should_not_receive(:remove_unused_checksum).with(valid_cksum_file) + @expected_cached_checksums.each do |cksum_key| + full_path_to_cksum = File.join(CHEF_SPEC_DATA, "checksum_cache", cksum_key) + Chef::ChecksumCache.should_receive(:remove_unused_checksum).with(full_path_to_cksum) + end + + Chef::ChecksumCache.cleanup_checksum_cache + end + + it "cleans all 0byte checksum files when it encounters a Marshal error" do + @cache.moneta.stub!(:fetch).and_raise(ArgumentError) + # This cache file is 0 bytes, raises an argument error when + # attempting to Marshal.load + File.should_receive(:unlink).with(File.join(CHEF_SPEC_DATA, "checksum_cache", "chef-file--tmp-chef-rendered-template20100929-10863-6m8zdk-0")) + @cache.lookup_checksum("chef-file--tmp-chef-rendered-template20100929-10863-6m8zdk-0", "foo") + end + end + + end + +end + diff --git a/spec/unit/chef_fs/diff_spec.rb b/spec/unit/chef_fs/diff_spec.rb new file mode 100644 index 0000000000..19d87052ad --- /dev/null +++ b/spec/unit/chef_fs/diff_spec.rb @@ -0,0 +1,328 @@ +# +# Author:: John Keiser (<jkeiser@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 'support/shared/unit/file_system_support' +require 'chef/chef_fs/file_pattern' +require 'chef/chef_fs/command_line' + +# Removes the date stamp from the diff and replaces it with ' DATE' +# example match: "/dev/null\t2012-10-16 16:15:54.000000000 +0000" +# windows match: "--- /dev/null\tTue Oct 16 18:04:34 2012" +def remove_date(diff) + diff.gsub(/([+-]{3}.*)\t.*/, '\1 DATE') +end + +describe 'diff' do + include FileSystemSupport + + context 'with two filesystems with all types of difference' do + let(:a) { + memory_fs('a', { + :both_dirs => { + :sub_both_dirs => { :subsub => nil }, + :sub_both_files => nil, + :sub_both_files_different => "a\n", + :sub_both_dirs_empty => {}, + :sub_dirs_empty_in_a_filled_in_b => {}, + :sub_dirs_empty_in_b_filled_in_a => { :subsub => nil }, + :sub_a_only_dir => { :subsub => nil }, + :sub_a_only_file => nil, + :sub_dir_in_a_file_in_b => {}, + :sub_file_in_a_dir_in_b => nil + }, + :both_files => nil, + :both_files_different => "a\n", + :both_dirs_empty => {}, + :dirs_empty_in_a_filled_in_b => {}, + :dirs_empty_in_b_filled_in_a => { :subsub => nil }, + :dirs_in_a_cannot_be_in_b => {}, + :file_in_a_cannot_be_in_b => nil, + :a_only_dir => { :subsub => nil }, + :a_only_file => nil, + :dir_in_a_file_in_b => {}, + :file_in_a_dir_in_b => nil + }, /cannot_be_in_a/) + } + let(:b) { + memory_fs('b', { + :both_dirs => { + :sub_both_dirs => { :subsub => nil }, + :sub_both_files => nil, + :sub_both_files_different => "b\n", + :sub_both_dirs_empty => {}, + :sub_dirs_empty_in_a_filled_in_b => { :subsub => nil }, + :sub_dirs_empty_in_b_filled_in_a => {}, + :sub_b_only_dir => { :subsub => nil }, + :sub_b_only_file => nil, + :sub_dir_in_a_file_in_b => nil, + :sub_file_in_a_dir_in_b => {} + }, + :both_files => nil, + :both_files_different => "b\n", + :both_dirs_empty => {}, + :dirs_empty_in_a_filled_in_b => { :subsub => nil }, + :dirs_empty_in_b_filled_in_a => {}, + :dirs_in_b_cannot_be_in_a => {}, + :file_in_b_cannot_be_in_a => nil, + :b_only_dir => { :subsub => nil }, + :b_only_file => nil, + :dir_in_a_file_in_b => nil, + :file_in_a_dir_in_b => {} + }, /cannot_be_in_b/) + } + it 'Chef::ChefFS::CommandLine.diff(/)' do + results = [] + Chef::ChefFS::CommandLine.diff(pattern('/'), a, b, nil, nil) do |diff| + results << remove_date(diff) + end + results.should =~ [ + 'diff --knife a/both_dirs/sub_both_files_different b/both_dirs/sub_both_files_different +--- a/both_dirs/sub_both_files_different DATE ++++ b/both_dirs/sub_both_files_different DATE +@@ -1 +1 @@ +-a ++b +','diff --knife a/both_dirs/sub_dirs_empty_in_a_filled_in_b/subsub b/both_dirs/sub_dirs_empty_in_a_filled_in_b/subsub +new file +--- /dev/null DATE ++++ b/both_dirs/sub_dirs_empty_in_a_filled_in_b/subsub DATE +@@ -0,0 +1 @@ ++subsub +','diff --knife a/both_dirs/sub_dirs_empty_in_b_filled_in_a/subsub b/both_dirs/sub_dirs_empty_in_b_filled_in_a/subsub +deleted file +--- a/both_dirs/sub_dirs_empty_in_b_filled_in_a/subsub DATE ++++ /dev/null DATE +@@ -1 +0,0 @@ +-subsub +','Only in a/both_dirs: sub_a_only_dir +','diff --knife a/both_dirs/sub_a_only_file b/both_dirs/sub_a_only_file +deleted file +--- a/both_dirs/sub_a_only_file DATE ++++ /dev/null DATE +@@ -1 +0,0 @@ +-sub_a_only_file +','File b/both_dirs/sub_dir_in_a_file_in_b is a directory while file b/both_dirs/sub_dir_in_a_file_in_b is a regular file +','File a/both_dirs/sub_file_in_a_dir_in_b is a regular file while file a/both_dirs/sub_file_in_a_dir_in_b is a directory +','Only in b/both_dirs: sub_b_only_dir +','diff --knife a/both_dirs/sub_b_only_file b/both_dirs/sub_b_only_file +new file +--- /dev/null DATE ++++ b/both_dirs/sub_b_only_file DATE +@@ -0,0 +1 @@ ++sub_b_only_file +','diff --knife a/both_files_different b/both_files_different +--- a/both_files_different DATE ++++ b/both_files_different DATE +@@ -1 +1 @@ +-a ++b +','diff --knife a/dirs_empty_in_a_filled_in_b/subsub b/dirs_empty_in_a_filled_in_b/subsub +new file +--- /dev/null DATE ++++ b/dirs_empty_in_a_filled_in_b/subsub DATE +@@ -0,0 +1 @@ ++subsub +','diff --knife a/dirs_empty_in_b_filled_in_a/subsub b/dirs_empty_in_b_filled_in_a/subsub +deleted file +--- a/dirs_empty_in_b_filled_in_a/subsub DATE ++++ /dev/null DATE +@@ -1 +0,0 @@ +-subsub +','Only in a: a_only_dir +','diff --knife a/a_only_file b/a_only_file +deleted file +--- a/a_only_file DATE ++++ /dev/null DATE +@@ -1 +0,0 @@ +-a_only_file +','File b/dir_in_a_file_in_b is a directory while file b/dir_in_a_file_in_b is a regular file +','File a/file_in_a_dir_in_b is a regular file while file a/file_in_a_dir_in_b is a directory +','Only in b: b_only_dir +','diff --knife a/b_only_file b/b_only_file +new file +--- /dev/null DATE ++++ b/b_only_file DATE +@@ -0,0 +1 @@ ++b_only_file +' ] + end + it 'Chef::ChefFS::CommandLine.diff(/both_dirs)' do + results = [] + Chef::ChefFS::CommandLine.diff(pattern('/both_dirs'), a, b, nil, nil) do |diff| + results << remove_date(diff) + end + results.should =~ [ + 'diff --knife a/both_dirs/sub_both_files_different b/both_dirs/sub_both_files_different +--- a/both_dirs/sub_both_files_different DATE ++++ b/both_dirs/sub_both_files_different DATE +@@ -1 +1 @@ +-a ++b +','diff --knife a/both_dirs/sub_dirs_empty_in_a_filled_in_b/subsub b/both_dirs/sub_dirs_empty_in_a_filled_in_b/subsub +new file +--- /dev/null DATE ++++ b/both_dirs/sub_dirs_empty_in_a_filled_in_b/subsub DATE +@@ -0,0 +1 @@ ++subsub +','diff --knife a/both_dirs/sub_dirs_empty_in_b_filled_in_a/subsub b/both_dirs/sub_dirs_empty_in_b_filled_in_a/subsub +deleted file +--- a/both_dirs/sub_dirs_empty_in_b_filled_in_a/subsub DATE ++++ /dev/null DATE +@@ -1 +0,0 @@ +-subsub +','Only in a/both_dirs: sub_a_only_dir +','diff --knife a/both_dirs/sub_a_only_file b/both_dirs/sub_a_only_file +deleted file +--- a/both_dirs/sub_a_only_file DATE ++++ /dev/null DATE +@@ -1 +0,0 @@ +-sub_a_only_file +','File b/both_dirs/sub_dir_in_a_file_in_b is a directory while file b/both_dirs/sub_dir_in_a_file_in_b is a regular file +','File a/both_dirs/sub_file_in_a_dir_in_b is a regular file while file a/both_dirs/sub_file_in_a_dir_in_b is a directory +','Only in b/both_dirs: sub_b_only_dir +','diff --knife a/both_dirs/sub_b_only_file b/both_dirs/sub_b_only_file +new file +--- /dev/null DATE ++++ b/both_dirs/sub_b_only_file DATE +@@ -0,0 +1 @@ ++sub_b_only_file +' ] + end + it 'Chef::ChefFS::CommandLine.diff(/) with depth 1' do + results = [] + Chef::ChefFS::CommandLine.diff(pattern('/'), a, b, 1, nil) do |diff| + results << remove_date(diff) + end + results.should =~ [ +'Common subdirectories: /both_dirs +','diff --knife a/both_files_different b/both_files_different +--- a/both_files_different DATE ++++ b/both_files_different DATE +@@ -1 +1 @@ +-a ++b +','Common subdirectories: /both_dirs_empty +','Common subdirectories: /dirs_empty_in_b_filled_in_a +','Common subdirectories: /dirs_empty_in_a_filled_in_b +','Only in a: a_only_dir +','diff --knife a/a_only_file b/a_only_file +deleted file +--- a/a_only_file DATE ++++ /dev/null DATE +@@ -1 +0,0 @@ +-a_only_file +','File b/dir_in_a_file_in_b is a directory while file b/dir_in_a_file_in_b is a regular file +','File a/file_in_a_dir_in_b is a regular file while file a/file_in_a_dir_in_b is a directory +','Only in b: b_only_dir +','diff --knife a/b_only_file b/b_only_file +new file +--- /dev/null DATE ++++ b/b_only_file DATE +@@ -0,0 +1 @@ ++b_only_file +' ] + end + it 'Chef::ChefFS::CommandLine.diff(/*_*) with depth 0' do + results = [] + Chef::ChefFS::CommandLine.diff(pattern('/*_*'), a, b, 0, nil) do |diff| + results << remove_date(diff) + end + results.should =~ [ +'Common subdirectories: /both_dirs +','diff --knife a/both_files_different b/both_files_different +--- a/both_files_different DATE ++++ b/both_files_different DATE +@@ -1 +1 @@ +-a ++b +','Common subdirectories: /both_dirs_empty +','Common subdirectories: /dirs_empty_in_b_filled_in_a +','Common subdirectories: /dirs_empty_in_a_filled_in_b +','Only in a: a_only_dir +','diff --knife a/a_only_file b/a_only_file +deleted file +--- a/a_only_file DATE ++++ /dev/null DATE +@@ -1 +0,0 @@ +-a_only_file +','File b/dir_in_a_file_in_b is a directory while file b/dir_in_a_file_in_b is a regular file +','File a/file_in_a_dir_in_b is a regular file while file a/file_in_a_dir_in_b is a directory +','Only in b: b_only_dir +','diff --knife a/b_only_file b/b_only_file +new file +--- /dev/null DATE ++++ b/b_only_file DATE +@@ -0,0 +1 @@ ++b_only_file +' ] + end + it 'Chef::ChefFS::CommandLine.diff(/) in name-only mode' do + results = [] + Chef::ChefFS::CommandLine.diff(pattern('/'), a, b, nil, :name_only) do |diff| + results << remove_date(diff) + end + results.should =~ [ + "b/both_dirs/sub_both_files_different\n", + "b/both_dirs/sub_dirs_empty_in_b_filled_in_a/subsub\n", + "b/both_dirs/sub_dirs_empty_in_a_filled_in_b/subsub\n", + "b/both_dirs/sub_a_only_dir\n", + "b/both_dirs/sub_a_only_file\n", + "b/both_dirs/sub_b_only_dir\n", + "b/both_dirs/sub_b_only_file\n", + "b/both_dirs/sub_dir_in_a_file_in_b\n", + "b/both_dirs/sub_file_in_a_dir_in_b\n", + "b/both_files_different\n", + "b/dirs_empty_in_b_filled_in_a/subsub\n", + "b/dirs_empty_in_a_filled_in_b/subsub\n", + "b/a_only_dir\n", + "b/a_only_file\n", + "b/b_only_dir\n", + "b/b_only_file\n", + "b/dir_in_a_file_in_b\n", + "b/file_in_a_dir_in_b\n" + ] + end + it 'Chef::ChefFS::CommandLine.diff(/) in name-status mode' do + results = [] + Chef::ChefFS::CommandLine.diff(pattern('/'), a, b, nil, :name_status) do |diff| + results << remove_date(diff) + end + results.should =~ [ + "M\tb/both_dirs/sub_both_files_different\n", + "D\tb/both_dirs/sub_dirs_empty_in_b_filled_in_a/subsub\n", + "A\tb/both_dirs/sub_dirs_empty_in_a_filled_in_b/subsub\n", + "D\tb/both_dirs/sub_a_only_dir\n", + "D\tb/both_dirs/sub_a_only_file\n", + "A\tb/both_dirs/sub_b_only_dir\n", + "A\tb/both_dirs/sub_b_only_file\n", + "T\tb/both_dirs/sub_dir_in_a_file_in_b\n", + "T\tb/both_dirs/sub_file_in_a_dir_in_b\n", + "M\tb/both_files_different\n", + "D\tb/dirs_empty_in_b_filled_in_a/subsub\n", + "A\tb/dirs_empty_in_a_filled_in_b/subsub\n", + "D\tb/a_only_dir\n", + "D\tb/a_only_file\n", + "A\tb/b_only_dir\n", + "A\tb/b_only_file\n", + "T\tb/dir_in_a_file_in_b\n", + "T\tb/file_in_a_dir_in_b\n" + ] + end + end +end diff --git a/spec/unit/chef_fs/file_pattern_spec.rb b/spec/unit/chef_fs/file_pattern_spec.rb new file mode 100644 index 0000000000..247ec01a77 --- /dev/null +++ b/spec/unit/chef_fs/file_pattern_spec.rb @@ -0,0 +1,526 @@ +# +# Author:: John Keiser (<jkeiser@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 'chef/chef_fs/file_pattern' + +describe Chef::ChefFS::FilePattern do + def p(str) + Chef::ChefFS::FilePattern.new(str) + end + + # Different kinds of patterns + context 'with empty pattern ""' do + let(:pattern) { Chef::ChefFS::FilePattern.new('') } + it 'match?' do + pattern.match?('').should be_true + pattern.match?('/').should be_false + pattern.match?('a').should be_false + pattern.match?('a/b').should be_false + end + it 'exact_path' do + pattern.exact_path.should == '' + end + it 'could_match_children?' do + pattern.could_match_children?('').should be_false + pattern.could_match_children?('a/b').should be_false + end + end + + context 'with root pattern "/"' do + let(:pattern) { Chef::ChefFS::FilePattern.new('/') } + it 'match?' do + pattern.match?('/').should be_true + pattern.match?('').should be_false + pattern.match?('a').should be_false + pattern.match?('/a').should be_false + end + it 'exact_path' do + pattern.exact_path.should == '/' + end + it 'could_match_children?' do + pattern.could_match_children?('').should be_false + pattern.could_match_children?('/').should be_false + pattern.could_match_children?('a').should be_false + pattern.could_match_children?('a/b').should be_false + pattern.could_match_children?('/a').should be_false + end + end + + context 'with simple pattern "abc"' do + let(:pattern) { Chef::ChefFS::FilePattern.new('abc') } + it 'match?' do + pattern.match?('abc').should be_true + pattern.match?('a').should be_false + pattern.match?('abcd').should be_false + pattern.match?('/abc').should be_false + pattern.match?('').should be_false + pattern.match?('/').should be_false + end + it 'exact_path' do + pattern.exact_path.should == 'abc' + end + it 'could_match_children?' do + pattern.could_match_children?('').should be_false + pattern.could_match_children?('abc').should be_false + pattern.could_match_children?('/abc').should be_false + end + end + + context 'with simple pattern "/abc"' do + let(:pattern) { Chef::ChefFS::FilePattern.new('/abc') } + it 'match?' do + pattern.match?('/abc').should be_true + pattern.match?('abc').should be_false + pattern.match?('a').should be_false + pattern.match?('abcd').should be_false + pattern.match?('').should be_false + pattern.match?('/').should be_false + end + it 'exact_path' do + pattern.exact_path.should == '/abc' + end + it 'could_match_children?' do + pattern.could_match_children?('abc').should be_false + pattern.could_match_children?('/abc').should be_false + pattern.could_match_children?('/').should be_true + pattern.could_match_children?('').should be_false + end + it 'exact_child_name_under' do + pattern.exact_child_name_under('/').should == 'abc' + end + end + + context 'with simple pattern "abc/def/ghi"' do + let(:pattern) { Chef::ChefFS::FilePattern.new('abc/def/ghi') } + it 'match?' do + pattern.match?('abc/def/ghi').should be_true + pattern.match?('/abc/def/ghi').should be_false + pattern.match?('abc').should be_false + pattern.match?('abc/def').should be_false + end + it 'exact_path' do + pattern.exact_path.should == 'abc/def/ghi' + end + it 'could_match_children?' do + pattern.could_match_children?('abc').should be_true + pattern.could_match_children?('xyz').should be_false + pattern.could_match_children?('/abc').should be_false + pattern.could_match_children?('abc/def').should be_true + pattern.could_match_children?('abc/xyz').should be_false + pattern.could_match_children?('abc/def/ghi').should be_false + end + it 'exact_child_name_under' do + pattern.exact_child_name_under('abc').should == 'def' + pattern.exact_child_name_under('abc/def').should == 'ghi' + end + end + + context 'with simple pattern "/abc/def/ghi"' do + let(:pattern) { Chef::ChefFS::FilePattern.new('/abc/def/ghi') } + it 'match?' do + pattern.match?('/abc/def/ghi').should be_true + pattern.match?('abc/def/ghi').should be_false + pattern.match?('/abc').should be_false + pattern.match?('/abc/def').should be_false + end + it 'exact_path' do + pattern.exact_path.should == '/abc/def/ghi' + end + it 'could_match_children?' do + pattern.could_match_children?('/abc').should be_true + pattern.could_match_children?('/xyz').should be_false + pattern.could_match_children?('abc').should be_false + pattern.could_match_children?('/abc/def').should be_true + pattern.could_match_children?('/abc/xyz').should be_false + pattern.could_match_children?('/abc/def/ghi').should be_false + end + it 'exact_child_name_under' do + pattern.exact_child_name_under('/').should == 'abc' + pattern.exact_child_name_under('/abc').should == 'def' + pattern.exact_child_name_under('/abc/def').should == 'ghi' + end + end + + context 'with simple pattern "a\*\b"' do + let(:pattern) { Chef::ChefFS::FilePattern.new('a\*\b') } + it 'match?' do + pattern.match?('a*b').should be_true + pattern.match?('ab').should be_false + pattern.match?('acb').should be_false + pattern.match?('ab').should be_false + end + it 'exact_path' do + pattern.exact_path.should == 'a*b' + end + it 'could_match_children?' do + pattern.could_match_children?('a/*b').should be_false + end + end + + context 'with star pattern "/abc/*/ghi"' do + let(:pattern) { Chef::ChefFS::FilePattern.new('/abc/*/ghi') } + it 'match?' do + pattern.match?('/abc/def/ghi').should be_true + pattern.match?('/abc/ghi').should be_false + end + it 'exact_path' do + pattern.exact_path.should be_nil + end + it 'could_match_children?' do + pattern.could_match_children?('/abc').should be_true + pattern.could_match_children?('/xyz').should be_false + pattern.could_match_children?('abc').should be_false + pattern.could_match_children?('/abc/def').should be_true + pattern.could_match_children?('/abc/xyz').should be_true + pattern.could_match_children?('/abc/def/ghi').should be_false + end + it 'exact_child_name_under' do + pattern.exact_child_name_under('/').should == 'abc' + pattern.exact_child_name_under('/abc').should == nil + pattern.exact_child_name_under('/abc/def').should == 'ghi' + end + end + + context 'with star pattern "/abc/d*f/ghi"' do + let(:pattern) { Chef::ChefFS::FilePattern.new('/abc/d*f/ghi') } + it 'match?' do + pattern.match?('/abc/def/ghi').should be_true + pattern.match?('/abc/dxf/ghi').should be_true + pattern.match?('/abc/df/ghi').should be_true + pattern.match?('/abc/dxyzf/ghi').should be_true + pattern.match?('/abc/d/ghi').should be_false + pattern.match?('/abc/f/ghi').should be_false + pattern.match?('/abc/ghi').should be_false + pattern.match?('/abc/xyz/ghi').should be_false + end + it 'exact_path' do + pattern.exact_path.should be_nil + end + it 'could_match_children?' do + pattern.could_match_children?('/abc').should be_true + pattern.could_match_children?('/xyz').should be_false + pattern.could_match_children?('abc').should be_false + pattern.could_match_children?('/abc/def').should be_true + pattern.could_match_children?('/abc/xyz').should be_false + pattern.could_match_children?('/abc/dxyzf').should be_true + pattern.could_match_children?('/abc/df').should be_true + pattern.could_match_children?('/abc/d').should be_false + pattern.could_match_children?('/abc/f').should be_false + pattern.could_match_children?('/abc/def/ghi').should be_false + end + it 'exact_child_name_under' do + pattern.exact_child_name_under('/').should == 'abc' + pattern.exact_child_name_under('/abc').should == nil + pattern.exact_child_name_under('/abc/def').should == 'ghi' + end + end + + context 'with star pattern "/abc/d??f/ghi"' do + let(:pattern) { Chef::ChefFS::FilePattern.new('/abc/d??f/ghi') } + it 'match?' do + pattern.match?('/abc/deef/ghi').should be_true + pattern.match?('/abc/deeef/ghi').should be_false + pattern.match?('/abc/def/ghi').should be_false + pattern.match?('/abc/df/ghi').should be_false + pattern.match?('/abc/d/ghi').should be_false + pattern.match?('/abc/f/ghi').should be_false + pattern.match?('/abc/ghi').should be_false + end + it 'exact_path' do + pattern.exact_path.should be_nil + end + it 'could_match_children?' do + pattern.could_match_children?('/abc').should be_true + pattern.could_match_children?('/xyz').should be_false + pattern.could_match_children?('abc').should be_false + pattern.could_match_children?('/abc/deef').should be_true + pattern.could_match_children?('/abc/deeef').should be_false + pattern.could_match_children?('/abc/def').should be_false + pattern.could_match_children?('/abc/df').should be_false + pattern.could_match_children?('/abc/d').should be_false + pattern.could_match_children?('/abc/f').should be_false + pattern.could_match_children?('/abc/deef/ghi').should be_false + end + it 'exact_child_name_under' do + pattern.exact_child_name_under('/').should == 'abc' + pattern.exact_child_name_under('/abc').should == nil + pattern.exact_child_name_under('/abc/deef').should == 'ghi' + end + end + + context 'with star pattern "/abc/d[a-z][0-9]f/ghi"' do + let(:pattern) { Chef::ChefFS::FilePattern.new('/abc/d[a-z][0-9]f/ghi') } + it 'match?' do + pattern.match?('/abc/de1f/ghi').should be_true + pattern.match?('/abc/deef/ghi').should be_false + pattern.match?('/abc/d11f/ghi').should be_false + pattern.match?('/abc/de11f/ghi').should be_false + pattern.match?('/abc/dee1f/ghi').should be_false + pattern.match?('/abc/df/ghi').should be_false + pattern.match?('/abc/d/ghi').should be_false + pattern.match?('/abc/f/ghi').should be_false + pattern.match?('/abc/ghi').should be_false + end + it 'exact_path' do + pattern.exact_path.should be_nil + end + it 'could_match_children?' do + pattern.could_match_children?('/abc').should be_true + pattern.could_match_children?('/xyz').should be_false + pattern.could_match_children?('abc').should be_false + pattern.could_match_children?('/abc/de1f').should be_true + pattern.could_match_children?('/abc/deef').should be_false + pattern.could_match_children?('/abc/d11f').should be_false + pattern.could_match_children?('/abc/de11f').should be_false + pattern.could_match_children?('/abc/dee1f').should be_false + pattern.could_match_children?('/abc/def').should be_false + pattern.could_match_children?('/abc/df').should be_false + pattern.could_match_children?('/abc/d').should be_false + pattern.could_match_children?('/abc/f').should be_false + pattern.could_match_children?('/abc/de1f/ghi').should be_false + end + it 'exact_child_name_under' do + pattern.exact_child_name_under('/').should == 'abc' + pattern.exact_child_name_under('/abc').should == nil + pattern.exact_child_name_under('/abc/de1f').should == 'ghi' + end + end + + context 'with star pattern "/abc/**/ghi"' do + let(:pattern) { Chef::ChefFS::FilePattern.new('/abc/**/ghi') } + it 'match?' do + pattern.match?('/abc/def/ghi').should be_true + pattern.match?('/abc/d/e/f/ghi').should be_true + pattern.match?('/abc/ghi').should be_false + pattern.match?('/abcdef/d/ghi').should be_false + pattern.match?('/abc/d/defghi').should be_false + pattern.match?('/xyz').should be_false + end + it 'exact_path' do + pattern.exact_path.should be_nil + end + it 'could_match_children?' do + pattern.could_match_children?('/abc').should be_true + pattern.could_match_children?('/abc/d').should be_true + pattern.could_match_children?('/abc/d/e').should be_true + pattern.could_match_children?('/abc/d/e/f').should be_true + pattern.could_match_children?('/abc/def/ghi').should be_true + pattern.could_match_children?('abc').should be_false + pattern.could_match_children?('/xyz').should be_false + end + it 'exact_child_name_under' do + pattern.exact_child_name_under('/').should == 'abc' + pattern.exact_child_name_under('/abc').should == nil + pattern.exact_child_name_under('/abc/def').should == nil + end + end + + context 'with star pattern "/abc**/ghi"' do + let(:pattern) { Chef::ChefFS::FilePattern.new('/abc**/ghi') } + it 'match?' do + pattern.match?('/abc/def/ghi').should be_true + pattern.match?('/abc/d/e/f/ghi').should be_true + pattern.match?('/abc/ghi').should be_true + pattern.match?('/abcdef/ghi').should be_true + pattern.match?('/abc/defghi').should be_false + pattern.match?('/xyz').should be_false + end + it 'exact_path' do + pattern.exact_path.should be_nil + end + it 'could_match_children?' do + pattern.could_match_children?('/abc').should be_true + pattern.could_match_children?('/abcdef').should be_true + pattern.could_match_children?('/abc/d/e').should be_true + pattern.could_match_children?('/abc/d/e/f').should be_true + pattern.could_match_children?('/abc/def/ghi').should be_true + pattern.could_match_children?('abc').should be_false + end + it 'could_match_children? /abc** returns false for /xyz' do + pending 'Make could_match_children? more rigorous' do + # At the moment, we return false for this, but in the end it would be nice to return true: + pattern.could_match_children?('/xyz').should be_false + end + end + it 'exact_child_name_under' do + pattern.exact_child_name_under('/').should == nil + pattern.exact_child_name_under('/abc').should == nil + pattern.exact_child_name_under('/abc/def').should == nil + end + end + + context 'with star pattern "/abc/**ghi"' do + let(:pattern) { Chef::ChefFS::FilePattern.new('/abc/**ghi') } + it 'match?' do + pattern.match?('/abc/def/ghi').should be_true + pattern.match?('/abc/def/ghi/ghi').should be_true + pattern.match?('/abc/def/ghi/jkl').should be_false + pattern.match?('/abc/d/e/f/ghi').should be_true + pattern.match?('/abc/ghi').should be_true + pattern.match?('/abcdef/ghi').should be_false + pattern.match?('/abc/defghi').should be_true + pattern.match?('/xyz').should be_false + end + it 'exact_path' do + pattern.exact_path.should be_nil + end + it 'could_match_children?' do + pattern.could_match_children?('/abc').should be_true + pattern.could_match_children?('/abcdef').should be_false + pattern.could_match_children?('/abc/d/e').should be_true + pattern.could_match_children?('/abc/d/e/f').should be_true + pattern.could_match_children?('/abc/def/ghi').should be_true + pattern.could_match_children?('abc').should be_false + pattern.could_match_children?('/xyz').should be_false + end + it 'exact_child_name_under' do + pattern.exact_child_name_under('/').should == 'abc' + pattern.exact_child_name_under('/abc').should == nil + pattern.exact_child_name_under('/abc/def').should == nil + end + end + + context 'with star pattern "a**b**c"' do + let(:pattern) { Chef::ChefFS::FilePattern.new('a**b**c') } + it 'match?' do + pattern.match?('axybzwc').should be_true + pattern.match?('abc').should be_true + pattern.match?('axyzwc').should be_false + pattern.match?('ac').should be_false + pattern.match?('a/x/y/b/z/w/c').should be_true + end + it 'exact_path' do + pattern.exact_path.should be_nil + end + end + + context 'normalization tests' do + it 'handles trailing slashes' do + p('abc/').normalized_pattern.should == 'abc' + p('abc/').exact_path.should == 'abc' + p('abc/').match?('abc').should be_true + p('//').normalized_pattern.should == '/' + p('//').exact_path.should == '/' + p('//').match?('/').should be_true + p('/./').normalized_pattern.should == '/' + p('/./').exact_path.should == '/' + p('/./').match?('/').should be_true + end + it 'handles multiple slashes' do + p('abc//def').normalized_pattern.should == 'abc/def' + p('abc//def').exact_path.should == 'abc/def' + p('abc//def').match?('abc/def').should be_true + p('abc//').normalized_pattern.should == 'abc' + p('abc//').exact_path.should == 'abc' + p('abc//').match?('abc').should be_true + end + it 'handles dot' do + p('abc/./def').normalized_pattern.should == 'abc/def' + p('abc/./def').exact_path.should == 'abc/def' + p('abc/./def').match?('abc/def').should be_true + p('./abc/def').normalized_pattern.should == 'abc/def' + p('./abc/def').exact_path.should == 'abc/def' + p('./abc/def').match?('abc/def').should be_true + p('/.').normalized_pattern.should == '/' + p('/.').exact_path.should == '/' + p('/.').match?('/').should be_true + end + it 'handles dot by itself', :pending => "decide what to do with dot by itself" do + p('.').normalized_pattern.should == '.' + p('.').exact_path.should == '.' + p('.').match?('.').should be_true + p('./').normalized_pattern.should == '.' + p('./').exact_path.should == '.' + p('./').match?('.').should be_true + end + it 'handles dotdot' do + p('abc/../def').normalized_pattern.should == 'def' + p('abc/../def').exact_path.should == 'def' + p('abc/../def').match?('def').should be_true + p('abc/def/../..').normalized_pattern.should == '' + p('abc/def/../..').exact_path.should == '' + p('abc/def/../..').match?('').should be_true + p('/*/../def').normalized_pattern.should == '/def' + p('/*/../def').exact_path.should == '/def' + p('/*/../def').match?('/def').should be_true + p('/*/*/../def').normalized_pattern.should == '/*/def' + p('/*/*/../def').exact_path.should be_nil + p('/*/*/../def').match?('/abc/def').should be_true + p('/abc/def/../..').normalized_pattern.should == '/' + p('/abc/def/../..').exact_path.should == '/' + p('/abc/def/../..').match?('/').should be_true + p('abc/../../def').normalized_pattern.should == '../def' + p('abc/../../def').exact_path.should == '../def' + p('abc/../../def').match?('../def').should be_true + end + it 'handles dotdot with double star' do + p('abc**/def/../ghi').exact_path.should be_nil + p('abc**/def/../ghi').match?('abc/ghi').should be_true + p('abc**/def/../ghi').match?('abc/x/y/z/ghi').should be_true + p('abc**/def/../ghi').match?('ghi').should be_false + end + it 'raises error on dotdot with overlapping double star' do + lambda { Chef::ChefFS::FilePattern.new('abc/**/../def').exact_path }.should raise_error(ArgumentError) + lambda { Chef::ChefFS::FilePattern.new('abc/**/abc/../../def').exact_path }.should raise_error(ArgumentError) + end + it 'handles leading dotdot' do + p('../abc/def').exact_path.should == '../abc/def' + p('../abc/def').match?('../abc/def').should be_true + p('/../abc/def').exact_path.should == '/abc/def' + p('/../abc/def').match?('/abc/def').should be_true + p('..').exact_path.should == '..' + p('..').match?('..').should be_true + p('/..').exact_path.should == '/' + p('/..').match?('/').should be_true + end + end + + + # match? + # - single element matches (empty, fixed, ?, *, characters, escapes) + # - nested matches + # - absolute matches + # - trailing slashes + # - ** + + # exact_path + # - empty + # - single element and nested matches, with escapes + # - absolute and relative + # - ?, *, characters, ** + + # could_match_children? + # + # + # + # + context 'with pattern "abc"' do + end + + context 'with pattern "/abc"' do + end + + context 'with pattern "abc/def/ghi"' do + end + + context 'with pattern "/abc/def/ghi"' do + end + + # Exercise the different methods to their maximum +end diff --git a/spec/unit/chef_fs/file_system/chef_server_root_dir_spec.rb b/spec/unit/chef_fs/file_system/chef_server_root_dir_spec.rb new file mode 100644 index 0000000000..b60193c4ae --- /dev/null +++ b/spec/unit/chef_fs/file_system/chef_server_root_dir_spec.rb @@ -0,0 +1,237 @@ +# +# Author:: John Keiser (<jkeiser@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 'chef/chef_fs/file_system/chef_server_root_dir' + +describe Chef::ChefFS::FileSystem::ChefServerRootDir do + shared_examples 'a json endpoint dir leaf' do + it 'parent is endpoint' do + endpoint_leaf.parent.should == endpoint + end + it 'name is correct' do + endpoint_leaf.name.should == "#{endpoint_leaf_name}.json" + end + it 'path is correct' do + endpoint_leaf.path.should == "/#{endpoint_name}/#{endpoint_leaf_name}.json" + end + it 'path_for_printing is correct' do + endpoint_leaf.path_for_printing.should == "remote/#{endpoint_name}/#{endpoint_leaf_name}.json" + end + it 'is not a directory' do + endpoint_leaf.dir?.should be_false + end + it 'exists' do + should_receive_children + endpoint_leaf.exists?.should be_true + end + it 'read returns content' do + @rest.should_receive(:get_rest).with("#{endpoint_name}/#{endpoint_leaf_name}").once.and_return( + { + 'a' => 'b' + }) + endpoint_leaf.read.should == '{ + "a": "b" +}' + end + end + + shared_examples 'a json rest endpoint dir' do + it 'parent is root' do + endpoint.parent.should == root_dir + end + it 'has correct name' do + endpoint.name.should == endpoint_name + end + it 'has correct path' do + endpoint.path.should == "/#{endpoint_name}" + end + it 'has correct path_for_printing' do + endpoint.path_for_printing.should == "remote/#{endpoint_name}" + end + it 'is a directory' do + endpoint.dir?.should be_true + end + it 'exists' do + endpoint.exists?.should be_true + end + it 'can have json files as children' do + endpoint.can_have_child?('blah.json', false).should be_true + end + it 'cannot have non-json files as children' do + endpoint.can_have_child?('blah', false).should be_false + end + it 'cannot have directories as children' do + endpoint.can_have_child?('blah', true).should be_false + endpoint.can_have_child?('blah.json', true).should be_false + end + let(:should_receive_children) { + @rest.should_receive(:get_rest).with(endpoint_name).once.and_return( + { + "achild" => "http://opscode.com/achild", + "bchild" => "http://opscode.com/bchild" + }) + } + it 'has correct children' do + should_receive_children + endpoint.children.map { |child| child.name }.should =~ %w(achild.json bchild.json) + end + context 'achild in endpoint.children' do + let(:endpoint_leaf_name) { 'achild' } + let(:endpoint_leaf) do + should_receive_children + endpoint.children.select { |child| child.name == 'achild.json' }.first + end + it_behaves_like 'a json endpoint dir leaf' + end + context 'endpoint.child(achild)' do + let(:endpoint_leaf_name) { 'achild' } + let(:endpoint_leaf) { endpoint.child('achild.json') } + it_behaves_like 'a json endpoint dir leaf' + end + context 'nonexistent child()' do + let(:nonexistent_child) { endpoint.child('blah.json') } + it 'has correct parent, name, path and path_for_printing' do + nonexistent_child.parent.should == endpoint + nonexistent_child.name.should == "blah.json" + nonexistent_child.path.should == "#{endpoint.path}/blah.json" + nonexistent_child.path_for_printing.should == "#{endpoint.path_for_printing}/blah.json" + end + it 'does not exist' do + should_receive_children + nonexistent_child.exists?.should be_false + end + it 'is not a directory' do + nonexistent_child.dir?.should be_false + end + it 'read returns NotFoundError' do + @rest.should_receive(:get_rest).with("#{endpoint_name}/blah").once.and_raise(Net::HTTPServerException.new(nil,Net::HTTPResponse.new(nil,'404',nil))) + expect { nonexistent_child.read }.to raise_error(Chef::ChefFS::FileSystem::NotFoundError) + end + end + end + + let(:root_dir) { + Chef::ChefFS::FileSystem::ChefServerRootDir.new('remote', + { + :chef_server_url => 'url', + :node_name => 'username', + :client_key => 'key' + }, 'everything') + } + before(:each) do + @rest = double("rest") + Chef::REST.stub(:new).with('url','username','key') { @rest } + end + context 'the root directory' do + it 'has no parent' do + root_dir.parent.should == nil + end + it 'is a directory' do + root_dir.dir?.should be_true + end + it 'exists' do + root_dir.exists?.should be_true + end + it 'has name ""' do + root_dir.name.should == "" + end + it 'has path /' do + root_dir.path.should == '/' + end + it 'has path_for_printing remote/' do + root_dir.path_for_printing.should == 'remote/' + end + it 'has correct children' do + root_dir.children.map { |child| child.name }.should =~ %w(clients cookbooks data_bags environments nodes roles users) + end + it 'can have children with the known names' do + %w(clients cookbooks data_bags environments nodes roles users).each { |child| root_dir.can_have_child?(child, true).should be_true } + end + it 'cannot have files as children' do + %w(clients cookbooks data_bags environments nodes roles users).each { |child| root_dir.can_have_child?(child, false).should be_false } + root_dir.can_have_child?('blah', false).should be_false + end + it 'cannot have other child directories than the known names' do + root_dir.can_have_child?('blah', true).should be_false + end + it 'child() responds to children' do + %w(clients cookbooks data_bags environments nodes roles users).each { |child| root_dir.child(child).exists?.should be_true } + end + context 'nonexistent child()' do + let(:nonexistent_child) { root_dir.child('blah') } + it 'has correct parent, name, path and path_for_printing' do + nonexistent_child.parent.should == root_dir + nonexistent_child.name.should == "blah" + nonexistent_child.path.should == "/blah" + nonexistent_child.path_for_printing.should == "remote/blah" + end + it 'does not exist' do + nonexistent_child.exists?.should be_false + end + it 'is not a directory' do + nonexistent_child.dir?.should be_false + end + it 'read returns NotFoundError' do + expect { nonexistent_child.read }.to raise_error(Chef::ChefFS::FileSystem::NotFoundError) + end + end + end + + context 'clients in children' do + let(:endpoint_name) { 'clients' } + let(:endpoint) { root_dir.children.select { |child| child.name == 'clients' }.first } + + it_behaves_like 'a json rest endpoint dir' + end + + context 'root.child(clients)' do + let(:endpoint_name) { 'clients' } + let(:endpoint) { root_dir.child('clients') } + + it_behaves_like 'a json rest endpoint dir' + end + + context 'root.child(environments)' do + let(:endpoint_name) { 'environments' } + let(:endpoint) { root_dir.child('environments') } + + it_behaves_like 'a json rest endpoint dir' + end + + context 'root.child(nodes)' do + let(:endpoint_name) { 'nodes' } + let(:endpoint) { root_dir.child('nodes') } + + it_behaves_like 'a json rest endpoint dir' + end + + context 'root.child(roles)' do + let(:endpoint_name) { 'roles' } + let(:endpoint) { root_dir.child('roles') } + + it_behaves_like 'a json rest endpoint dir' + end + + context 'root.child(users)' do + let(:endpoint_name) { 'users' } + let(:endpoint) { root_dir.child('users') } + + it_behaves_like 'a json rest endpoint dir' + end +end diff --git a/spec/unit/chef_fs/file_system/cookbooks_dir_spec.rb b/spec/unit/chef_fs/file_system/cookbooks_dir_spec.rb new file mode 100644 index 0000000000..35a777a9e4 --- /dev/null +++ b/spec/unit/chef_fs/file_system/cookbooks_dir_spec.rb @@ -0,0 +1,568 @@ +# +# Author:: John Keiser (<jkeiser@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 'chef/chef_fs/file_system/chef_server_root_dir' +require 'chef/chef_fs/file_system' + +describe Chef::ChefFS::FileSystem::CookbooksDir do + let(:root_dir) { + Chef::ChefFS::FileSystem::ChefServerRootDir.new('remote', + { + :chef_server_url => 'url', + :node_name => 'username', + :client_key => 'key' + }, + 'everything') + } + let(:cookbooks_dir) { root_dir.child('cookbooks') } + let(:should_list_cookbooks) do + @rest.should_receive(:get_rest).with('cookbooks').once.and_return( + { + "achild" => "http://opscode.com/achild", + "bchild" => "http://opscode.com/bchild" + }) + end + before(:each) do + @rest = double("rest") + Chef::REST.stub(:new).with('url','username','key') { @rest } + end + + it 'has / as parent' do + cookbooks_dir.parent.should == root_dir + end + it 'is a directory' do + cookbooks_dir.dir?.should be_true + end + it 'exists' do + cookbooks_dir.exists?.should be_true + end + it 'has name cookbooks' do + cookbooks_dir.name.should == 'cookbooks' + end + it 'has path /cookbooks' do + cookbooks_dir.path.should == '/cookbooks' + end + it 'has path_for_printing remote/cookbooks' do + cookbooks_dir.path_for_printing.should == 'remote/cookbooks' + end + it 'has correct children' do + should_list_cookbooks + cookbooks_dir.children.map { |child| child.name }.should =~ %w(achild bchild) + end + it 'can have directories as children' do + cookbooks_dir.can_have_child?('blah', true).should be_true + end + it 'cannot have files as children' do + cookbooks_dir.can_have_child?('blah', false).should be_false + end + + # + # Cookbook dir (/cookbooks/<blah>) + # + shared_examples_for 'a segment directory' do + it 'has cookbook as parent' do + segment_dir.parent.should == cookbook_dir + end + it 'exists' do + segment_dir.exists?.should be_true + end + it 'is a directory' do + segment_dir.dir?.should be_true + end + it 'name is correct' do + segment_dir.name.should == segment_dir_name + end + it 'path is correct' do + segment_dir.path.should == "/cookbooks/#{cookbook_dir_name}/#{segment_dir_name}" + end + it 'path_for_printing is correct' do + segment_dir.path_for_printing.should == "remote/cookbooks/#{cookbook_dir_name}/#{segment_dir_name}" + end + it 'has the right children' do + segment_dir.children =~ %w(a.rb b.txt subdir) + end + it 'children are identical to child()' do + segment_dir.child('a.rb').should == segment_dir.children.select { |child| child.name == 'a.rb' }.first + segment_dir.child('b.txt').should == segment_dir.children.select { |child| child.name == 'b.txt' }.first + segment_dir.child('subdir').should == segment_dir.children.select { |child| child.name == 'subdir' }.first + end + context 'subdirectory' do + it 'has segment as a parent' do + segment_dir.child('subdir').parent.should == segment_dir + end + it 'exists' do + segment_dir.child('subdir').exists?.should be_true + end + it 'is a directory' do + segment_dir.child('subdir').dir?.should be_true + end + it 'name is subdir' do + segment_dir.child('subdir').name.should == 'subdir' + end + it 'path is correct' do + segment_dir.child('subdir').path.should == "/cookbooks/#{cookbook_dir_name}/#{segment_dir_name}/subdir" + end + it 'path_for_printing is correct' do + segment_dir.child('subdir').path_for_printing.should == "remote/cookbooks/#{cookbook_dir_name}/#{segment_dir_name}/subdir" + end + it 'has the right children' do + segment_dir.child('subdir').children =~ %w(a.rb b.txt) + end + it 'children are identical to child()' do + segment_dir.child('subdir').child('a.rb').should == segment_dir.child('subdir').children.select { |child| child.name == 'a.rb' }.first + segment_dir.child('subdir').child('b.txt').should == segment_dir.child('subdir').children.select { |child| child.name == 'b.txt' }.first + end + end + end + + shared_examples_for 'a cookbook' do + it 'has cookbooks as parent' do + cookbook_dir.parent == cookbooks_dir + end + it 'is a directory' do + should_list_cookbooks + cookbook_dir.dir?.should be_true + end + it 'exists' do + should_list_cookbooks + cookbook_dir.exists?.should be_true + end + it 'has name <cookbook name>' do + cookbook_dir.name.should == cookbook_dir_name + end + it 'has path /cookbooks/<cookbook name>' do + cookbook_dir.path.should == "/cookbooks/#{cookbook_dir_name}" + end + it 'has path_for_printing remote/cookbooks/<cookbook name>' do + cookbook_dir.path_for_printing.should == "remote/cookbooks/#{cookbook_dir_name}" + end + it 'can have segment directories as children' do + cookbook_dir.can_have_child?('attributes', true).should be_true + cookbook_dir.can_have_child?('definitions', true).should be_true + cookbook_dir.can_have_child?('recipes', true).should be_true + cookbook_dir.can_have_child?('libraries', true).should be_true + cookbook_dir.can_have_child?('templates', true).should be_true + cookbook_dir.can_have_child?('files', true).should be_true + cookbook_dir.can_have_child?('resources', true).should be_true + cookbook_dir.can_have_child?('providers', true).should be_true + end + it 'cannot have arbitrary directories as children' do + cookbook_dir.can_have_child?('blah', true).should be_false + cookbook_dir.can_have_child?('root_files', true).should be_false + end + it 'can have files as children' do + cookbook_dir.can_have_child?('blah', false).should be_true + cookbook_dir.can_have_child?('root_files', false).should be_true + cookbook_dir.can_have_child?('attributes', false).should be_true + cookbook_dir.can_have_child?('definitions', false).should be_true + cookbook_dir.can_have_child?('recipes', false).should be_true + cookbook_dir.can_have_child?('libraries', false).should be_true + cookbook_dir.can_have_child?('templates', false).should be_true + cookbook_dir.can_have_child?('files', false).should be_true + cookbook_dir.can_have_child?('resources', false).should be_true + cookbook_dir.can_have_child?('providers', false).should be_true + end + # TODO test empty parts, cross-contamination (root_files named templates/x.txt, libraries named recipes/blah.txt) + context 'with a full directory structure' do + def json_file(path, checksum) + filename = Chef::ChefFS::PathUtils.split(path)[-1] + { + :name => filename, + :url => "cookbook_file:#{path}", + :checksum => checksum, + :path => path, + :specificity => "default" + } + end + def json_files(cookbook_dir) + result = [] + files.each do |filename| + if filename =~ /^#{cookbook_dir}\// + result << json_file(filename, file_checksums[filename]) + end + end + result + end + let(:files) { + result = [] + %w(attributes definitions files libraries providers recipes resources templates).each do |segment| + result << "#{segment}/a.rb" + result << "#{segment}/b.txt" + result << "#{segment}/subdir/a.rb" + result << "#{segment}/subdir/b.txt" + end + result << 'a.rb' + result << 'b.txt' + result << 'subdir/a.rb' + result << 'subdir/b.txt' + result << 'root_files' + result + } + let(:file_checksums) { + result = {} + files.each_with_index do |file, i| + result[file] = i.to_s(16) + end + result + } + let(:should_get_cookbook) do + cookbook = double('cookbook') + cookbook.should_receive(:manifest).and_return({ + :attributes => json_files('attributes'), + :definitions => json_files('definitions'), + :files => json_files('files'), + :libraries => json_files('libraries'), + :providers => json_files('providers'), + :recipes => json_files('recipes'), + :resources => json_files('resources'), + :templates => json_files('templates'), + :root_files => [ + json_file('a.rb', file_checksums['a.rb']), + json_file('b.txt', file_checksums['b.txt']), + json_file('subdir/a.rb', file_checksums['subdir/a.rb']), + json_file('subdir/b.txt', file_checksums['subdir/b.txt']), + json_file('root_files', file_checksums['root_files']) + ] + }) + @rest.should_receive(:get_rest).with("cookbooks/#{cookbook_dir_name}/_latest").once.and_return(cookbook) + end + + it 'has correct children' do + should_get_cookbook + cookbook_dir.children.map { |child| child.name }.should =~ %w(attributes definitions files libraries providers recipes resources templates a.rb b.txt subdir root_files) + end + it 'children and child() yield the exact same objects' do + should_get_cookbook + cookbook_dir.children.each { |child| child.should == cookbook_dir.child(child.name) } + end + it 'all files exist (recursive) and have correct parent, path, path_for_printing, checksum and type' do + should_get_cookbook + file_checksums.each do |path, checksum| + file = Chef::ChefFS::FileSystem.resolve_path(cookbook_dir, path) + file_parts = path.split('/') + if file_parts.length == 3 + file.parent.parent.parent.should == cookbook_dir + elsif file_parts.length == 2 + file.parent.parent.should == cookbook_dir + else + file.parent.should == cookbook_dir + end + file.exists?.should be_true + file.dir?.should be_false + file.name.should == file_parts[-1] + file.path.should == "/cookbooks/#{cookbook_dir_name}/#{path}" + file.path_for_printing.should == "remote/cookbooks/#{cookbook_dir_name}/#{path}" + file.checksum.should == checksum + end + end + it 'all files can be read' do + should_get_cookbook + files.each do |path| + @rest.should_receive(:get_rest).with("cookbook_file:#{path}").once.and_return("This is #{path}'s content") + @rest.should_receive(:sign_on_redirect).with(no_args()).once.and_return(true) + @rest.should_receive(:sign_on_redirect=).with(false).once + @rest.should_receive(:sign_on_redirect=).with(true).once + file = Chef::ChefFS::FileSystem.resolve_path(cookbook_dir, path) + file.read.should == "This is #{path}'s content" + end + end + + context 'the attributes segment' do + let(:segment_dir) { cookbook_dir.child('attributes') } + let(:segment_dir_name) { 'attributes' } + it_behaves_like 'a segment directory' + + before(:each) do + should_get_cookbook + end + + it 'can have ruby files' do + should_get_cookbook + segment_dir.can_have_child?('blah.rb', false).should be_true + segment_dir.can_have_child?('.blah.rb', false).should be_true + end + it 'cannot have non-ruby files' do + should_get_cookbook + segment_dir.can_have_child?('blah.txt', false).should be_false + segment_dir.can_have_child?('.blah.txt', false).should be_false + end + it 'cannot have subdirectories' do + should_get_cookbook + segment_dir.can_have_child?('blah', true).should be_false + end + end + + context 'the definitions segment' do + let(:segment_dir) { cookbook_dir.child('definitions') } + let(:segment_dir_name) { 'definitions' } + it_behaves_like 'a segment directory' + + before(:each) do + should_get_cookbook + end + + it 'can have ruby files' do + segment_dir.can_have_child?('blah.rb', false).should be_true + segment_dir.can_have_child?('.blah.rb', false).should be_true + end + it 'cannot have non-ruby files' do + segment_dir.can_have_child?('blah.txt', false).should be_false + segment_dir.can_have_child?('.blah.txt', false).should be_false + end + it 'cannot have subdirectories' do + segment_dir.can_have_child?('blah', true).should be_false + end + end + + context 'the files segment' do + let(:segment_dir) { cookbook_dir.child('files') } + let(:segment_dir_name) { 'files' } + it_behaves_like 'a segment directory' + + before(:each) do + should_get_cookbook + end + + it 'can have ruby files' do + segment_dir.can_have_child?('blah.rb', false).should be_true + segment_dir.can_have_child?('.blah.rb', false).should be_true + end + it 'can have non-ruby files' do + segment_dir.can_have_child?('blah.txt', false).should be_true + segment_dir.can_have_child?('.blah.txt', false).should be_true + end + it 'can have subdirectories' do + segment_dir.can_have_child?('blah', true).should be_true + end + it 'subdirectories can have ruby files' do + segment_dir.child('subdir').can_have_child?('blah.rb', false).should be_true + segment_dir.child('subdir').can_have_child?('.blah.rb', false).should be_true + end + it 'subdirectories can have non-ruby files' do + segment_dir.child('subdir').can_have_child?('blah.txt', false).should be_true + segment_dir.child('subdir').can_have_child?('.blah.txt', false).should be_true + end + it 'subdirectories can have subdirectories' do + segment_dir.child('subdir').can_have_child?('blah', true).should be_true + end + end + + context 'the libraries segment' do + let(:segment_dir) { cookbook_dir.child('libraries') } + let(:segment_dir_name) { 'libraries' } + it_behaves_like 'a segment directory' + + before(:each) do + should_get_cookbook + end + + it 'can have ruby files' do + segment_dir.can_have_child?('blah.rb', false).should be_true + segment_dir.can_have_child?('.blah.rb', false).should be_true + end + it 'cannot have non-ruby files' do + segment_dir.can_have_child?('blah.txt', false).should be_false + segment_dir.can_have_child?('.blah.txt', false).should be_false + end + it 'cannot have subdirectories' do + segment_dir.can_have_child?('blah', true).should be_false + end + end + + context 'the providers segment' do + let(:segment_dir) { cookbook_dir.child('providers') } + let(:segment_dir_name) { 'providers' } + it_behaves_like 'a segment directory' + + before(:each) do + should_get_cookbook + end + + it 'can have ruby files' do + segment_dir.can_have_child?('blah.rb', false).should be_true + segment_dir.can_have_child?('.blah.rb', false).should be_true + end + it 'cannot have non-ruby files' do + segment_dir.can_have_child?('blah.txt', false).should be_false + segment_dir.can_have_child?('.blah.txt', false).should be_false + end + it 'can have subdirectories' do + segment_dir.can_have_child?('blah', true).should be_true + end + it 'subdirectories can have ruby files' do + segment_dir.child('subdir').can_have_child?('blah.rb', false).should be_true + segment_dir.child('subdir').can_have_child?('.blah.rb', false).should be_true + end + it 'subdirectories cannot have non-ruby files' do + segment_dir.child('subdir').can_have_child?('blah.txt', false).should be_false + segment_dir.child('subdir').can_have_child?('.blah.txt', false).should be_false + end + it 'subdirectories can have subdirectories' do + segment_dir.child('subdir').can_have_child?('blah', true).should be_true + end + end + + context 'the recipes segment' do + let(:segment_dir) { cookbook_dir.child('recipes') } + let(:segment_dir_name) { 'recipes' } + it_behaves_like 'a segment directory' + + before(:each) do + should_get_cookbook + end + + it 'can have ruby files' do + segment_dir.can_have_child?('blah.rb', false).should be_true + segment_dir.can_have_child?('.blah.rb', false).should be_true + end + it 'cannot have non-ruby files' do + segment_dir.can_have_child?('blah.txt', false).should be_false + segment_dir.can_have_child?('.blah.txt', false).should be_false + end + it 'cannot have subdirectories' do + segment_dir.can_have_child?('blah', true).should be_false + end + end + + context 'the resources segment' do + let(:segment_dir) { cookbook_dir.child('resources') } + let(:segment_dir_name) { 'resources' } + it_behaves_like 'a segment directory' + + before(:each) do + should_get_cookbook + end + + it 'can have ruby files' do + segment_dir.can_have_child?('blah.rb', false).should be_true + segment_dir.can_have_child?('.blah.rb', false).should be_true + end + it 'cannot have non-ruby files' do + segment_dir.can_have_child?('blah.txt', false).should be_false + segment_dir.can_have_child?('.blah.txt', false).should be_false + end + it 'can have subdirectories' do + segment_dir.can_have_child?('blah', true).should be_true + end + it 'subdirectories can have ruby files' do + segment_dir.child('subdir').can_have_child?('blah.rb', false).should be_true + segment_dir.child('subdir').can_have_child?('.blah.rb', false).should be_true + end + it 'subdirectories cannot have non-ruby files' do + segment_dir.child('subdir').can_have_child?('blah.txt', false).should be_false + segment_dir.child('subdir').can_have_child?('.blah.txt', false).should be_false + end + it 'subdirectories can have subdirectories' do + segment_dir.child('subdir').can_have_child?('blah', true).should be_true + end + end + + context 'the templates segment' do + let(:segment_dir) { cookbook_dir.child('templates') } + let(:segment_dir_name) { 'templates' } + it_behaves_like 'a segment directory' + + before(:each) do + should_get_cookbook + end + + it 'can have ruby files' do + segment_dir.can_have_child?('blah.rb', false).should be_true + segment_dir.can_have_child?('.blah.rb', false).should be_true + end + it 'can have non-ruby files' do + segment_dir.can_have_child?('blah.txt', false).should be_true + segment_dir.can_have_child?('.blah.txt', false).should be_true + end + it 'can have subdirectories' do + segment_dir.can_have_child?('blah', true).should be_true + end + it 'subdirectories can have ruby files' do + segment_dir.child('subdir').can_have_child?('blah.rb', false).should be_true + segment_dir.child('subdir').can_have_child?('.blah.rb', false).should be_true + end + it 'subdirectories can have non-ruby files' do + segment_dir.child('subdir').can_have_child?('blah.txt', false).should be_true + segment_dir.child('subdir').can_have_child?('.blah.txt', false).should be_true + end + it 'subdirectories can have subdirectories' do + segment_dir.child('subdir').can_have_child?('blah', true).should be_true + end + end + + context 'root subdirectories' do + let(:root_subdir) { cookbook_dir.child('subdir') } + + before(:each) do + should_get_cookbook + end + + # Really, since these shouldn't exist in the first place, + # it doesn't matter; but these REALLY shouldn't be able to + # have any files in them at all. + it 'can have ruby files' do + root_subdir.can_have_child?('blah.rb', false).should be_true + root_subdir.can_have_child?('.blah.rb', false).should be_true + end + it 'can have non-ruby files' do + root_subdir.can_have_child?('blah.txt', false).should be_true + root_subdir.can_have_child?('.blah.txt', false).should be_true + end + it 'cannot have subdirectories' do + root_subdir.can_have_child?('blah', true).should be_false + end + end + end + end + + context 'achild from cookbooks_dir.children' do + let(:cookbook_dir_name) { 'achild' } + let(:cookbook_dir) do + should_list_cookbooks + cookbooks_dir.children.select { |child| child.name == 'achild' }.first + end + it_behaves_like 'a cookbook' + end + context 'cookbooks_dir.child(achild)' do + let(:cookbook_dir_name) { 'achild' } + let(:cookbook_dir) { cookbooks_dir.child('achild') } + it_behaves_like 'a cookbook' + end + context 'nonexistent cookbooks_dir.child()' do + let(:nonexistent_child) { cookbooks_dir.child('blah') } + it 'has correct parent, name, path and path_for_printing' do + nonexistent_child.parent.should == cookbooks_dir + nonexistent_child.name.should == "blah" + nonexistent_child.path.should == "/cookbooks/blah" + nonexistent_child.path_for_printing.should == "remote/cookbooks/blah" + end + it 'does not exist' do + should_list_cookbooks + nonexistent_child.exists?.should be_false + end + it 'is a directory' do + should_list_cookbooks + nonexistent_child.dir?.should be_false + end + it 'read returns NotFoundError' do + expect { nonexistent_child.read }.to raise_error(Chef::ChefFS::FileSystem::NotFoundError) + end + end + +end diff --git a/spec/unit/chef_fs/file_system/data_bags_dir_spec.rb b/spec/unit/chef_fs/file_system/data_bags_dir_spec.rb new file mode 100644 index 0000000000..c8d29da84e --- /dev/null +++ b/spec/unit/chef_fs/file_system/data_bags_dir_spec.rb @@ -0,0 +1,220 @@ +# +# Author:: John Keiser (<jkeiser@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 'chef/chef_fs/file_system/chef_server_root_dir' + +describe Chef::ChefFS::FileSystem::DataBagsDir do + let(:root_dir) { + Chef::ChefFS::FileSystem::ChefServerRootDir.new('remote', + { + :chef_server_url => 'url', + :node_name => 'username', + :client_key => 'key' + }, 'everything') + } + let(:data_bags_dir) { root_dir.child('data_bags') } + let(:should_list_data_bags) do + @rest.should_receive(:get_rest).with('data').once.and_return( + { + "achild" => "http://opscode.com/achild", + "bchild" => "http://opscode.com/bchild" + }) + end + before(:each) do + @rest = double("rest") + Chef::REST.stub(:new).with('url','username','key') { @rest } + end + + it 'has / as parent' do + data_bags_dir.parent.should == root_dir + end + it 'is a directory' do + data_bags_dir.dir?.should be_true + end + it 'exists' do + data_bags_dir.exists?.should be_true + end + it 'has name data_bags' do + data_bags_dir.name.should == 'data_bags' + end + it 'has path /data_bags' do + data_bags_dir.path.should == '/data_bags' + end + it 'has path_for_printing remote/data_bags' do + data_bags_dir.path_for_printing.should == 'remote/data_bags' + end + it 'has correct children' do + should_list_data_bags + data_bags_dir.children.map { |child| child.name }.should =~ %w(achild bchild) + end + it 'can have directories as children' do + data_bags_dir.can_have_child?('blah', true).should be_true + end + it 'cannot have files as children' do + data_bags_dir.can_have_child?('blah', false).should be_false + end + + shared_examples_for 'a data bag item' do + it 'has data bag as parent' do + data_bag_item.parent.should == data_bag_dir + end + it 'is not a directory' do + data_bag_item.dir?.should be_false + end + it 'exists' do + should_list_data_bag_items + data_bag_item.exists?.should be_true + end + it 'has correct name' do + data_bag_item.name.should == data_bag_item_name + end + it 'has correct path' do + data_bag_item.path.should == "/data_bags/#{data_bag_dir_name}/#{data_bag_item_name}" + end + it 'has correct path_for_printing' do + data_bag_item.path_for_printing.should == "remote/data_bags/#{data_bag_dir_name}/#{data_bag_item_name}" + end + it 'reads correctly' do + @rest.should_receive(:get_rest).with("data/#{data_bag_dir_name}/#{data_bag_item_short_name}").once.and_return({ + 'a' => 'b' + }) + data_bag_item.read.should == '{ + "a": "b" +}' + end + end + + shared_examples_for 'a data bag' do + let(:should_list_data_bag_items) do + @rest.should_receive(:get_rest).with("data/#{data_bag_dir_name}").once.and_return( + { + "aitem" => "http://opscode.com/achild", + "bitem" => "http://opscode.com/bchild" + }) + end + it 'has /data as a parent' do + data_bag_dir.parent.should == data_bags_dir + end + it 'is a directory' do + should_list_data_bags + data_bag_dir.dir?.should be_true + end + it 'exists' do + should_list_data_bags + data_bag_dir.exists?.should be_true + end + it 'has correct name' do + data_bag_dir.name.should == data_bag_dir_name + end + it 'has correct path' do + data_bag_dir.path.should == "/data_bags/#{data_bag_dir_name}" + end + it 'has correct path_for_printing' do + data_bag_dir.path_for_printing.should == "remote/data_bags/#{data_bag_dir_name}" + end + it 'has correct children' do + should_list_data_bag_items + data_bag_dir.children.map { |child| child.name }.should =~ %w(aitem.json bitem.json) + end + it 'can have json files as children' do + data_bag_dir.can_have_child?('blah.json', false).should be_true + end + it 'cannot have non-json files as children' do + data_bag_dir.can_have_child?('blah', false).should be_false + end + it 'cannot have directories as children' do + data_bag_dir.can_have_child?('blah', true).should be_false + data_bag_dir.can_have_child?('blah.json', true).should be_false + end + context 'aitem from data_bag.children' do + let(:data_bag_item) do + should_list_data_bag_items + data_bag_dir.children.select { |child| child.name == 'aitem.json' }.first + end + let(:data_bag_item_short_name) { 'aitem' } + let(:data_bag_item_name) { 'aitem.json' } + it_behaves_like 'a data bag item' + end + context 'data_bag.child(aitem)' do + let(:data_bag_item) { data_bag_dir.child('aitem.json') } + let(:data_bag_item_short_name) { 'aitem' } + let(:data_bag_item_name) { 'aitem.json' } + it_behaves_like 'a data bag item' + end + context 'nonexistent child()' do + let(:nonexistent_child) { data_bag_dir.child('blah.json') } + it 'has correct parent, name, path and path_for_printing' do + nonexistent_child.parent.should == data_bag_dir + nonexistent_child.name.should == "blah.json" + nonexistent_child.path.should == "/data_bags/#{data_bag_dir_name}/blah.json" + nonexistent_child.path_for_printing.should == "remote/data_bags/#{data_bag_dir_name}/blah.json" + end + it 'does not exist' do + should_list_data_bag_items + nonexistent_child.exists?.should be_false + end + it 'is not a directory' do + nonexistent_child.dir?.should be_false + end + it 'read returns NotFoundError' do + @rest.should_receive(:get_rest).with("data/#{data_bag_dir_name}/blah").once.and_raise(Net::HTTPServerException.new(nil,Net::HTTPResponse.new(nil,'404',nil))) + expect { nonexistent_child.read }.to raise_error(Chef::ChefFS::FileSystem::NotFoundError) + end + end + end + + context 'achild from data_bags.children' do + let(:data_bag_dir) do + should_list_data_bags + data_bags_dir.children.select { |child| child.name == 'achild' }.first + end + let(:data_bag_dir_name) { 'achild' } + it_behaves_like 'a data bag' + end + + context 'data_bags.child(achild)' do + let(:data_bag_dir) do + data_bags_dir.child('achild') + end + let(:data_bag_dir_name) { 'achild' } + it_behaves_like 'a data bag' + end + + context 'nonexistent child()' do + let(:nonexistent_child) { data_bags_dir.child('blah') } + it 'has correct parent, name, path and path_for_printing' do + nonexistent_child.parent.should == data_bags_dir + nonexistent_child.name.should == "blah" + nonexistent_child.path.should == "/data_bags/blah" + nonexistent_child.path_for_printing.should == "remote/data_bags/blah" + end + it 'does not exist' do + should_list_data_bags + nonexistent_child.exists?.should be_false + end + it 'is not a directory' do + should_list_data_bags + nonexistent_child.dir?.should be_false + end + it 'read returns NotFoundError' do + expect { nonexistent_child.read }.to raise_error(Chef::ChefFS::FileSystem::NotFoundError) + end + end + +end diff --git a/spec/unit/chef_fs/file_system_spec.rb b/spec/unit/chef_fs/file_system_spec.rb new file mode 100644 index 0000000000..40d85e8a53 --- /dev/null +++ b/spec/unit/chef_fs/file_system_spec.rb @@ -0,0 +1,136 @@ +# +# Author:: John Keiser (<jkeiser@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 'support/shared/unit/file_system_support' +require 'chef/chef_fs/file_system' +require 'chef/chef_fs/file_pattern' + +describe Chef::ChefFS::FileSystem do + include FileSystemSupport + + context 'with empty filesystem' do + let(:fs) { memory_fs('', {}) } + + context 'list' do + it '/' do + list_should_yield_paths(fs, '/', '/') + end + it '/a' do + list_should_yield_paths(fs, '/a', '/a') + end + it '/a/b' do + list_should_yield_paths(fs, '/a/b') + end + it '/*' do + list_should_yield_paths(fs, '/*', '/') + end + end + + context 'resolve_path' do + it '/' do + Chef::ChefFS::FileSystem.resolve_path(fs, '/').path.should == '/' + end + it 'nonexistent /a' do + Chef::ChefFS::FileSystem.resolve_path(fs, '/a').path.should == '/a' + end + it 'nonexistent /a/b' do + Chef::ChefFS::FileSystem.resolve_path(fs, '/a/b').path.should == '/a/b' + end + end + end + + context 'with a populated filesystem' do + let(:fs) { + memory_fs('', { + :a => { + :aa => { + :c => '', + :zz => '' + }, + :ab => { + :c => '', + } + }, + :x => '' + }) + } + context 'list' do + it '/**' do + list_should_yield_paths(fs, '/**', '/', '/a', '/x', '/a/aa', '/a/aa/c', '/a/aa/zz', '/a/ab', '/a/ab/c') + end + it '/' do + list_should_yield_paths(fs, '/', '/') + end + it '/*' do + list_should_yield_paths(fs, '/*', '/', '/a', '/x') + end + it '/*/*' do + list_should_yield_paths(fs, '/*/*', '/a/aa', '/a/ab') + end + it '/*/*/*' do + list_should_yield_paths(fs, '/*/*/*', '/a/aa/c', '/a/aa/zz', '/a/ab/c') + end + it '/*/*/?' do + list_should_yield_paths(fs, '/*/*/?', '/a/aa/c', '/a/ab/c') + end + it '/a/*/c' do + list_should_yield_paths(fs, '/a/*/c', '/a/aa/c', '/a/ab/c') + end + it '/**b/c' do + list_should_yield_paths(fs, '/**b/c', '/a/ab/c') + end + it '/a/ab/c' do + no_blocking_calls_allowed + list_should_yield_paths(fs, '/a/ab/c', '/a/ab/c') + end + it 'nonexistent /a/ab/blah' do + no_blocking_calls_allowed + list_should_yield_paths(fs, '/a/ab/blah', '/a/ab/blah') + end + it 'nonexistent /a/ab/blah/bjork' do + no_blocking_calls_allowed + list_should_yield_paths(fs, '/a/ab/blah/bjork') + end + end + + context 'resolve_path' do + before(:each) do + no_blocking_calls_allowed + end + it 'resolves /' do + Chef::ChefFS::FileSystem.resolve_path(fs, '/').path.should == '/' + end + it 'resolves /x' do + Chef::ChefFS::FileSystem.resolve_path(fs, '/x').path.should == '/x' + end + it 'resolves /a' do + Chef::ChefFS::FileSystem.resolve_path(fs, '/a').path.should == '/a' + end + it 'resolves /a/aa' do + Chef::ChefFS::FileSystem.resolve_path(fs, '/a/aa').path.should == '/a/aa' + end + it 'resolves /a/aa/zz' do + Chef::ChefFS::FileSystem.resolve_path(fs, '/a/aa/zz').path.should == '/a/aa/zz' + end + it 'resolves nonexistent /y/x/w' do + Chef::ChefFS::FileSystem.resolve_path(fs, '/y/x/w').path.should == '/y/x/w' + end + end + end +end diff --git a/spec/unit/chef_spec.rb b/spec/unit/chef_spec.rb new file mode 100644 index 0000000000..cf60e64629 --- /dev/null +++ b/spec/unit/chef_spec.rb @@ -0,0 +1,25 @@ +# +# 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 do + it "should have a version defined" do + Chef::VERSION.should match(/(\d+)\.(\d+)\.(\d+)/) + end +end diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb new file mode 100644 index 0000000000..9d0c88dad1 --- /dev/null +++ b/spec/unit/client_spec.rb @@ -0,0 +1,290 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Tim Hinderliter (<tim@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Copyright:: Copyright 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' + +require 'chef/run_context' +require 'chef/rest' +require 'rbconfig' + +shared_examples_for Chef::Client do + before do + Chef::Log.logger = Logger.new(StringIO.new) + + # Node/Ohai data + @hostname = "hostname" + @fqdn = "hostname.example.org" + Chef::Config[:node_name] = @fqdn + ohai_data = { :fqdn => @fqdn, + :hostname => @hostname, + :platform => 'example-platform', + :platform_version => 'example-platform-1.0', + :data => {} } + ohai_data.stub!(:all_plugins).and_return(true) + ohai_data.stub!(:data).and_return(ohai_data) + Ohai::System.stub!(:new).and_return(ohai_data) + + @node = Chef::Node.new + @node.name(@fqdn) + @node.chef_environment("_default") + + @client = Chef::Client.new + @client.node = @node + end + + describe "authentication protocol selection" do + after do + Chef::Config[:authentication_protocol_version] = "1.0" + end + + context "when the node name is <= 90 bytes" do + it "does not force the authentication protocol to 1.1" do + Chef::Config[:node_name] = ("f" * 90) + # ugly that this happens as a side effect of a getter :( + @client.node_name + Chef::Config[:authentication_protocol_version].should == "1.0" + end + end + + context "when the node name is > 90 bytes" do + it "sets the authentication protocol to version 1.1" do + Chef::Config[:node_name] = ("f" * 91) + # ugly that this happens as a side effect of a getter :( + @client.node_name + Chef::Config[:authentication_protocol_version].should == "1.1" + end + end + end + + describe "run" do + + it "should identify the node and run ohai, then register the client" do + mock_chef_rest_for_node = mock("Chef::REST (node)") + mock_chef_rest_for_client = mock("Chef::REST (client)") + mock_chef_rest_for_node_save = mock("Chef::REST (node save)") + mock_chef_runner = mock("Chef::Runner") + + # --Client.register + # Make sure Client#register thinks the client key doesn't + # exist, so it tries to register and create one. + File.should_receive(:exists?).with(Chef::Config[:client_key]).exactly(1).times.and_return(false) + + # Client.register will register with the validation client name. + Chef::REST.should_receive(:new).with(Chef::Config[:client_url], Chef::Config[:validation_client_name], Chef::Config[:validation_key]).exactly(1).and_return(mock_chef_rest_for_client) + mock_chef_rest_for_client.should_receive(:register).with(@fqdn, Chef::Config[:client_key]).exactly(1).and_return(true) + # Client.register will then turn around create another + + # Chef::REST object, this time with the client key it got from the + # previous step. + Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url], @fqdn, Chef::Config[:client_key]).exactly(1).and_return(mock_chef_rest_for_node) + + # --Client#build_node + # looks up the node, which we will return, then later saves it. + Chef::Node.should_receive(:find_or_create).with(@fqdn).and_return(@node) + + # --ResourceReporter#node_load_completed + # gets a run id from the server for storing resource history + # (has its own tests, so stubbing it here.) + Chef::ResourceReporter.any_instance.should_receive(:node_load_completed) + + # --ResourceReporter#run_completed + # updates the server with the resource history + # (has its own tests, so stubbing it here.) + Chef::ResourceReporter.any_instance.should_receive(:run_completed) + # --Client#setup_run_context + # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync + # + Chef::CookbookSynchronizer.any_instance.should_receive(:sync_cookbooks) + mock_chef_rest_for_node.should_receive(:post_rest).with("environments/_default/cookbook_versions", {:run_list => []}).and_return({}) + + # --Client#converge + Chef::Runner.should_receive(:new).and_return(mock_chef_runner) + mock_chef_runner.should_receive(:converge).and_return(true) + + # --Client#save_updated_node + Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(mock_chef_rest_for_node_save) + mock_chef_rest_for_node_save.should_receive(:put_rest).with("nodes/#{@fqdn}", @node).and_return(true) + + Chef::RunLock.any_instance.should_receive(:acquire) + Chef::RunLock.any_instance.should_receive(:release) + + # Post conditions: check that node has been filled in correctly + @client.should_receive(:run_started) + @client.should_receive(:run_completed_successfully) + + if(Chef::Config[:client_fork]) + require 'stringio' + if(Chef::Config[:pipe_node]) + pipe_sim = StringIO.new + pipe_sim.should_receive(:close).exactly(4).and_return(nil) + res = '' + pipe_sim.should_receive(:puts) do |string| + res.replace(string) + end + pipe_sim.should_receive(:gets).and_return(res) + IO.should_receive(:pipe).and_return([pipe_sim, pipe_sim]) + IO.should_receive(:select).and_return(true) + end + proc_ret = Class.new.new + proc_ret.should_receive(:success?).and_return(true) + Process.should_receive(:waitpid2).and_return([1, proc_ret]) + @client.should_receive(:exit).and_return(nil) + @client.should_receive(:fork) do |&block| + block.call + end + end + + # This is what we're testing. + @client.run + + if(!Chef::Config[:client_fork] || Chef::Config[:pipe_node]) + @node.automatic_attrs[:platform].should == "example-platform" + @node.automatic_attrs[:platform_version].should == "example-platform-1.0" + end + end + + describe "when notifying other objects of the status of the chef run" do + before do + Chef::Client.clear_notifications + Chef::Node.stub!(:find_or_create).and_return(@node) + @node.stub!(:save) + @client.build_node + end + + it "notifies observers that the run has started" do + notified = false + Chef::Client.when_run_starts do |run_status| + run_status.node.should == @node + notified = true + end + + @client.run_started + notified.should be_true + end + + it "notifies observers that the run has completed successfully" do + notified = false + Chef::Client.when_run_completes_successfully do |run_status| + run_status.node.should == @node + notified = true + end + + @client.run_completed_successfully + notified.should be_true + end + + it "notifies observers that the run failed" do + notified = false + Chef::Client.when_run_fails do |run_status| + run_status.node.should == @node + notified = true + end + + @client.run_failed + notified.should be_true + end + end + end + + describe "build_node" do + it "should expand the roles and recipes for the node" do + @node.run_list << "role[role_containing_cookbook1]" + role_containing_cookbook1 = Chef::Role.new + role_containing_cookbook1.name("role_containing_cookbook1") + role_containing_cookbook1.run_list << "cookbook1" + + # build_node will call Node#expand! with server, which will + # eventually hit the server to expand the included role. + mock_chef_rest = mock("Chef::REST") + mock_chef_rest.should_receive(:get_rest).with("roles/role_containing_cookbook1").and_return(role_containing_cookbook1) + Chef::REST.should_receive(:new).and_return(mock_chef_rest) + + # check pre-conditions. + @node[:roles].should be_nil + @node[:recipes].should be_nil + + @client.build_node + + # check post-conditions. + @node[:roles].should_not be_nil + @node[:roles].length.should == 1 + @node[:roles].should include("role_containing_cookbook1") + @node[:recipes].should_not be_nil + @node[:recipes].length.should == 1 + @node[:recipes].should include("cookbook1") + end + end + + describe "when a run list override is provided" do + before do + @node = Chef::Node.new + @node.name(@fqdn) + @node.chef_environment("_default") + @node.automatic_attrs[:platform] = "example-platform" + @node.automatic_attrs[:platform_version] = "example-platform-1.0" + end + + it "should permit spaces in overriding run list" do + @client = Chef::Client.new(nil, :override_runlist => 'role[a], role[b]') + end + + it "should override the run list and save original runlist" do + @client = Chef::Client.new(nil, :override_runlist => 'role[test_role]') + @client.node = @node + + @node.run_list << "role[role_containing_cookbook1]" + + override_role = Chef::Role.new + override_role.name 'test_role' + override_role.run_list << 'cookbook1' + + original_runlist = @node.run_list.dup + + mock_chef_rest = mock("Chef::REST") + mock_chef_rest.should_receive(:get_rest).with("roles/test_role").and_return(override_role) + Chef::REST.should_receive(:new).and_return(mock_chef_rest) + + @node.should_receive(:save).and_return(nil) + + @client.build_node + + @node[:roles].should_not be_nil + @node[:roles].should eql(['test_role']) + @node[:recipes].should eql(['cookbook1']) + + @client.save_updated_node + + @node.run_list.should == original_runlist + + end + end + +end + +describe Chef::Client do + it_behaves_like Chef::Client +end + +describe "Chef::Client Forked" do + it_behaves_like Chef::Client + before do + Chef::Config[:client_fork] = true + end +end diff --git a/spec/unit/config_spec.rb b/spec/unit/config_spec.rb new file mode 100644 index 0000000000..89161c9df1 --- /dev/null +++ b/spec/unit/config_spec.rb @@ -0,0 +1,215 @@ +# +# 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::Config do + before(:all) do + @original_config = Chef::Config.hash_dup + @original_env = { 'HOME' => ENV['HOME'], 'SYSTEMDRIVE' => ENV['SYSTEMDRIVE'], 'HOMEPATH' => ENV['HOMEPATH'], 'USERPROFILE' => ENV['USERPROFILE'] } + end + + shared_examples_for "server URL" do + it "should set the registration url" do + Chef::Config.registration_url.should == "https://junglist.gen.nz" + end + + it "should set the template url" do + Chef::Config.template_url.should == "https://junglist.gen.nz" + end + + it "should set the remotefile url" do + Chef::Config.remotefile_url.should == "https://junglist.gen.nz" + end + + it "should set the search url" do + Chef::Config.search_url.should == "https://junglist.gen.nz" + end + + it "should set the role url" do + Chef::Config.role_url.should == "https://junglist.gen.nz" + end + end + + describe "config attribute writer: chef_server_url" do + before do + Chef::Config.chef_server_url = "https://junglist.gen.nz" + end + + it_behaves_like "server URL" + end + + context "when the url has a leading space" do + before do + Chef::Config.chef_server_url = " https://junglist.gen.nz" + end + + it_behaves_like "server URL" + end + + describe "class method: manage_secret_key" do + before do + Chef::FileCache.stub!(:load).and_return(true) + Chef::FileCache.stub!(:has_key?).with("chef_server_cookie_id").and_return(false) + end + + it "should generate and store a chef server cookie id" do + Chef::FileCache.should_receive(:store).with("chef_server_cookie_id", /\w{40}/).and_return(true) + Chef::Config.manage_secret_key + end + + describe "when the filecache has a chef server cookie id key" do + before do + Chef::FileCache.stub!(:has_key?).with("chef_server_cookie_id").and_return(true) + end + + it "should not generate and store a chef server cookie id" do + Chef::FileCache.should_not_receive(:store).with("chef_server_cookie_id", /\w{40}/).and_return(true) + Chef::Config.manage_secret_key + end + end + + end + + describe "config attribute writer: log_method=" do + describe "when given an object that responds to sync= e.g. IO" do + it "should configure itself to use the IO as log_location" do + Chef::Config.log_location = STDOUT + Chef::Config.log_location.should == STDOUT + end + end + + describe "when given an object that is stringable (to_str)" do + before do + @mockfile = mock("File", :path => "/var/log/chef/client.log", :sync= => true) + File.should_receive(:new). + with("/var/log/chef/client.log", "a"). + and_return(@mockfile) + end + + after do + Chef::Config.log_location = STDOUT + end + + it "should configure itself to use a File object based upon the String" do + Chef::Config.log_location = "/var/log/chef/client.log" + Chef::Config.log_location.path.should == "/var/log/chef/client.log" + end + end + end + + describe "class method: openid_providers=" do + it "should not log an appropriate deprecation info message" do + Chef::Log.should_not_receive(:info).with("DEPRECATION: openid_providers will be removed, please use authorized_openid_providers").and_return(true) + Chef::Config.openid_providers = %w{opscode.com junglist.gen.nz} + end + + it "should internally configure authorized_openid_providers with the value given" do + Chef::Config.should_receive(:configure).and_return(%w{opscode.com junglist.gen.nz}) + Chef::Config.openid_providers = %w{opscode.com junglist.gen.nz} + end + end + + describe "class method: plaform_specific_path" do + it "should return given path on non-windows systems" do + platform_mock :unix do + path = "/etc/chef/cookbooks" + Chef::Config.platform_specific_path(path).should == "/etc/chef/cookbooks" + end + end + + it "should return a windows path on windows systems" do + platform_mock :windows do + path = "/etc/chef/cookbooks" + ENV.stub!(:[]).with('SYSTEMDRIVE').and_return('C:') + # match on a regex that looks for the base path with an optional + # system drive at the beginning (c:) + # system drive is not hardcoded b/c it can change and b/c it is not present on linux systems + Chef::Config.platform_specific_path(path).should == "C:\\chef\\cookbooks" + end + end + end + + describe "default values" do + before(:each) do + # reload Chef::Config to ensure defaults are truely active + load File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "lib", "chef", "config.rb")) + end + + after(:each) do + # reload spec helper to re-set any spec specific Chef::Config values + load File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper.rb")) + end + + it "Chef::Config[:file_backup_path] defaults to /var/chef/backup" do + backup_path = if windows? + "#{ENV['SYSTEMDRIVE']}\\chef\\backup" + else + "/var/chef/backup" + end + Chef::Config[:file_backup_path].should == backup_path + end + + it "Chef::Config[:ssl_verify_mode] defaults to :verify_none" do + Chef::Config[:ssl_verify_mode].should == :verify_none + end + + it "Chef::Config[:ssl_ca_path] defaults to nil" do + Chef::Config[:ssl_ca_path].should be_nil + end + + it "Chef::Config[:ssl_ca_file] defaults to nil" do + Chef::Config[:ssl_ca_file].should be_nil + end + + it "Chef::Config[:data_bag_path] defaults to /var/chef/data_bags" do + data_bag_path = if windows? + "C:\\chef\\data_bags" + else + "/var/chef/data_bags" + end + + Chef::Config[:data_bag_path].should == data_bag_path + end + end + + describe "Chef::Config[:user_home]" do + it "should set when HOME is provided" do + ENV['HOME'] = "/home/kitten" + load File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "lib", "chef", "config.rb")) + Chef::Config[:user_home].should == "/home/kitten" + end + + it "should be set when only USERPROFILE is provided" do + ENV['HOME'], ENV['SYSTEMDRIVE'], ENV['HOMEPATH'] = nil, nil, nil + ENV['USERPROFILE'] = "/users/kitten" + load File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "lib", "chef", "config.rb")) + Chef::Config[:user_home].should == "/users/kitten" + end + + after(:each) do + @original_env.each do |env_setting| + ENV[env_setting[0]] = env_setting[1] + end + end + end + + after(:each) do + Chef::Config.configuration = @original_config + end +end diff --git a/spec/unit/cookbook/chefignore_spec.rb b/spec/unit/cookbook/chefignore_spec.rb new file mode 100644 index 0000000000..30b97e865d --- /dev/null +++ b/spec/unit/cookbook/chefignore_spec.rb @@ -0,0 +1,38 @@ +#-- +# Author:: Daniel DeLeo (<dan@opscode.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::Cookbook::Chefignore do + before do + @chefignore = Chef::Cookbook::Chefignore.new(File.join(CHEF_SPEC_DATA, 'cookbooks')) + end + + it "loads the globs in the chefignore file" do + @chefignore.ignores.should =~ %w[recipes/ignoreme.rb] + end + + it "removes items from an array that match the ignores" do + file_list = %w[ recipes/ignoreme.rb recipes/dontignoreme.rb ] + @chefignore.remove_ignores_from(file_list).should == %w[recipes/dontignoreme.rb] + end + + it "determines if a file is ignored" do + @chefignore.ignored?('recipes/ignoreme.rb').should be_true + @chefignore.ignored?('recipes/dontignoreme.rb').should be_false + end +end diff --git a/spec/unit/cookbook/metadata_spec.rb b/spec/unit/cookbook/metadata_spec.rb new file mode 100644 index 0000000000..2757f92506 --- /dev/null +++ b/spec/unit/cookbook/metadata_spec.rb @@ -0,0 +1,627 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright 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' +require 'chef/cookbook/metadata' + +describe Chef::Cookbook::Metadata do + before(:each) do + @cookbook = Chef::CookbookVersion.new('test_cookbook') + @meta = Chef::Cookbook::Metadata.new(@cookbook) + end + + describe "when comparing for equality" do + before do + @fields = [ :name, :description, :long_description, :maintainer, + :maintainer_email, :license, :platforms, :dependencies, + :recommendations, :suggestions, :conflicting, :providing, + :replacing, :attributes, :groupings, :recipes, :version] + end + + it "does not depend on object identity for equality" do + @meta.should == @meta.dup + end + + it "is not equal to another object if it isn't have all of the metadata fields" do + @fields.each_index do |field_to_remove| + fields_to_include = @fields.dup + fields_to_include.delete_at(field_to_remove) + almost_duck_type = Struct.new(*fields_to_include).new + @fields.each do |field| + setter = "#{field}=" + metadata_value = @meta.send(field) + almost_duck_type.send(setter, metadata_value) if almost_duck_type.respond_to?(setter) + @mets.should_not == almost_duck_type + end + end + end + + it "is equal to another object if it has equal values for all metadata fields" do + duck_type = Struct.new(*@fields).new + @fields.each do |field| + setter = "#{field}=" + metadata_value = @meta.send(field) + duck_type.send(setter, metadata_value) + end + @meta.should == duck_type + end + + it "is not equal if any values are different" do + duck_type_class = Struct.new(*@fields) + @fields.each do |field_to_change| + duck_type = duck_type_class.new + + @fields.each do |field| + setter = "#{field}=" + metadata_value = @meta.send(field) + duck_type.send(setter, metadata_value) + end + + field_to_change + + duck_type.send("#{field_to_change}=".to_sym, :epic_fail) + @meta.should_not == duck_type + end + end + + end + + describe "when first created" do + it "should return a Chef::Cookbook::Metadata object" do + @meta.should be_a_kind_of(Chef::Cookbook::Metadata) + end + + it "should allow a cookbook as the first argument" do + lambda { Chef::Cookbook::Metadata.new(@cookbook) }.should_not raise_error + end + + it "should allow an maintainer name for the second argument" do + lambda { Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown') }.should_not raise_error + end + + it "should set the maintainer name from the second argument" do + md = Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown') + md.maintainer.should == 'Bobo T. Clown' + end + + it "should allow an maintainer email for the third argument" do + lambda { Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown', 'bobo@clown.co') }.should_not raise_error + end + + it "should set the maintainer email from the third argument" do + md = Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown', 'bobo@clown.co') + md.maintainer_email.should == 'bobo@clown.co' + end + + it "should allow a license for the fourth argument" do + lambda { Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown', 'bobo@clown.co', 'Clown License v1') }.should_not raise_error + end + + it "should set the license from the fourth argument" do + md = Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown', 'bobo@clown.co', 'Clown License v1') + md.license.should == 'Clown License v1' + end + end + + describe "cookbook" do + it "should return the cookbook we were initialized with" do + @meta.cookbook.should eql(@cookbook) + end + end + + describe "name" do + it "should return the name of the cookbook" do + @meta.name.should eql(@cookbook.name) + end + end + + describe "platforms" do + it "should return the current platform hash" do + @meta.platforms.should be_a_kind_of(Hash) + end + end + + describe "adding a supported platform" do + it "should support adding a supported platform with a single expression" do + @meta.supports("ubuntu", ">= 8.04") + @meta.platforms["ubuntu"].should == '>= 8.04' + end + end + + describe "meta-data attributes" do + params = { + :maintainer => "Adam Jacob", + :maintainer_email => "adam@opscode.com", + :license => "Apache v2.0", + :description => "Foobar!", + :long_description => "Much Longer\nSeriously", + :version => "0.6.0" + } + params.sort { |a,b| a.to_s <=> b.to_s }.each do |field, field_value| + describe field do + it "should be set-able via #{field}" do + @meta.send(field, field_value).should eql(field_value) + end + it "should be get-able via #{field}" do + @meta.send(field, field_value) + @meta.send(field).should eql(field_value) + end + end + end + + describe "version transformation" do + it "should transform an '0.6' version to '0.6.0'" do + @meta.send(:version, "0.6").should eql("0.6.0") + end + + it "should spit out '0.6.0' after transforming '0.6'" do + @meta.send(:version, "0.6") + @meta.send(:version).should eql("0.6.0") + end + end + end + + describe "describing dependencies" do + dep_types = { + :depends => [ :dependencies, "foo::bar", "> 0.2" ], + :recommends => [ :recommendations, "foo::bar", ">= 0.2" ], + :suggests => [ :suggestions, "foo::bar", "> 0.2" ], + :conflicts => [ :conflicting, "foo::bar", "~> 0.2" ], + :provides => [ :providing, "foo::bar", "<= 0.2" ], + :replaces => [ :replacing, "foo::bar", "= 0.2.1" ], + } + dep_types.sort { |a,b| a.to_s <=> b.to_s }.each do |dep, dep_args| + check_with = dep_args.shift + describe dep do + it "should be set-able via #{dep}" do + @meta.send(dep, *dep_args).should == dep_args[1] + end + it "should be get-able via #{check_with}" do + @meta.send(dep, *dep_args) + @meta.send(check_with).should == { dep_args[0] => dep_args[1] } + end + end + end + + + describe "in the obsoleted format" do + dep_types = { + :depends => [ "foo::bar", "> 0.2", "< 1.0" ], + :recommends => [ "foo::bar", ">= 0.2", "< 1.0" ], + :suggests => [ "foo::bar", "> 0.2", "< 1.0" ], + :conflicts => [ "foo::bar", "> 0.2", "< 1.0" ], + :provides => [ "foo::bar", "> 0.2", "< 1.0" ], + :replaces => [ "foo::bar", "> 0.2.1", "< 1.0" ], + } + + dep_types.each do |dep, dep_args| + it "for #{dep} raises an informative error instead of vomiting on your shoes" do + lambda {@meta.send(dep, *dep_args)}.should raise_error(Chef::Exceptions::ObsoleteDependencySyntax) + end + end + end + + + describe "with obsolete operators" do + dep_types = { + :depends => [ "foo::bar", ">> 0.2"], + :recommends => [ "foo::bar", ">> 0.2"], + :suggests => [ "foo::bar", ">> 0.2"], + :conflicts => [ "foo::bar", ">> 0.2"], + :provides => [ "foo::bar", ">> 0.2"], + :replaces => [ "foo::bar", ">> 0.2.1"], + } + + dep_types.each do |dep, dep_args| + it "for #{dep} raises an informative error instead of vomiting on your shoes" do + lambda {@meta.send(dep, *dep_args)}.should raise_error(Chef::Exceptions::InvalidVersionConstraint) + end + end + end + end + + describe "attribute groupings" do + it "should allow you set a grouping" do + group = { + "title" => "MySQL Tuning", + "description" => "Setting from the my.cnf file that allow you to tune your mysql server" + } + @meta.grouping("/db/mysql/databases/tuning", group).should == group + end + it "should not accept anything but a string for display_name" do + lambda { + @meta.grouping("db/mysql/databases", :title => "foo") + }.should_not raise_error(ArgumentError) + lambda { + @meta.grouping("db/mysql/databases", :title => Hash.new) + }.should raise_error(ArgumentError) + end + + it "should not accept anything but a string for the description" do + lambda { + @meta.grouping("db/mysql/databases", :description => "foo") + }.should_not raise_error(ArgumentError) + lambda { + @meta.grouping("db/mysql/databases", :description => Hash.new) + }.should raise_error(ArgumentError) + end + end + + describe "cookbook attributes" do + it "should allow you set an attributes metadata" do + attrs = { + "display_name" => "MySQL Databases", + "description" => "Description of MySQL", + "choice" => ['dedicated', 'shared'], + "calculated" => false, + "type" => 'string', + "required" => 'recommended', + "recipes" => [ "mysql::server", "mysql::master" ], + "default" => [ ] + } + @meta.attribute("/db/mysql/databases", attrs).should == attrs + end + + it "should not accept anything but a string for display_name" do + lambda { + @meta.attribute("db/mysql/databases", :display_name => "foo") + }.should_not raise_error(ArgumentError) + lambda { + @meta.attribute("db/mysql/databases", :display_name => Hash.new) + }.should raise_error(ArgumentError) + end + + it "should not accept anything but a string for the description" do + lambda { + @meta.attribute("db/mysql/databases", :description => "foo") + }.should_not raise_error(ArgumentError) + lambda { + @meta.attribute("db/mysql/databases", :description => Hash.new) + }.should raise_error(ArgumentError) + end + + it "should not accept anything but an array of strings for choice" do + lambda { + @meta.attribute("db/mysql/databases", :choice => ['dedicated', 'shared']) + }.should_not raise_error(ArgumentError) + lambda { + @meta.attribute("db/mysql/databases", :choice => [10, 'shared']) + }.should raise_error(ArgumentError) + lambda { + @meta.attribute("db/mysql/databases", :choice => Hash.new) + }.should raise_error(ArgumentError) + end + + it "should set choice to empty array by default" do + @meta.attribute("db/mysql/databases", {}) + @meta.attributes["db/mysql/databases"][:choice].should == [] + end + + it "should let calculated be true or false" do + lambda { + @meta.attribute("db/mysql/databases", :calculated => true) + }.should_not raise_error(ArgumentError) + lambda { + @meta.attribute("db/mysql/databases", :calculated => false) + }.should_not raise_error(ArgumentError) + lambda { + @meta.attribute("db/mysql/databases", :calculated => Hash.new) + }.should raise_error(ArgumentError) + end + + it "should set calculated to false by default" do + @meta.attribute("db/mysql/databases", {}) + @meta.attributes["db/mysql/databases"][:calculated].should == false + end + + it "accepts String for the attribute type" do + lambda { + @meta.attribute("db/mysql/databases", :type => "string") + }.should_not raise_error(ArgumentError) + end + + it "accepts Array for the attribute type" do + lambda { + @meta.attribute("db/mysql/databases", :type => "array") + }.should_not raise_error(ArgumentError) + lambda { + @meta.attribute("db/mysql/databases", :type => Array.new) + }.should raise_error(ArgumentError) + end + + it "accepts symbol for the attribute type" do + lambda { + @meta.attribute("db/mysql/databases", :type => "symbol") + }.should_not raise_error(ArgumentError) + end + + it "should let type be hash (backwards compatability only)" do + lambda { + @meta.attribute("db/mysql/databases", :type => "hash") + }.should_not raise_error(ArgumentError) + end + + it "should let required be required, recommended or optional" do + lambda { + @meta.attribute("db/mysql/databases", :required => 'required') + }.should_not raise_error(ArgumentError) + lambda { + @meta.attribute("db/mysql/databases", :required => 'recommended') + }.should_not raise_error(ArgumentError) + lambda { + @meta.attribute("db/mysql/databases", :required => 'optional') + }.should_not raise_error(ArgumentError) + end + + it "should convert required true to required" do + lambda { + @meta.attribute("db/mysql/databases", :required => true) + }.should_not raise_error(ArgumentError) + #attrib = @meta.attributes["db/mysql/databases"][:required].should == "required" + end + + it "should convert required false to optional" do + lambda { + @meta.attribute("db/mysql/databases", :required => false) + }.should_not raise_error(ArgumentError) + #attrib = @meta.attributes["db/mysql/databases"][:required].should == "optional" + end + + it "should set required to 'optional' by default" do + @meta.attribute("db/mysql/databases", {}) + @meta.attributes["db/mysql/databases"][:required].should == 'optional' + end + + it "should make sure recipes is an array" do + lambda { + @meta.attribute("db/mysql/databases", :recipes => []) + }.should_not raise_error(ArgumentError) + lambda { + @meta.attribute("db/mysql/databases", :required => Hash.new) + }.should raise_error(ArgumentError) + end + + it "should set recipes to an empty array by default" do + @meta.attribute("db/mysql/databases", {}) + @meta.attributes["db/mysql/databases"][:recipes].should == [] + end + + it "should allow the default value to be a string, array, or hash" do + lambda { + @meta.attribute("db/mysql/databases", :default => []) + }.should_not raise_error(ArgumentError) + lambda { + @meta.attribute("db/mysql/databases", :default => {}) + }.should_not raise_error(ArgumentError) + lambda { + @meta.attribute("db/mysql/databases", :default => "alice in chains") + }.should_not raise_error(ArgumentError) + lambda { + @meta.attribute("db/mysql/databases", :required => :not_gonna_do_it) + }.should raise_error(ArgumentError) + end + + it "should error if default used with calculated" do + lambda { + attrs = { + :calculated => true, + :default => [ "I thought you said calculated" ] + } + @meta.attribute("db/mysql/databases", attrs) + }.should raise_error(ArgumentError) + lambda { + attrs = { + :calculated => true, + :default => "I thought you said calculated" + } + @meta.attribute("db/mysql/databases", attrs) + }.should raise_error(ArgumentError) + end + + it "should allow a default that is a choice" do + lambda { + attrs = { + :choice => [ "a", "b", "c"], + :default => "b" + } + @meta.attribute("db/mysql/databases", attrs) + }.should_not raise_error(ArgumentError) + lambda { + attrs = { + :choice => [ "a", "b", "c", "d", "e"], + :default => ["b", "d"] + } + @meta.attribute("db/mysql/databases", attrs) + }.should_not raise_error(ArgumentError) + end + + it "should error if default is not a choice" do + lambda { + attrs = { + :choice => [ "a", "b", "c"], + :default => "d" + } + @meta.attribute("db/mysql/databases", attrs) + }.should raise_error(ArgumentError) + lambda { + attrs = { + :choice => [ "a", "b", "c", "d", "e"], + :default => ["b", "z"] + } + @meta.attribute("db/mysql/databases", attrs) + }.should raise_error(ArgumentError) + end + end + + describe "recipes" do + before(:each) do + @cookbook.recipe_files = [ "default.rb", "enlighten.rb" ] + @meta = Chef::Cookbook::Metadata.new(@cookbook) + end + + it "should have the names of the recipes" do + @meta.recipes["test_cookbook"].should == "" + @meta.recipes["test_cookbook::enlighten"].should == "" + end + + it "should let you set the description for a recipe" do + @meta.recipe "test_cookbook", "It, um... tests stuff?" + @meta.recipes["test_cookbook"].should == "It, um... tests stuff?" + end + + it "should automatically provide each recipe" do + @meta.providing.has_key?("test_cookbook").should == true + @meta.providing.has_key?("test_cookbook::enlighten").should == true + end + + end + + describe "json" do + before(:each) do + @cookbook.recipe_files = [ "default.rb", "enlighten.rb" ] + @meta = Chef::Cookbook::Metadata.new(@cookbook) + @meta.version "1.0" + @meta.maintainer "Bobo T. Clown" + @meta.maintainer_email "bobo@example.com" + @meta.long_description "I have a long arm!" + @meta.supports :ubuntu, "> 8.04" + @meta.depends "bobo", "= 1.0" + @meta.depends "bobotclown", "= 1.1" + @meta.recommends "snark", "< 3.0" + @meta.suggests "kindness", "> 2.0" + @meta.conflicts "hatred" + @meta.provides "foo(:bar, :baz)" + @meta.replaces "snarkitron" + @meta.recipe "test_cookbook::enlighten", "is your buddy" + @meta.attribute "bizspark/has_login", + :display_name => "You have nothing" + @meta.version "1.2.3" + end + + describe "serialize" do + before(:each) do + @serial = Chef::JSONCompat.from_json(@meta.to_json) + end + + it "should serialize to a json hash" do + Chef::JSONCompat.from_json(@meta.to_json).should be_a_kind_of(Hash) + end + + %w{ + name + description + long_description + maintainer + maintainer_email + license + platforms + dependencies + suggestions + recommendations + conflicting + providing + replacing + attributes + recipes + version + }.each do |t| + it "should include '#{t}'" do + @serial[t].should == @meta.send(t.to_sym) + end + end + end + + describe "deserialize" do + before(:each) do + @deserial = Chef::Cookbook::Metadata.from_json(@meta.to_json) + end + + it "should deserialize to a Chef::Cookbook::Metadata object" do + @deserial.should be_a_kind_of(Chef::Cookbook::Metadata) + end + + %w{ + name + description + long_description + maintainer + maintainer_email + license + platforms + dependencies + suggestions + recommendations + conflicting + providing + replacing + attributes + recipes + version + }.each do |t| + it "should match '#{t}'" do + @deserial.send(t.to_sym).should == @meta.send(t.to_sym) + end + end + end + + describe "from_hash" do + before(:each) do + @hash = @meta.to_hash + end + + [:dependencies, + :recommendations, + :suggestions, + :conflicting, + :replacing].each do |to_check| + it "should transform deprecated greater than syntax for :#{to_check.to_s}" do + @hash[to_check.to_s]["foo::bar"] = ">> 0.2" + deserial = Chef::Cookbook::Metadata.from_hash(@hash) + deserial.send(to_check)["foo::bar"].should == '> 0.2' + end + + it "should transform deprecated less than syntax for :#{to_check.to_s}" do + @hash[to_check.to_s]["foo::bar"] = "<< 0.2" + deserial = Chef::Cookbook::Metadata.from_hash(@hash) + deserial.send(to_check)["foo::bar"].should == '< 0.2' + end + + it "should ignore multiple dependency constraints for :#{to_check.to_s}" do + @hash[to_check.to_s]["foo::bar"] = [ ">= 1.0", "<= 5.2" ] + deserial = Chef::Cookbook::Metadata.from_hash(@hash) + deserial.send(to_check)["foo::bar"].should == [] + end + + it "should accept an empty array of dependency constraints for :#{to_check.to_s}" do + @hash[to_check.to_s]["foo::bar"] = [] + deserial = Chef::Cookbook::Metadata.from_hash(@hash) + deserial.send(to_check)["foo::bar"].should == [] + end + + it "should accept single-element arrays of dependency constraints for :#{to_check.to_s}" do + @hash[to_check.to_s]["foo::bar"] = [ ">= 2.0" ] + deserial = Chef::Cookbook::Metadata.from_hash(@hash) + deserial.send(to_check)["foo::bar"].should == ">= 2.0" + end + end + end + + end + +end diff --git a/spec/unit/cookbook/synchronizer_spec.rb b/spec/unit/cookbook/synchronizer_spec.rb new file mode 100644 index 0000000000..e84fd3cfc5 --- /dev/null +++ b/spec/unit/cookbook/synchronizer_spec.rb @@ -0,0 +1,258 @@ +require 'spec_helper' +require 'chef/cookbook/synchronizer' +require 'chef/cookbook_version' + +describe Chef::CookbookCacheCleaner do + describe "when cleaning up unused cookbook components" do + + before do + @cleaner = Chef::CookbookCacheCleaner.instance + @cleaner.reset! + end + + it "removes all files that belong to unused cookbooks" do + end + + it "removes all files not validated during the chef run" do + file_cache = mock("Chef::FileCache with files from unused cookbooks") + unused_template_files = %w{cookbooks/unused/templates/default/foo.conf.erb cookbooks/unused/tempaltes/default/bar.conf.erb} + valid_cached_cb_files = %w{cookbooks/valid1/recipes/default.rb cookbooks/valid2/recipes/default.rb} + @cleaner.mark_file_as_valid('cookbooks/valid1/recipes/default.rb') + @cleaner.mark_file_as_valid('cookbooks/valid2/recipes/default.rb') + file_cache.should_receive(:find).with(File.join(%w{cookbooks ** *})).and_return(valid_cached_cb_files + unused_template_files) + file_cache.should_receive(:delete).with('cookbooks/unused/templates/default/foo.conf.erb') + file_cache.should_receive(:delete).with('cookbooks/unused/tempaltes/default/bar.conf.erb') + cookbook_hash = {"valid1"=> {}, "valid2" => {}} + @cleaner.stub!(:cache).and_return(file_cache) + @cleaner.cleanup_file_cache + end + + describe "on chef-solo" do + before do + Chef::Config[:solo] = true + end + + after do + Chef::Config[:solo] = false + end + + it "does not remove anything" do + @cleaner.cache.stub!(:find).and_return(%w{cookbooks/valid1/recipes/default.rb cookbooks/valid2/recipes/default.rb}) + @cleaner.cache.should_not_receive(:delete) + @cleaner.cleanup_file_cache + end + + end + + end +end + +describe Chef::CookbookSynchronizer do + before do + segments = [ :resources, :providers, :recipes, :definitions, :libraries, :attributes, :files, :templates, :root_files ] + @cookbook_manifest = {} + @cookbook_a = Chef::CookbookVersion.new("cookbook_a") + @cookbook_a_manifest = segments.inject({}) {|h, segment| h[segment.to_s] = []; h} + @cookbook_a_default_recipe = { "path" => "recipes/default.rb", + "url" => "http://chef.example.com/abc123", + "checksum" => "abc123" } + @cookbook_a_manifest["recipes"] = [ @cookbook_a_default_recipe ] + + @cookbook_a_default_attrs = { "path" => "attributes/default.rb", + "url" => "http://chef.example.com/abc456", + "checksum" => "abc456" } + @cookbook_a_manifest["attributes"] = [ @cookbook_a_default_attrs ] + @cookbook_a_manifest["templates"] = [{"path" => "templates/default/apache2.conf.erb", "url" => "http://chef.example.com/ffffff"}] + @cookbook_a.manifest = @cookbook_a_manifest + @cookbook_manifest["cookbook_a"] = @cookbook_a + + @events = Chef::EventDispatch::Dispatcher.new + @synchronizer = Chef::CookbookSynchronizer.new(@cookbook_manifest, @events) + end + + it "lists the cookbook names" do + @synchronizer.cookbook_names.should == %w[cookbook_a] + end + + it "lists the cookbook manifests" do + @synchronizer.cookbooks.should == [@cookbook_a] + end + + context "when the cache contains unneeded cookbooks" do + before do + @file_cache = mock("Chef::FileCache with files from unused cookbooks") + @valid_cached_cb_files = %w{cookbooks/valid1/recipes/default.rb cookbooks/valid2/recipes/default.rb} + @obsolete_cb_files = %w{cookbooks/old1/recipes/default.rb cookbooks/old2/recipes/default.rb} + + @cookbook_hash = {"valid1"=> {}, "valid2" => {}} + + @synchronizer = Chef::CookbookSynchronizer.new(@cookbook_hash, @events) + end + + it "removes unneeded cookbooks" do + @file_cache.should_receive(:find).with(File.join(%w{cookbooks ** *})).and_return(@valid_cached_cb_files + @obsolete_cb_files) + @file_cache.should_receive(:delete).with('cookbooks/old1/recipes/default.rb') + @file_cache.should_receive(:delete).with('cookbooks/old2/recipes/default.rb') + @synchronizer.stub!(:cache).and_return(@file_cache) + @synchronizer.clear_obsoleted_cookbooks + end + end + + describe "when syncing cookbooks with the server" do + before do + # Would rather not stub out methods on the test subject, but setting up + # the state is a PITA and tests for this behavior are above. + @synchronizer.should_receive(:clear_obsoleted_cookbooks) + + @server_api = mock("Chef::REST (mock)") + @file_cache = mock("Chef::FileCache (mock)") + @synchronizer.stub!(:server_api).and_return(@server_api) + @synchronizer.stub!(:cache).and_return(@file_cache) + + + @cookbook_a_default_recipe_tempfile = mock("Tempfile for cookbook_a default.rb recipe", + :path => "/tmp/cookbook_a_recipes_default_rb") + + @cookbook_a_default_attribute_tempfile = mock("Tempfile for cookbook_a default.rb attr file", + :path => "/tmp/cookbook_a_attributes_default_rb") + + end + + context "when the cache does not contain the desired files" do + before do + + # Files are not in the cache: + @file_cache.should_receive(:has_key?). + with("cookbooks/cookbook_a/recipes/default.rb"). + and_return(false) + @file_cache.should_receive(:has_key?). + with("cookbooks/cookbook_a/attributes/default.rb"). + and_return(false) + + # Fetch and copy default.rb recipe + @server_api.should_receive(:get_rest). + with('http://chef.example.com/abc123', true). + and_return(@cookbook_a_default_recipe_tempfile) + @file_cache.should_receive(:move_to). + with("/tmp/cookbook_a_recipes_default_rb", "cookbooks/cookbook_a/recipes/default.rb") + @file_cache.should_receive(:load). + with("cookbooks/cookbook_a/recipes/default.rb", false). + and_return("/file-cache/cookbooks/cookbook_a/recipes/default.rb") + + # Fetch and copy default.rb attribute file + @server_api.should_receive(:get_rest). + with('http://chef.example.com/abc456', true). + and_return(@cookbook_a_default_attribute_tempfile) + @file_cache.should_receive(:move_to). + with("/tmp/cookbook_a_attributes_default_rb", "cookbooks/cookbook_a/attributes/default.rb") + @file_cache.should_receive(:load). + with("cookbooks/cookbook_a/attributes/default.rb", false). + and_return("/file-cache/cookbooks/cookbook_a/attributes/default.rb") + end + + it "fetches eagerly loaded files" do + @synchronizer.sync_cookbooks + end + + it "does not fetch templates or cookbook files" do + # Implicitly tested in previous test; this test is just for behavior specification. + @server_api.should_not_receive(:get_rest). + with('http://chef.example.com/ffffff', true) + + @synchronizer.sync_cookbooks + end + + end + + context "when the cache contains outdated files" do + before do + # Files are in the cache: + @file_cache.should_receive(:has_key?). + with("cookbooks/cookbook_a/recipes/default.rb"). + and_return(true) + @file_cache.should_receive(:has_key?). + with("cookbooks/cookbook_a/attributes/default.rb"). + and_return(true) + + + # Fetch and copy default.rb recipe + @server_api.should_receive(:get_rest). + with('http://chef.example.com/abc123', true). + and_return(@cookbook_a_default_recipe_tempfile) + @file_cache.should_receive(:move_to). + with("/tmp/cookbook_a_recipes_default_rb", "cookbooks/cookbook_a/recipes/default.rb") + @file_cache.should_receive(:load). + with("cookbooks/cookbook_a/recipes/default.rb", false). + twice. + and_return("/file-cache/cookbooks/cookbook_a/recipes/default.rb") + + # Current file has fff000, want abc123 + Chef::CookbookVersion.should_receive(:checksum_cookbook_file). + with("/file-cache/cookbooks/cookbook_a/recipes/default.rb"). + and_return("fff000") + + # Fetch and copy default.rb attribute file + @server_api.should_receive(:get_rest). + with('http://chef.example.com/abc456', true). + and_return(@cookbook_a_default_attribute_tempfile) + @file_cache.should_receive(:move_to). + with("/tmp/cookbook_a_attributes_default_rb", "cookbooks/cookbook_a/attributes/default.rb") + @file_cache.should_receive(:load). + with("cookbooks/cookbook_a/attributes/default.rb", false). + twice. + and_return("/file-cache/cookbooks/cookbook_a/attributes/default.rb") + + # Current file has fff000, want abc456 + Chef::CookbookVersion.should_receive(:checksum_cookbook_file). + with("/file-cache/cookbooks/cookbook_a/attributes/default.rb"). + and_return("fff000") + end + + it "updates the outdated files" do + @synchronizer.sync_cookbooks + end + end + + context "when the cache is up to date" do + before do + # Files are in the cache: + @file_cache.should_receive(:has_key?). + with("cookbooks/cookbook_a/recipes/default.rb"). + and_return(true) + @file_cache.should_receive(:has_key?). + with("cookbooks/cookbook_a/attributes/default.rb"). + and_return(true) + + # Current file has abc123, want abc123 + Chef::CookbookVersion.should_receive(:checksum_cookbook_file). + with("/file-cache/cookbooks/cookbook_a/recipes/default.rb"). + and_return("abc123") + + # Current file has abc456, want abc456 + Chef::CookbookVersion.should_receive(:checksum_cookbook_file). + with("/file-cache/cookbooks/cookbook_a/attributes/default.rb"). + and_return("abc456") + + @file_cache.should_receive(:load). + with("cookbooks/cookbook_a/recipes/default.rb", false). + twice. + and_return("/file-cache/cookbooks/cookbook_a/recipes/default.rb") + + @file_cache.should_receive(:load). + with("cookbooks/cookbook_a/attributes/default.rb", false). + twice. + and_return("/file-cache/cookbooks/cookbook_a/attributes/default.rb") + end + + it "does not update files" do + @file_cache.should_not_receive(:move_to) + @server_api.should_not_receive(:get_rest) + @synchronizer.sync_cookbooks + end + + end + + end + +end + diff --git a/spec/unit/cookbook/syntax_check_spec.rb b/spec/unit/cookbook/syntax_check_spec.rb new file mode 100644 index 0000000000..b41c2ddf0a --- /dev/null +++ b/spec/unit/cookbook/syntax_check_spec.rb @@ -0,0 +1,211 @@ +# +# 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. +# + +################################################### +# OLD: +################################################### +# def test_ruby(cookbook_dir) +# cache = Chef::ChecksumCache.instance +# Dir[File.join(cookbook_dir, '**', '*.rb')].each do |ruby_file| +# key = cache.generate_key(ruby_file, "chef-test") +# fstat = File.stat(ruby_file) +# +# if cache.lookup_checksum(key, fstat) +# Chef::Log.info("No change in checksum of #{ruby_file}") +# else +# Chef::Log.info("Testing #{ruby_file} for syntax errors...") +# Chef::Mixin::Command.run_command(:command => "ruby -c #{ruby_file}", :output_on_failure => true) +# cache.generate_checksum(key, ruby_file, fstat) +# end +# end +# end +# +#def test_templates(cookbook_dir) +# cache = Chef::ChecksumCache.instance +# Dir[File.join(cookbook_dir, '**', '*.erb')].each do |erb_file| +# key = cache.generate_key(erb_file, "chef-test") +# fstat = File.stat(erb_file) +# +# if cache.lookup_checksum(key, fstat) +# Chef::Log.info("No change in checksum of #{erb_file}") +# else +# Chef::Log.info("Testing template #{erb_file} for syntax errors...") +# Chef::Mixin::Command.run_command(:command => "sh -c 'erubis -x #{erb_file} | ruby -c'", :output_on_failure => true) +# cache.generate_checksum(key, erb_file, fstat) +# end +# end +#end +# + +################################################### +# NEW: +################################################### +# def test_template_file(cookbook_dir, erb_file) +# Chef::Log.debug("Testing template #{erb_file} for syntax errors...") +# result = shell_out("sh -c 'erubis -x #{erb_file} | ruby -c'") +# result.error! +# rescue Mixlib::ShellOut::ShellCommandFailed +# file_relative_path = erb_file[/^#{Regexp.escape(cookbook_dir+File::Separator)}(.*)/, 1] +# Chef::Log.fatal("Erb template #{file_relative_path} has a syntax error:") +# result.stderr.each_line { |l| Chef::Log.fatal(l.chomp) } +# exit(1) +# end +# +# def test_ruby_file(cookbook_dir, ruby_file) +# Chef::Log.debug("Testing #{ruby_file} for syntax errors...") +# result = shell_out("ruby -c #{ruby_file}") +# result.error! +# rescue Mixlib::ShellOut::ShellCommandFailed +# file_relative_path = ruby_file[/^#{Regexp.escape(cookbook_dir+File::Separator)}(.*)/, 1] +# Chef::Log.fatal("Cookbook file #{file_relative_path} has a syntax error:") +# result.stderr.each_line { |l| Chef::Log.fatal(l.chomp) } +# exit(1) +# end +# + +require 'spec_helper' +require "chef/cookbook/syntax_check" + +describe Chef::Cookbook::SyntaxCheck do + before do + Chef::Log.logger = Logger.new(StringIO.new) + + @cookbook_path = File.join(CHEF_SPEC_DATA, 'cookbooks', 'openldap') + + @attr_files = %w{default.rb smokey.rb}.map { |f| File.join(@cookbook_path, 'attributes', f) } + @defn_files = %w{client.rb server.rb}.map { |f| File.join(@cookbook_path, 'definitions', f)} + @recipes = %w{default.rb gigantor.rb one.rb}.map { |f| File.join(@cookbook_path, 'recipes', f) } + @ruby_files = @attr_files + @defn_files + @recipes + + @template_files = %w{openldap_stuff.conf.erb openldap_variable_stuff.conf.erb test.erb}.map { |f| File.join(@cookbook_path, 'templates', 'default', f)} + + @syntax_check = Chef::Cookbook::SyntaxCheck.new(@cookbook_path) + end + + it "creates a syntax checker given the cookbook name when Chef::Config.cookbook_path is set" do + Chef::Config[:cookbook_path] = File.dirname(@cookbook_path) + syntax_check = Chef::Cookbook::SyntaxCheck.for_cookbook(:openldap) + syntax_check.cookbook_path.should == @cookbook_path + end + + describe "when first created" do + it "has the path to the cookbook to syntax check" do + @syntax_check.cookbook_path.should == @cookbook_path + end + + it "has access to the checksum cache" do + @syntax_check.cache.should equal(Chef::ChecksumCache.instance) + end + + it "lists the ruby files in the cookbook" do + @syntax_check.ruby_files.sort.should == @ruby_files.sort + end + + it "lists the erb templates in the cookbook" do + @syntax_check.template_files.sort.should == @template_files.sort + end + + end + + describe "when validating cookbooks" do + before do + Chef::Config[:cache_type] = 'Memory' + @checksum_cache_klass = Class.new(Chef::ChecksumCache) + @checksum_cache = @checksum_cache_klass.instance + @checksum_cache.reset!('Memory') + @syntax_check.stub!(:cache).and_return(@checksum_cache) + $stdout.stub!(:write) + end + + describe "and the files have not been syntax checked previously" do + it "shows that all ruby files require a syntax check" do + @syntax_check.untested_ruby_files.sort.should == @ruby_files.sort + end + + it "shows that all template files require a syntax check" do + @syntax_check.untested_template_files.sort.should == @template_files.sort + end + + it "removes a ruby file from the list of untested files after it is marked as validated" do + recipe = File.join(@cookbook_path, 'recipes', 'default.rb') + @syntax_check.validated(recipe) + @syntax_check.untested_ruby_files.should_not include(recipe) + end + + it "removes a template file from the list of untested files after it is marked as validated" do + template = File.join(@cookbook_path, 'templates', 'default', 'test.erb') + @syntax_check.validated(template) + @syntax_check.untested_template_files.should_not include(template) + end + + it "validates all ruby files" do + @syntax_check.validate_ruby_files.should be_true + @syntax_check.untested_ruby_files.should be_empty + end + + it "validates all templates" do + @syntax_check.validate_templates.should be_true + @syntax_check.untested_template_files.should be_empty + end + + describe "and a file has a syntax error" do + before do + @cookbook_path = File.join(CHEF_SPEC_DATA, 'cookbooks', 'borken') + @syntax_check.cookbook_path.replace(@cookbook_path) + end + + it "it indicates that a ruby file has a syntax error" do + @syntax_check.validate_ruby_files.should be_false + end + + it "does not remove the invalid file from the list of untested files" do + @syntax_check.untested_ruby_files.should include(File.join(@cookbook_path, 'recipes', 'default.rb')) + lambda { @syntax_check.validate_ruby_files }.should_not change(@syntax_check, :untested_ruby_files) + end + + it "indicates that a template file has a syntax error" do + @syntax_check.validate_templates.should be_false + end + + it "does not remove the invalid template from the list of untested templates" do + @syntax_check.untested_template_files.should include(File.join(@cookbook_path, 'templates', 'default', 'borken.erb')) + lambda {@syntax_check.validate_templates}.should_not change(@syntax_check, :untested_template_files) + end + + end + + end + + describe "and the files have been syntax checked previously" do + before do + @syntax_check.untested_ruby_files.each { |f| @syntax_check.validated(f) } + @syntax_check.untested_template_files.each { |f| @syntax_check.validated(f) } + end + + it "does not syntax check ruby files" do + @syntax_check.should_not_receive(:shell_out) + @syntax_check.validate_ruby_files.should be_true + end + + it "does not syntax check templates" do + @syntax_check.should_not_receive(:shell_out) + @syntax_check.validate_templates.should be_true + end + end + end +end diff --git a/spec/unit/cookbook_loader_spec.rb b/spec/unit/cookbook_loader_spec.rb new file mode 100644 index 0000000000..1d694b5ef6 --- /dev/null +++ b/spec/unit/cookbook_loader_spec.rb @@ -0,0 +1,207 @@ +# +# 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::CookbookLoader do + before(:each) do + @repo_paths = [ File.expand_path(File.join(CHEF_SPEC_DATA, "kitchen")), + File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks")) ] + @cookbook_loader = Chef::CookbookLoader.new(@repo_paths) + end + + describe "loading all cookbooks" do + before(:each) do + @cookbook_loader.load_cookbooks + end + + describe "[]" do + it "should return cookbook objects with []" do + @cookbook_loader[:openldap].should be_a_kind_of(Chef::CookbookVersion) + end + + it "should raise an exception if it cannot find a cookbook with []" do + lambda { @cookbook_loader[:monkeypoop] }.should raise_error(Chef::Exceptions::CookbookNotFoundInRepo) + end + + it "should allow you to look up available cookbooks with [] and a symbol" do + @cookbook_loader[:openldap].name.should eql(:openldap) + end + + it "should allow you to look up available cookbooks with [] and a string" do + @cookbook_loader["openldap"].name.should eql(:openldap) + end + end + + describe "each" do + it "should allow you to iterate over cookbooks with each" do + seen = Hash.new + @cookbook_loader.each do |cookbook_name, cookbook| + seen[cookbook_name] = true + end + seen.should have_key("openldap") + seen.should have_key("apache2") + end + + it "should iterate in alphabetical order" do + seen = Array.new + @cookbook_loader.each do |cookbook_name, cookbook| + seen << cookbook_name + end + seen[0].should == "angrybash" + seen[1].should == "apache2" + seen[2].should == "borken" + seen[3].should == "java" + seen[4].should == "openldap" + end + end + + describe "load_cookbooks" do + it "should find all the cookbooks in the cookbook path" do + Chef::Config.cookbook_path << File.expand_path(File.join(CHEF_SPEC_DATA, "hidden-cookbooks")) + @cookbook_loader.load_cookbooks + @cookbook_loader.should have_key(:openldap) + @cookbook_loader.should have_key(:apache2) + end + + it "should allow you to override an attribute file via cookbook_path" do + @cookbook_loader[:openldap].attribute_filenames.detect { |f| + f =~ /cookbooks\/openldap\/attributes\/default.rb/ + }.should_not eql(nil) + @cookbook_loader[:openldap].attribute_filenames.detect { |f| + f =~ /kitchen\/openldap\/attributes\/default.rb/ + }.should eql(nil) + end + + it "should load different attribute files from deeper paths" do + @cookbook_loader[:openldap].attribute_filenames.detect { |f| + f =~ /kitchen\/openldap\/attributes\/robinson.rb/ + }.should_not eql(nil) + end + + it "should allow you to override a definition file via cookbook_path" do + @cookbook_loader[:openldap].definition_filenames.detect { |f| + f =~ /cookbooks\/openldap\/definitions\/client.rb/ + }.should_not eql(nil) + @cookbook_loader[:openldap].definition_filenames.detect { |f| + f =~ /kitchen\/openldap\/definitions\/client.rb/ + }.should eql(nil) + end + + it "should load definition files from deeper paths" do + @cookbook_loader[:openldap].definition_filenames.detect { |f| + f =~ /kitchen\/openldap\/definitions\/drewbarrymore.rb/ + }.should_not eql(nil) + end + + it "should allow you to override a recipe file via cookbook_path" do + @cookbook_loader[:openldap].recipe_filenames.detect { |f| + f =~ /cookbooks\/openldap\/recipes\/gigantor.rb/ + }.should_not eql(nil) + @cookbook_loader[:openldap].recipe_filenames.detect { |f| + f =~ /kitchen\/openldap\/recipes\/gigantor.rb/ + }.should eql(nil) + end + + it "should load recipe files from deeper paths" do + @cookbook_loader[:openldap].recipe_filenames.detect { |f| + f =~ /kitchen\/openldap\/recipes\/woot.rb/ + }.should_not eql(nil) + end + + it "should allow you to have an 'ignore' file, which skips loading files in later cookbooks" do + @cookbook_loader[:openldap].recipe_filenames.detect { |f| + f =~ /kitchen\/openldap\/recipes\/ignoreme.rb/ + }.should eql(nil) + end + + it "should find files that start with a ." do + @cookbook_loader[:openldap].file_filenames.detect { |f| + f =~ /\.dotfile$/ + }.should =~ /\.dotfile$/ + @cookbook_loader[:openldap].file_filenames.detect { |f| + f =~ /\.ssh\/id_rsa$/ + }.should =~ /\.ssh\/id_rsa$/ + end + + it "should load the metadata for the cookbook" do + @cookbook_loader.metadata[:openldap].name.should == :openldap + @cookbook_loader.metadata[:openldap].should be_a_kind_of(Chef::Cookbook::Metadata) + end + + it "should check each cookbook directory only once (CHEF-3487)" do + cookbooks = [] + @repo_paths.each do |repo_path| + cookbooks |= Dir[File.join(repo_path, "*")] + end + cookbooks.each do |cookbook| + File.should_receive(:directory?).with(cookbook).once; + end + @cookbook_loader.load_cookbooks + end + end # load_cookbooks + + end # loading all cookbooks + + describe "loading only one cookbook" do + before(:each) do + @cookbook_loader = Chef::CookbookLoader.new(@repo_paths) + @cookbook_loader.load_cookbook("openldap") + end + + it "should have loaded the correct cookbook" do + seen = Hash.new + @cookbook_loader.each do |cookbook_name, cookbook| + seen[cookbook_name] = true + end + seen.should have_key("openldap") + end + + it "should not load the cookbook again when accessed" do + @cookbook_loader.should_not_receive('load_cookbook') + @cookbook_loader["openldap"] + end + + it "should not load the other cookbooks" do + seen = Hash.new + @cookbook_loader.each do |cookbook_name, cookbook| + seen[cookbook_name] = true + end + seen.should_not have_key("apache2") + end + + it "should load another cookbook lazily with []" do + @cookbook_loader["apache2"].should be_a_kind_of(Chef::CookbookVersion) + end + + describe "loading all cookbooks after loading only one cookbook" do + before(:each) do + @cookbook_loader.load_cookbooks + end + + it "should load all cookbooks" do + seen = Hash.new + @cookbook_loader.each do |cookbook_name, cookbook| + seen[cookbook_name] = true + end + seen.should have_key("openldap") + seen.should have_key("apache2") + end + end + end # loading only one cookbook +end diff --git a/spec/unit/cookbook_manifest_spec.rb b/spec/unit/cookbook_manifest_spec.rb new file mode 100644 index 0000000000..7da87e93a5 --- /dev/null +++ b/spec/unit/cookbook_manifest_spec.rb @@ -0,0 +1,554 @@ +# +# Author:: Tim Hinderliter (<tim@opscode.com>) +# Author:: Christopher Walters (<cw@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' + +describe "Chef::CookbookVersion manifest" do + before(:each) do + @cookbook = Chef::CookbookVersion.new "test-cookbook" + @cookbook.manifest = { + "files" => + [ + # afile.rb + { + :name => "afile.rb", + :path => "files/host-examplehost.example.org/afile.rb", + :checksum => "csum-host", + :specificity => "host-examplehost.example.org" + }, + { + :name => "afile.rb", + :path => "files/ubuntu-9.10/afile.rb", + :checksum => "csum-platver-full", + :specificity => "ubuntu-9.10" + }, + { + :name => "afile.rb", + :path => "files/newubuntu-9/afile.rb", + :checksum => "csum-platver-partial", + :specificity => "newubuntu-9" + }, + { + :name => "afile.rb", + :path => "files/ubuntu/afile.rb", + :checksum => "csum-plat", + :specificity => "ubuntu" + }, + { + :name => "afile.rb", + :path => "files/default/afile.rb", + :checksum => "csum-default", + :specificity => "default" + }, + + # for different/odd platform_versions + { + :name => "bfile.rb", + :path => "files/fakeos-2.0.rc.1/bfile.rb", + :checksum => "csum2-platver-full", + :specificity => "fakeos-2.0.rc.1" + }, + { + :name => "bfile.rb", + :path => "files/newfakeos-2.0.rc/bfile.rb", + :checksum => "csum2-platver-partial", + :specificity => "newfakeos-2.0.rc" + }, + { + :name => "bfile.rb", + :path => "files/fakeos-maple tree/bfile.rb", + :checksum => "csum3-platver-full", + :specificity => "maple tree" + }, + { + :name => "bfile.rb", + :path => "files/fakeos-1/bfile.rb", + :checksum => "csum4-platver-full", + :specificity => "fakeos-1" + }, + + # directory adirectory + { + :name => "anotherfile1.rb", + :path => "files/host-examplehost.example.org/adirectory/anotherfile1.rb.host", + :checksum => "csum-host-1", + :specificity => "host-examplehost.example.org" + }, + { + :name => "anotherfile2.rb", + :path => "files/host-examplehost.example.org/adirectory/anotherfile2.rb.host", + :checksum => "csum-host-2", + :specificity => "host-examplehost.example.org" + }, + + { + :name => "anotherfile1.rb", + :path => "files/ubuntu-9.10/adirectory/anotherfile1.rb.platform-full-version", + :checksum => "csum-platver-full-1", + :specificity => "ubuntu-9.10" + }, + { + :name => "anotherfile2.rb", + :path => "files/ubuntu-9.10/adirectory/anotherfile2.rb.platform-full-version", + :checksum => "csum-platver-full-2", + :specificity => "ubuntu-9.10" + }, + + { + :name => "anotherfile1.rb", + :path => "files/newubuntu-9/adirectory/anotherfile1.rb.platform-partial-version", + :checksum => "csum-platver-partial-1", + :specificity => "newubuntu-9" + }, + { + :name => "anotherfile2.rb", + :path => "files/newubuntu-9/adirectory/anotherfile2.rb.platform-partial-version", + :checksum => "csum-platver-partial-2", + :specificity => "nweubuntu-9" + }, + + { + :name => "anotherfile1.rb", + :path => "files/ubuntu/adirectory/anotherfile1.rb.platform", + :checksum => "csum-plat-1", + :specificity => "ubuntu" + }, + { + :name => "anotherfile2.rb", + :path => "files/ubuntu/adirectory/anotherfile2.rb.platform", + :checksum => "csum-plat-2", + :specificity => "ubuntu" + }, + + { + :name => "anotherfile1.rb", + :path => "files/default/adirectory/anotherfile1.rb.default", + :checksum => "csum-default-1", + :specificity => "default" + }, + { + :name => "anotherfile2.rb", + :path => "files/default/adirectory/anotherfile2.rb.default", + :checksum => "csum-default-2", + :specificity => "default" + }, + # for different/odd platform_versions + { + :name => "anotherfile1.rb", + :path => "files/fakeos-2.0.rc.1/adirectory/anotherfile1.rb.platform-full-version", + :checksum => "csum2-platver-full-1", + :specificity => "fakeos-2.0.rc.1" + }, + { + :name => "anotherfile2.rb", + :path => "files/fakeos-2.0.rc.1/adirectory/anotherfile2.rb.platform-full-version", + :checksum => "csum2-platver-full-2", + :specificity => "fakeos-2.0.rc.1" + }, + { + :name => "anotherfile1.rb", + :path => "files/newfakeos-2.0.rc.1/adirectory/anotherfile1.rb.platform-partial-version", + :checksum => "csum2-platver-partial-1", + :specificity => "newfakeos-2.0.rc" + }, + { + :name => "anotherfile2.rb", + :path => "files/newfakeos-2.0.rc.1/adirectory/anotherfile2.rb.platform-partial-version", + :checksum => "csum2-platver-partial-2", + :specificity => "newfakeos-2.0.rc" + }, + { + :name => "anotherfile1.rb", + :path => "files/fakeos-maple tree/adirectory/anotherfile1.rb.platform-full-version", + :checksum => "csum3-platver-full-1", + :specificity => "fakeos-maple tree" + }, + { + :name => "anotherfile2.rb", + :path => "files/fakeos-maple tree/adirectory/anotherfile2.rb.platform-full-version", + :checksum => "csum3-platver-full-2", + :specificity => "fakeos-maple tree" + }, + { + :name => "anotherfile1.rb", + :path => "files/fakeos-1/adirectory/anotherfile1.rb.platform-full-version", + :checksum => "csum4-platver-full-1", + :specificity => "fakeos-1" + }, + { + :name => "anotherfile2.rb", + :path => "files/fakeos-1/adirectory/anotherfile2.rb.platform-full-version", + :checksum => "csum4-platver-full-2", + :specificity => "fakeos-1" + }, + ] + } + + end + + + it "should return a manifest record based on priority preference: host" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "ubuntu" + node.automatic_attrs[:platform_version] = "9.10" + node.automatic_attrs[:fqdn] = "examplehost.example.org" + + manifest_record = @cookbook.preferred_manifest_record(node, :files, "afile.rb") + manifest_record.should_not be_nil + manifest_record[:checksum].should == "csum-host" + end + + it "should return a manifest record based on priority preference: platform & full version" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "ubuntu" + node.automatic_attrs[:platform_version] = "9.10" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_record = @cookbook.preferred_manifest_record(node, :files, "afile.rb") + manifest_record.should_not be_nil + manifest_record[:checksum].should == "csum-platver-full" + end + + it "should return a manifest record based on priority preference: platform & partial version" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "newubuntu" + node.automatic_attrs[:platform_version] = "9.10" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_record = @cookbook.preferred_manifest_record(node, :files, "afile.rb") + manifest_record.should_not be_nil + manifest_record[:checksum].should == "csum-platver-partial" + end + + it "should return a manifest record based on priority preference: platform only" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "ubuntu" + node.automatic_attrs[:platform_version] = "1.0" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_record = @cookbook.preferred_manifest_record(node, :files, "afile.rb") + manifest_record.should_not be_nil + manifest_record[:checksum].should == "csum-plat" + end + + it "should return a manifest record based on priority preference: default" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "notubuntu" + node.automatic_attrs[:platform_version] = "1.0" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_record = @cookbook.preferred_manifest_record(node, :files, "afile.rb") + manifest_record.should_not be_nil + manifest_record[:checksum].should == "csum-default" + end + + it "should return a manifest record based on priority preference: platform & full version - platform_version variant 1" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "fakeos" + node.automatic_attrs[:platform_version] = "2.0.rc.1" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_record = @cookbook.preferred_manifest_record(node, :files, "bfile.rb") + manifest_record.should_not be_nil + manifest_record[:checksum].should == "csum2-platver-full" + end + + it "should return a manifest record based on priority preference: platform & partial version - platform_version variant 1" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "newfakeos" + node.automatic_attrs[:platform_version] = "2.0.rc.1" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_record = @cookbook.preferred_manifest_record(node, :files, "bfile.rb") + manifest_record.should_not be_nil + manifest_record[:checksum].should == "csum2-platver-partial" + end + + it "should return a manifest record based on priority preference: platform & full version - platform_version variant 2" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "fakeos" + node.automatic_attrs[:platform_version] = "maple tree" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_record = @cookbook.preferred_manifest_record(node, :files, "bfile.rb") + manifest_record.should_not be_nil + manifest_record[:checksum].should == "csum3-platver-full" + end + + it "should return a manifest record based on priority preference: platform & full version - platform_version variant 3" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "fakeos" + node.automatic_attrs[:platform_version] = "1" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_record = @cookbook.preferred_manifest_record(node, :files, "bfile.rb") + manifest_record.should_not be_nil + manifest_record[:checksum].should == "csum4-platver-full" + end + + describe "when fetching the contents of a directory by file specificity" do + + it "should return a directory of manifest records based on priority preference: host" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "ubuntu" + node.automatic_attrs[:platform_version] = "9.10" + node.automatic_attrs[:fqdn] = "examplehost.example.org" + + manifest_records = @cookbook.preferred_manifest_records_for_directory(node, :files, "adirectory") + manifest_records.should_not be_nil + manifest_records.size.should == 2 + + checksums = manifest_records.map{ |manifest_record| manifest_record[:checksum] } + checksums.sort.should == ["csum-host-1", "csum-host-2"] + end + + it "should return a directory of manifest records based on priority preference: platform & full version" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "ubuntu" + node.automatic_attrs[:platform_version] = "9.10" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_records = @cookbook.preferred_manifest_records_for_directory(node, :files, "adirectory") + manifest_records.should_not be_nil + manifest_records.size.should == 2 + + checksums = manifest_records.map{ |manifest_record| manifest_record[:checksum] } + checksums.sort.should == ["csum-platver-full-1", "csum-platver-full-2"] + end + + it "should return a directory of manifest records based on priority preference: platform & partial version" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "newubuntu" + node.automatic_attrs[:platform_version] = "9.10" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_records = @cookbook.preferred_manifest_records_for_directory(node, :files, "adirectory") + manifest_records.should_not be_nil + manifest_records.size.should == 2 + + checksums = manifest_records.map{ |manifest_record| manifest_record[:checksum] } + checksums.sort.should == ["csum-platver-partial-1", "csum-platver-partial-2"] + end + + it "should return a directory of manifest records based on priority preference: platform only" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "ubuntu" + node.automatic_attrs[:platform_version] = "1.0" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_records = @cookbook.preferred_manifest_records_for_directory(node, :files, "adirectory") + manifest_records.should_not be_nil + manifest_records.size.should == 2 + + checksums = manifest_records.map{ |manifest_record| manifest_record[:checksum] } + checksums.sort.should == ["csum-plat-1", "csum-plat-2"] + end + + it "should return a directory of manifest records based on priority preference: default" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "notubuntu" + node.automatic_attrs[:platform_version] = "1.0" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_records = @cookbook.preferred_manifest_records_for_directory(node, :files, "adirectory") + manifest_records.should_not be_nil + manifest_records.size.should == 2 + + checksums = manifest_records.map{ |manifest_record| manifest_record[:checksum] } + checksums.sort.should == ["csum-default-1", "csum-default-2"] + end + + it "should return a manifest record based on priority preference: platform & full version - platform_version variant 1" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "fakeos" + node.automatic_attrs[:platform_version] = "2.0.rc.1" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_records = @cookbook.preferred_manifest_records_for_directory(node, :files, "adirectory") + manifest_records.should_not be_nil + manifest_records.size.should == 2 + + checksums = manifest_records.map{ |manifest_record| manifest_record[:checksum] } + checksums.sort.should == ["csum2-platver-full-1", "csum2-platver-full-2"] + end + + it "should return a manifest record based on priority preference: platform & partial version - platform_version variant 1" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "newfakeos" + node.automatic_attrs[:platform_version] = "2.0.rc.1" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_records = @cookbook.preferred_manifest_records_for_directory(node, :files, "adirectory") + manifest_records.should_not be_nil + manifest_records.size.should == 2 + + checksums = manifest_records.map{ |manifest_record| manifest_record[:checksum] } + checksums.sort.should == ["csum2-platver-partial-1", "csum2-platver-partial-2"] + end + + it "should return a manifest record based on priority preference: platform & full version - platform_version variant 2" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "fakeos" + node.automatic_attrs[:platform_version] = "maple tree" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_records = @cookbook.preferred_manifest_records_for_directory(node, :files, "adirectory") + manifest_records.should_not be_nil + manifest_records.size.should == 2 + + checksums = manifest_records.map{ |manifest_record| manifest_record[:checksum] } + checksums.sort.should == ["csum3-platver-full-1", "csum3-platver-full-2"] + end + + it "should return a manifest record based on priority preference: platform & full version - platform_version variant 3" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "fakeos" + node.automatic_attrs[:platform_version] = "1" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + manifest_records = @cookbook.preferred_manifest_records_for_directory(node, :files, "adirectory") + manifest_records.should_not be_nil + manifest_records.size.should == 2 + + checksums = manifest_records.map{ |manifest_record| manifest_record[:checksum] } + checksums.sort.should == ["csum4-platver-full-1", "csum4-platver-full-2"] + end + end + + ## Globbing the relative paths out of the manifest records ## + + describe "when globbing for relative file paths based on filespecificity" do + it "should return a list of relative paths based on priority preference: host" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "ubuntu" + node.automatic_attrs[:platform_version] = "9.10" + node.automatic_attrs[:fqdn] = "examplehost.example.org" + + filenames = @cookbook.relative_filenames_in_preferred_directory(node, :files, "adirectory") + filenames.should_not be_nil + filenames.size.should == 2 + + filenames.sort.should == ['anotherfile1.rb.host', 'anotherfile2.rb.host'] + end + + it "should return a list of relative paths based on priority preference: platform & full version" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "ubuntu" + node.automatic_attrs[:platform_version] = "9.10" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + filenames = @cookbook.relative_filenames_in_preferred_directory(node, :files, "adirectory") + filenames.should_not be_nil + filenames.size.should == 2 + + filenames.sort.should == ['anotherfile1.rb.platform-full-version', 'anotherfile2.rb.platform-full-version'] + end + + it "should return a list of relative paths based on priority preference: platform & partial version" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "newubuntu" + node.automatic_attrs[:platform_version] = "9.10" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + filenames = @cookbook.relative_filenames_in_preferred_directory(node, :files, "adirectory") + filenames.should_not be_nil + filenames.size.should == 2 + + filenames.sort.should == ['anotherfile1.rb.platform-partial-version', 'anotherfile2.rb.platform-partial-version'] + end + + it "should return a list of relative paths based on priority preference: platform only" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "ubuntu" + node.automatic_attrs[:platform_version] = "1.0" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + filenames = @cookbook.relative_filenames_in_preferred_directory(node, :files, "adirectory") + filenames.should_not be_nil + filenames.size.should == 2 + + filenames.sort.should == ['anotherfile1.rb.platform', 'anotherfile2.rb.platform'] + end + + it "should return a list of relative paths based on priority preference: default" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "notubuntu" + node.automatic_attrs[:platform_version] = "1.0" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + filenames = @cookbook.relative_filenames_in_preferred_directory(node, :files, "adirectory") + filenames.should_not be_nil + filenames.size.should == 2 + + filenames.sort.should == ['anotherfile1.rb.default', 'anotherfile2.rb.default'] + end + + it "should return a list of relative paths based on priority preference: platform & full version - platform_version variant 1" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "fakeos" + node.automatic_attrs[:platform_version] = "2.0.rc.1" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + filenames = @cookbook.relative_filenames_in_preferred_directory(node, :files, "adirectory") + filenames.should_not be_nil + filenames.size.should == 2 + + filenames.sort.should == ['anotherfile1.rb.platform-full-version', 'anotherfile2.rb.platform-full-version'] + end + + it "should return a list of relative paths based on priority preference: platform & partial version - platform_version variant 1" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "newfakeos" + node.automatic_attrs[:platform_version] = "2.0.rc.1" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + filenames = @cookbook.relative_filenames_in_preferred_directory(node, :files, "adirectory") + filenames.should_not be_nil + filenames.size.should == 2 + + filenames.sort.should == ['anotherfile1.rb.platform-partial-version', 'anotherfile2.rb.platform-partial-version'] + end + + it "should return a list of relative paths based on priority preference: platform & full version - platform_version variant 2" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "fakeos" + node.automatic_attrs[:platform_version] = "maple tree" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + filenames = @cookbook.relative_filenames_in_preferred_directory(node, :files, "adirectory") + filenames.should_not be_nil + filenames.size.should == 2 + + filenames.sort.should == ['anotherfile1.rb.platform-full-version', 'anotherfile2.rb.platform-full-version'] + end + + it "should return a list of relative paths based on priority preference: platform & full version - platform_version variant 3" do + node = Chef::Node.new + node.automatic_attrs[:platform] = "fakeos" + node.automatic_attrs[:platform_version] = "1" + node.automatic_attrs[:fqdn] = "differenthost.example.org" + + filenames = @cookbook.relative_filenames_in_preferred_directory(node, :files, "adirectory") + filenames.should_not be_nil + filenames.size.should == 2 + + filenames.sort.should == ['anotherfile1.rb.platform-full-version', 'anotherfile2.rb.platform-full-version'] + end + end +end diff --git a/spec/unit/cookbook_spec.rb b/spec/unit/cookbook_spec.rb new file mode 100644 index 0000000000..c28a5c7a2a --- /dev/null +++ b/spec/unit/cookbook_spec.rb @@ -0,0 +1,84 @@ +# +# 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::CookbookVersion do +# COOKBOOK_PATH = File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "cookbooks", "openldap")) + before(:each) do + @cookbook_repo = File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "cookbooks")) + cl = Chef::CookbookLoader.new(@cookbook_repo) + cl.load_cookbooks + @cookbook_collection = Chef::CookbookCollection.new(cl) + @cookbook = @cookbook_collection[:openldap] + @node = Chef::Node.new + @node.name "JuliaChild" + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, @cookbook_collection, @events) + end + + it "should have a name" do + @cookbook.name.should == :openldap + end + + it "should allow you to set the list of attribute files and create the mapping from short names to paths" do + @cookbook.attribute_filenames = [ "attributes/one.rb", "attributes/two.rb" ] + @cookbook.attribute_filenames.should == [ "attributes/one.rb", "attributes/two.rb" ] + @cookbook.attribute_filenames_by_short_filename.keys.sort.should eql(["one", "two"]) + @cookbook.attribute_filenames_by_short_filename["one"].should == "attributes/one.rb" + @cookbook.attribute_filenames_by_short_filename["two"].should == "attributes/two.rb" + end + + it "should allow you to set the list of recipe files and create the mapping of recipe short name to filename" do + @cookbook.recipe_filenames = [ "recipes/one.rb", "recipes/two.rb" ] + @cookbook.recipe_filenames.should == [ "recipes/one.rb", "recipes/two.rb" ] + @cookbook.recipe_filenames_by_name.keys.sort.should eql(["one", "two"]) + @cookbook.recipe_filenames_by_name["one"].should == "recipes/one.rb" + @cookbook.recipe_filenames_by_name["two"].should == "recipes/two.rb" + end + + it "should generate a list of recipes by fully-qualified name" do + @cookbook.recipe_filenames = [ "recipes/one.rb", "/recipes/two.rb", "three.rb" ] + @cookbook.fully_qualified_recipe_names.include?("openldap::one").should == true + @cookbook.fully_qualified_recipe_names.include?("openldap::two").should == true + @cookbook.fully_qualified_recipe_names.include?("openldap::three").should == true + end + + it "should find a preferred file" do + pending + end + + it "should not return an unchanged preferred file" do + pending + @cookbook.preferred_filename(@node, :files, 'a-filename', 'the-checksum').should be_nil + end + + it "should allow you to include a fully-qualified recipe using the DSL" do + # DSL method include_recipe allows multiple arguments, so extract the first + recipe = @run_context.include_recipe("openldap::gigantor").first + + recipe.recipe_name.should == "gigantor" + recipe.cookbook_name.should == :openldap + @run_context.resource_collection[0].name.should == "blanket" + end + + it "should raise an ArgumentException if you try to load a bad recipe name" do + lambda { @cookbook.load_recipe("doesnt_exist", @node) }.should raise_error(ArgumentError) + end + +end diff --git a/spec/unit/cookbook_version_spec.rb b/spec/unit/cookbook_version_spec.rb new file mode 100644 index 0000000000..85e1db1fae --- /dev/null +++ b/spec/unit/cookbook_version_spec.rb @@ -0,0 +1,307 @@ +# +# 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' + +describe Chef::CookbookVersion do + describe "when first created" do + before do + @cookbook_version = Chef::CookbookVersion.new("tatft") + end + + it "has a name" do + @cookbook_version.name.should == 'tatft' + end + + it "has no attribute files" do + @cookbook_version.attribute_filenames.should be_empty + end + + it "has no resource definition files" do + @cookbook_version.definition_filenames.should be_empty + end + + it "has no cookbook files" do + @cookbook_version.file_filenames.should be_empty + end + + it "has no recipe files" do + @cookbook_version.recipe_filenames.should be_empty + end + + it "has no library files" do + @cookbook_version.library_filenames.should be_empty + end + + it "has no LWRP resource files" do + @cookbook_version.resource_filenames.should be_empty + end + + it "has no LWRP provider files" do + @cookbook_version.provider_filenames.should be_empty + end + + it "has no metadata files" do + @cookbook_version.metadata_filenames.should be_empty + end + + it "is not frozen" do + @cookbook_version.should_not be_frozen_version + end + + it "can be frozen" do + @cookbook_version.freeze_version + @cookbook_version.should be_frozen_version + end + + it "is \"ready\"" do + # WTF is this? what are the valid states? and why aren't they set with encapsulating methods? + # [Dan 15-Jul-2010] + @cookbook_version.status.should == :ready + end + + it "has empty metadata" do + @cookbook_version.metadata.should == Chef::Cookbook::Metadata.new + end + + it "creates a manifest hash of its contents" do + expected = {"recipes"=>[], + "definitions"=>[], + "libraries"=>[], + "attributes"=>[], + "files"=>[], + "templates"=>[], + "resources"=>[], + "providers"=>[], + "root_files"=>[], + "cookbook_name"=>"tatft", + "metadata"=>Chef::Cookbook::Metadata.new, + "version"=>"0.0.0", + "name"=>"tatft-0.0.0"} + @cookbook_version.manifest.should == expected + end + end + + describe "after the cookbook has been loaded" do + MD5 = /[0-9a-f]{32}/ + + before do + # Currently the cookbook loader finds all the files then tells CookbookVersion + # where they are. + @cookbook_version = Chef::CookbookVersion.new("tatft") + + @cookbook = Hash.new { |hash, key| hash[key] = [] } + + cookbook_root = File.join(CHEF_SPEC_DATA, 'cb_version_cookbooks', 'tatft') + + # Dunno if the paths here are representitive of what is set by CookbookLoader... + @cookbook[:attribute_filenames] = Dir[File.join(cookbook_root, 'attributes', '**', '*.rb')] + @cookbook[:definition_filenames] = Dir[File.join(cookbook_root, 'definitions', '**', '*.rb')] + @cookbook[:file_filenames] = Dir[File.join(cookbook_root, 'files', '**', '*.tgz')] + @cookbook[:recipe_filenames] = Dir[File.join(cookbook_root, 'recipes', '**', '*.rb')] + @cookbook[:template_filenames] = Dir[File.join(cookbook_root, 'templates', '**', '*.erb')] + @cookbook[:library_filenames] = Dir[File.join(cookbook_root, 'libraries', '**', '*.rb')] + @cookbook[:resource_filenames] = Dir[File.join(cookbook_root, 'resources', '**', '*.rb')] + @cookbook[:provider_filenames] = Dir[File.join(cookbook_root, 'providers', '**', '*.rb')] + @cookbook[:root_filenames] = Array(File.join(cookbook_root, 'README.rdoc')) + @cookbook[:metadata_filenames] = Array(File.join(cookbook_root, 'metadata.json')) + + @cookbook_version.attribute_filenames = @cookbook[:attribute_filenames] + @cookbook_version.definition_filenames = @cookbook[:definition_filenames] + @cookbook_version.recipe_filenames = @cookbook[:recipe_filenames] + @cookbook_version.template_filenames = @cookbook[:template_filenames] + @cookbook_version.file_filenames = @cookbook[:file_filenames] + @cookbook_version.library_filenames = @cookbook[:library_filenames] + @cookbook_version.resource_filenames = @cookbook[:resource_filenames] + @cookbook_version.provider_filenames = @cookbook[:provider_filenames] + @cookbook_version.root_filenames = @cookbook[:root_filenames] + @cookbook_version.metadata_filenames = @cookbook[:metadata_filenames] + end + + it "generates a manifest containing the cookbook's files" do + manifest = @cookbook_version.manifest + + manifest["metadata"].should == Chef::Cookbook::Metadata.new + manifest["cookbook_name"].should == "tatft" + + manifest["recipes"].should have(1).recipe_file + + recipe = manifest["recipes"].first + recipe["name"].should == "default.rb" + recipe["path"].should == "recipes/default.rb" + recipe["checksum"].should match(MD5) + recipe["specificity"].should == "default" + + manifest["definitions"].should have(1).definition_file + + definition = manifest["definitions"].first + definition["name"].should == "runit_service.rb" + definition["path"].should == "definitions/runit_service.rb" + definition["checksum"].should match(MD5) + definition["specificity"].should == "default" + + manifest["libraries"].should have(1).library_file + + library = manifest["libraries"].first + library["name"].should == "ownage.rb" + library["path"].should == "libraries/ownage.rb" + library["checksum"].should match(MD5) + library["specificity"].should == "default" + + manifest["attributes"].should have(1).attribute_file + + attribute_file = manifest["attributes"].first + attribute_file["name"].should == "default.rb" + attribute_file["path"].should == "attributes/default.rb" + attribute_file["checksum"].should match(MD5) + attribute_file["specificity"].should == "default" + + manifest["files"].should have(1).cookbook_file + + cookbook_file = manifest["files"].first + cookbook_file["name"].should == "giant_blob.tgz" + cookbook_file["path"].should == "files/default/giant_blob.tgz" + cookbook_file["checksum"].should match(MD5) + cookbook_file["specificity"].should == "default" + + manifest["templates"].should have(1).template + + template = manifest["templates"].first + template["name"].should == "configuration.erb" + template["path"].should == "templates/default/configuration.erb" + template["checksum"].should match(MD5) + template["specificity"].should == "default" + + manifest["resources"].should have(1).lwr + + lwr = manifest["resources"].first + lwr["name"].should == "lwr.rb" + lwr["path"].should == "resources/lwr.rb" + lwr["checksum"].should match(MD5) + lwr["specificity"].should == "default" + + manifest["providers"].should have(1).lwp + + lwp = manifest["providers"].first + lwp["name"].should == "lwp.rb" + lwp["path"].should == "providers/lwp.rb" + lwp["checksum"].should match(MD5) + lwp["specificity"].should == "default" + + manifest["root_files"].should have(1).file_in_the_cookbook_root + + readme = manifest["root_files"].first + readme["name"].should == "README.rdoc" + readme["path"].should == "README.rdoc" + readme["checksum"].should match(MD5) + readme["specificity"].should == "default" + end + + describe "raises an error when attempting to load a missing cookbook_file and" do + before do + node = Chef::Node.new.tap do |n| + n.name("sample.node") + n.automatic_attrs[:fqdn] = "sample.example.com" + n.automatic_attrs[:platform] = "ubuntu" + n.automatic_attrs[:platform_version] = "10.04" + end + @attempt_to_load_file = lambda { @cookbook_version.preferred_manifest_record(node, :files, "no-such-thing.txt") } + end + + it "describes the cookbook and version" do + useful_explanation = Regexp.new(Regexp.escape("Cookbook 'tatft' (0.0.0) does not contain")) + @attempt_to_load_file.should raise_error(Chef::Exceptions::FileNotFound, useful_explanation) + end + + it "lists suggested places to look" do + useful_explanation = Regexp.new(Regexp.escape("files/default/no-such-thing.txt")) + @attempt_to_load_file.should raise_error(Chef::Exceptions::FileNotFound, useful_explanation) + end + end + + end + + + describe "<=>" do + + it "should sort based on the version number" do + examples = [ + # smaller, larger + ["1.0", "2.0"], + ["1.2.3", "1.2.4"], + ["1.2.3", "1.3.0"], + ["1.2.3", "1.3"], + ["1.2.3", "2.1.1"], + ["1.2.3", "2.1"], + ["1.2", "1.2.4"], + ["1.2", "1.3.0"], + ["1.2", "1.3"], + ["1.2", "2.1.1"], + ["1.2", "2.1"] + ] + examples.each do |smaller, larger| + sm = Chef::CookbookVersion.new("foo") + lg = Chef::CookbookVersion.new("foo") + sm.version = smaller + lg.version = larger + sm.should be < lg + lg.should be > sm + sm.should_not == lg + end + end + + it "should equate versions 1.2 and 1.2.0" do + a = Chef::CookbookVersion.new("foo") + b = Chef::CookbookVersion.new("foo") + a.version = "1.2" + b.version = "1.2.0" + a.should == b + end + + + it "should not allow you to sort cookbooks with different names" do + apt = Chef::CookbookVersion.new "apt" + apt.version = "1.0" + god = Chef::CookbookVersion.new "god" + god.version = "2.0" + lambda {apt <=> god}.should raise_error(Chef::Exceptions::CookbookVersionNameMismatch) + end + end + + describe "when you set a version" do + before do + @cbv = Chef::CookbookVersion.new("version validation") + end + it "should accept valid cookbook versions" do + good_versions = %w(1.2 1.2.3 1000.80.50000 0.300.25) + good_versions.each do |v| + @cbv.version = v + end + end + + it "should raise InvalidVersion for bad cookbook versions" do + bad_versions = ["1.2.3.4", "1.2.a4", "1", "a", "1.2 3", "1.2 a", + "1 2 3", "1-2-3", "1_2_3", "1.2_3", "1.2-3"] + the_error = Chef::Exceptions::InvalidCookbookVersion + bad_versions.each do |v| + lambda {@cbv.version = v}.should raise_error(the_error) + end + end + + end + +end diff --git a/spec/unit/daemon_spec.rb b/spec/unit/daemon_spec.rb new file mode 100644 index 0000000000..1efdf2a2ad --- /dev/null +++ b/spec/unit/daemon_spec.rb @@ -0,0 +1,281 @@ +# +# Author:: AJ Christensen (<aj@junglist.gen.nz>) +# 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::Daemon do + before do + @original_config = Chef::Config.configuration + if windows? + mock_struct = #Struct::Passwd.new(nil, nil, 111, 111) + mock_struct = OpenStruct.new(:uid => 2342, :gid => 2342) + Etc.stub!(:getpwnam).and_return mock_struct + Etc.stub!(:getgrnam).and_return mock_struct + # mock unimplemented methods + Process.stub!(:initgroups).and_return nil + Process::GID.stub!(:change_privilege).and_return 11 + Process::UID.stub!(:change_privilege).and_return 11 + end + end + + after do + Chef::Config.configuration.replace(@original_config) + end + + describe ".running?" do + + before do + Chef::Daemon.name = "spec" + end + + describe "when a pid file exists" do + + before do + Chef::Daemon.stub!(:pid_from_file).and_return(1337) + end + + it "should check that there is a process matching the pidfile" do + Process.should_receive(:kill).with(0, 1337) + Chef::Daemon.running? + end + + end + + describe "when the pid file is nonexistent" do + + before do + Chef::Daemon.stub!(:pid_from_file).and_return(nil) + end + + it "should return false" do + Chef::Daemon.running?.should be_false + end + + end + end + + describe ".pid_file" do + + describe "when the pid_file option has been set" do + + before do + Chef::Config[:pid_file] = "/var/run/chef/chef-client.pid" + end + + after do + Chef::Config.configuration.replace(@original_config) + end + + it "should return the supplied value" do + Chef::Daemon.pid_file.should eql("/var/run/chef/chef-client.pid") + end + end + + describe "without the pid_file option set" do + + before do + Chef::Config[:pid_file] = nil + Chef::Daemon.name = "chef-client" + end + + it "should return a valued based on @name" do + Chef::Daemon.pid_file.should eql("/tmp/chef-client.pid") + end + + end + end + + describe ".pid_from_file" do + + before do + Chef::Config[:pid_file] = "/var/run/chef/chef-client.pid" + end + + it "should suck the pid out of pid_file" do + File.should_receive(:read).with("/var/run/chef/chef-client.pid").and_return("1337") + Chef::Daemon.pid_from_file + end + end + + describe ".save_pid_file" do + + before do + Process.stub!(:pid).and_return(1337) + Chef::Config[:pid_file] = "/var/run/chef/chef-client.pid" + Chef::Application.stub!(:fatal!).and_return(true) + @f_mock = mock(File, { :print => true, :close => true, :write => true }) + File.stub!(:open).with("/var/run/chef/chef-client.pid", "w").and_yield(@f_mock) + end + + it "should try and create the parent directory" do + FileUtils.should_receive(:mkdir_p).with("/var/run/chef") + Chef::Daemon.save_pid_file + end + + it "should open the pid file for writing" do + File.should_receive(:open).with("/var/run/chef/chef-client.pid", "w") + Chef::Daemon.save_pid_file + end + + it "should write the pid, converted to string, to the pid file" do + @f_mock.should_receive(:write).with("1337").once.and_return(true) + Chef::Daemon.save_pid_file + end + + end + + describe ".remove_pid_file" do + before do + Chef::Config[:pid_file] = "/var/run/chef/chef-client.pid" + end + + describe "when the pid file exists" do + + before do + File.stub!(:exists?).with("/var/run/chef/chef-client.pid").and_return(true) + end + + it "should remove the file" do + FileUtils.should_receive(:rm).with("/var/run/chef/chef-client.pid") + Chef::Daemon.remove_pid_file + end + + + end + + describe "when the pid file does not exist" do + + before do + File.stub!(:exists?).with("/var/run/chef/chef-client.pid").and_return(false) + end + + it "should not remove the file" do + FileUtils.should_not_receive(:rm) + Chef::Daemon.remove_pid_file + end + + end + end + + describe ".change_privilege" do + + before do + Chef::Application.stub!(:fatal!).and_return(true) + Chef::Config[:user] = 'aj' + Dir.stub!(:chdir) + end + + it "changes the working directory to root" do + Dir.rspec_reset + Dir.should_receive(:chdir).with("/").and_return(0) + Chef::Daemon.change_privilege + end + + describe "when the user and group options are supplied" do + + before do + Chef::Config[:group] = 'staff' + end + + it "should log an appropriate info message" do + Chef::Log.should_receive(:info).with("About to change privilege to aj:staff") + Chef::Daemon.change_privilege + end + + it "should call _change_privilege with the user and group" do + Chef::Daemon.should_receive(:_change_privilege).with("aj", "staff") + Chef::Daemon.change_privilege + end + end + + describe "when just the user option is supplied" do + before do + Chef::Config[:group] = nil + end + + it "should log an appropriate info message" do + Chef::Log.should_receive(:info).with("About to change privilege to aj") + Chef::Daemon.change_privilege + end + + it "should call _change_privilege with just the user" do + Chef::Daemon.should_receive(:_change_privilege).with("aj") + Chef::Daemon.change_privilege + end + end + end + + describe "._change_privilege" do + + before do + Process.stub!(:euid).and_return(0) + Process.stub!(:egid).and_return(0) + + Process::UID.stub!(:change_privilege).and_return(nil) + Process::GID.stub!(:change_privilege).and_return(nil) + + @pw_user = mock("Struct::Passwd", :uid => 501) + @pw_group = mock("Struct::Group", :gid => 20) + + Process.stub!(:initgroups).and_return(true) + + Etc.stub!(:getpwnam).and_return(@pw_user) + Etc.stub!(:getgrnam).and_return(@pw_group) + end + + describe "with sufficient privileges" do + before do + Process.stub!(:euid).and_return(0) + Process.stub!(:egid).and_return(0) + end + + it "should initialize the supplemental group list" do + Process.should_receive(:initgroups).with("aj", 20) + Chef::Daemon._change_privilege("aj") + end + + it "should attempt to change the process GID" do + Process::GID.should_receive(:change_privilege).with(20).and_return(20) + Chef::Daemon._change_privilege("aj") + end + + it "should attempt to change the process UID" do + Process::UID.should_receive(:change_privilege).with(501).and_return(501) + Chef::Daemon._change_privilege("aj") + end + end + + describe "with insufficient privileges" do + before do + Process.stub!(:euid).and_return(999) + Process.stub!(:egid).and_return(999) + end + + it "should log an appropriate error message and fail miserably" do + Process.stub!(:initgroups).and_raise(Errno::EPERM) + error = "Operation not permitted" + if RUBY_PLATFORM.match("solaris2") + error = "Not owner" + end + Chef::Application.should_receive(:fatal!).with("Permission denied when trying to change 999:999 to 501:20. #{error}") + Chef::Daemon._change_privilege("aj") + end + end + + end +end diff --git a/spec/unit/data_bag_item_spec.rb b/spec/unit/data_bag_item_spec.rb new file mode 100644 index 0000000000..6f46c81054 --- /dev/null +++ b/spec/unit/data_bag_item_spec.rb @@ -0,0 +1,280 @@ +# +# 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 'chef/data_bag_item' + +describe Chef::DataBagItem do + before(:each) do + @data_bag_item = Chef::DataBagItem.new + end + + describe "initialize" do + it "should be a Chef::DataBagItem" do + @data_bag_item.should be_a_kind_of(Chef::DataBagItem) + end + end + + describe "data_bag" do + it "should let you set the data_bag to a string" do + @data_bag_item.data_bag("clowns").should == "clowns" + end + + it "should return the current data_bag type" do + @data_bag_item.data_bag "clowns" + @data_bag_item.data_bag.should == "clowns" + end + + it "should not accept spaces" do + lambda { @data_bag_item.data_bag "clown masters" }.should raise_error(ArgumentError) + end + + it "should throw an ArgumentError if you feed it anything but a string" do + lambda { @data_bag_item.data_bag Hash.new }.should raise_error(ArgumentError) + end + end + + describe "raw_data" do + it "should let you set the raw_data with a hash" do + lambda { @data_bag_item.raw_data = { "id" => "octahedron" } }.should_not raise_error + end + + it "should let you set the raw_data from a mash" do + lambda { @data_bag_item.raw_data = Mash.new({ "id" => "octahedron" }) }.should_not raise_error + end + + it "should raise an exception if you set the raw data without a key" do + lambda { @data_bag_item.raw_data = { "monkey" => "pants" } }.should raise_error(ArgumentError) + end + + it "should raise an exception if you set the raw data to something other than a hash" do + lambda { @data_bag_item.raw_data = "katie rules" }.should raise_error(ArgumentError) + end + + it "should accept alphanum/-/_ for the id" do + lambda { @data_bag_item.raw_data = { "id" => "h1-_" } }.should_not raise_error(ArgumentError) + end + + it "should raise an exception if the id contains anything but alphanum/-/_" do + lambda { @data_bag_item.raw_data = { "id" => "!@#" } }.should raise_error(ArgumentError) + end + + it "should return the raw data" do + @data_bag_item.raw_data = { "id" => "highway_of_emptiness" } + @data_bag_item.raw_data.should == { "id" => "highway_of_emptiness" } + end + + it "should be a Mash by default" do + @data_bag_item.raw_data.should be_a_kind_of(Mash) + end + end + + describe "object_name" do + before(:each) do + @data_bag_item.data_bag("dreams") + @data_bag_item.raw_data = { "id" => "the_beatdown" } + end + + it "should return an object name based on the bag name and the raw_data id" do + @data_bag_item.object_name.should == "data_bag_item_dreams_the_beatdown" + end + end + + describe "class method object_name" do + it "should return an object name based based on the bag name and an id" do + Chef::DataBagItem.object_name("zen", "master").should == "data_bag_item_zen_master" + end + end + + describe "when used like a Hash" do + before(:each) do + @data_bag_item.raw_data = { "id" => "journey", "trials" => "been through" } + end + + it "responds to keys" do + @data_bag_item.keys.should include("id") + @data_bag_item.keys.should include("trials") + end + + it "supports element reference with []" do + @data_bag_item["id"].should == "journey" + end + + it "implements all the methods of Hash" do + methods = [:rehash, :to_hash, :[], :fetch, :[]=, :store, :default, + :default=, :default_proc, :index, :size, :length, + :empty?, :each_value, :each_key, :each_pair, :each, :keys, :values, + :values_at, :delete, :delete_if, :reject!, :clear, + :invert, :update, :replace, :merge!, :merge, :has_key?, :has_value?, + :key?, :value?] + methods.each do |m| + @data_bag_item.should respond_to(m) + end + end + + end + + describe "to_hash" do + before(:each) do + @data_bag_item.data_bag("still_lost") + @data_bag_item.raw_data = { "id" => "whoa", "i_know" => "kung_fu" } + @to_hash = @data_bag_item.to_hash + end + + it "should return a hash" do + @to_hash.should be_a_kind_of(Hash) + end + + it "should have the raw_data keys as top level keys" do + @to_hash["id"].should == "whoa" + @to_hash["i_know"].should == "kung_fu" + end + + it "should have the chef_type of data_bag_item" do + @to_hash["chef_type"].should == "data_bag_item" + end + + it "should have the data_bag set" do + @to_hash["data_bag"].should == "still_lost" + end + end + + describe "when deserializing from JSON" do + before(:each) do + @data_bag_item.data_bag('mars_volta') + @data_bag_item.raw_data = { "id" => "octahedron", "snooze" => { "finally" => :world_will }} + @deserial = Chef::JSONCompat.from_json(@data_bag_item.to_json) + end + + it "should deserialize to a Chef::DataBagItem object" do + @deserial.should be_a_kind_of(Chef::DataBagItem) + end + + it "should have a matching 'data_bag' value" do + @deserial.data_bag.should == @data_bag_item.data_bag + end + + it "should have a matching 'id' key" do + @deserial["id"].should == "octahedron" + end + + it "should have a matching 'snooze' key" do + @deserial["snooze"].should == { "finally" => "world_will" } + end + end + + describe "when converting to a string" do + it "converts to a string in the form data_bag_item[ID]" do + @data_bag_item['id'] = "heart of darkness" + @data_bag_item.to_s.should == 'data_bag_item[heart of darkness]' + end + + it "inspects as data_bag_item[BAG, ID, RAW_DATA]" do + raw_data = {"id" => "heart_of_darkness", "author" => "Conrad"} + @data_bag_item.raw_data = raw_data + @data_bag_item.data_bag("books") + + @data_bag_item.inspect.should == "data_bag_item[\"books\", \"heart_of_darkness\", #{raw_data.inspect}]" + end + end + + describe "save" do + before do + @rest = mock("Chef::REST") + Chef::REST.stub!(:new).and_return(@rest) + @data_bag_item['id'] = "heart of darkness" + raw_data = {"id" => "heart_of_darkness", "author" => "Conrad"} + @data_bag_item.raw_data = raw_data + @data_bag_item.data_bag("books") + end + it "should update the item when it already exists" do + @rest.should_receive(:put_rest).with("data/books/heart_of_darkness", @data_bag_item) + @data_bag_item.save + end + + it "should create if the item is not found" do + exception = mock("404 error", :code => "404") + @rest.should_receive(:put_rest).and_raise(Net::HTTPServerException.new("foo", exception)) + @rest.should_receive(:post_rest).with("data/books", @data_bag_item) + @data_bag_item.save + end + describe "when whyrun mode is enabled" do + before do + Chef::Config[:why_run] = true + end + after do + Chef::Config[:why_run] = false + end + it "should not save" do + @rest.should_not_receive(:put_rest) + @rest.should_not_receive(:post_rest) + @data_bag_item.data_bag("books") + @data_bag_item.save + end + end + + + end + + describe "when loading" do + before do + @data_bag_item.raw_data = {"id" => "charlie", "shell" => "zsh", "ssh_keys" => %w{key1 key2}} + @data_bag_item.data_bag("users") + end + + describe "from an API call" do + before do + @http_client = mock("Chef::REST") + Chef::REST.stub!(:new).and_return(@http_client) + end + + it "converts raw data to a data bag item" do + @http_client.should_receive(:get_rest).with("data/users/charlie").and_return(@data_bag_item.to_hash) + item = Chef::DataBagItem.load(:users, "charlie") + item.should be_a_kind_of(Chef::DataBagItem) + item.should == @data_bag_item + end + + it "does not convert when a DataBagItem is returned from the API call" do + @http_client.should_receive(:get_rest).with("data/users/charlie").and_return(@data_bag_item) + item = Chef::DataBagItem.load(:users, "charlie") + item.should be_a_kind_of(Chef::DataBagItem) + item.should equal(@data_bag_item) + end + end + + describe "in solo mode" do + before do + Chef::Config[:solo] = true + end + + after do + Chef::Config[:solo] = false + end + + it "converts the raw data to a data bag item" do + Chef::DataBag.should_receive(:load).with('users').and_return({'charlie' => @data_bag_item.to_hash}) + item = Chef::DataBagItem.load('users', 'charlie') + item.should be_a_kind_of(Chef::DataBagItem) + item.should == @data_bag_item + end + end + + end + +end diff --git a/spec/unit/data_bag_spec.rb b/spec/unit/data_bag_spec.rb new file mode 100644 index 0000000000..ec45e28a9b --- /dev/null +++ b/spec/unit/data_bag_spec.rb @@ -0,0 +1,169 @@ +# +# 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 'chef/data_bag' + +describe Chef::DataBag do + before(:each) do + @data_bag = Chef::DataBag.new + end + + describe "initialize" do + it "should be a Chef::DataBag" do + @data_bag.should be_a_kind_of(Chef::DataBag) + end + end + + describe "name" do + it "should let you set the name to a string" do + @data_bag.name("clowns").should == "clowns" + end + + it "should return the current name" do + @data_bag.name "clowns" + @data_bag.name.should == "clowns" + end + + it "should not accept spaces" do + lambda { @data_bag.name "clown masters" }.should raise_error(ArgumentError) + end + + it "should throw an ArgumentError if you feed it anything but a string" do + lambda { @data_bag.name Hash.new }.should raise_error(ArgumentError) + end + end + + describe "deserialize" do + before(:each) do + @data_bag.name('mars_volta') + @deserial = Chef::JSONCompat.from_json(@data_bag.to_json) + end + + it "should deserialize to a Chef::DataBag object" do + @deserial.should be_a_kind_of(Chef::DataBag) + end + + %w{ + name + }.each do |t| + it "should match '#{t}'" do + @deserial.send(t.to_sym).should == @data_bag.send(t.to_sym) + end + end + + end + + describe "when saving" do + before do + @data_bag.name('piggly_wiggly') + @rest = mock("Chef::REST") + Chef::REST.stub!(:new).and_return(@rest) + end + + it "should update the data bag when it already exists" do + @rest.should_receive(:put_rest).with("data/piggly_wiggly", @data_bag) + @data_bag.save + end + + it "should create the data bag when it is not found" do + exception = mock("404 error", :code => "404") + @rest.should_receive(:put_rest).and_raise(Net::HTTPServerException.new("foo", exception)) + @rest.should_receive(:post_rest).with("data", @data_bag) + @data_bag.save + end + + describe "when whyrun mode is enabled" do + before do + Chef::Config[:why_run] = true + end + after do + Chef::Config[:why_run] = false + end + it "should not save" do + @rest.should_not_receive(:put_rest) + @rest.should_not_receive(:post_rest) + @data_bag.save + end + end + + end + describe "when loading" do + describe "from an API call" do + before do + Chef::Config[:chef_server_url] = 'https://myserver.example.com' + @http_client = mock('Chef::REST') + end + + it "should get the data bag from the server" do + Chef::REST.should_receive(:new).with('https://myserver.example.com').and_return(@http_client) + @http_client.should_receive(:get_rest).with('data/foo') + Chef::DataBag.load('foo') + end + + it "should return the data bag" do + Chef::REST.stub!(:new).and_return(@http_client) + @http_client.should_receive(:get_rest).with('data/foo').and_return({'bar' => 'https://myserver.example.com/data/foo/bar'}) + data_bag = Chef::DataBag.load('foo') + data_bag.should == {'bar' => 'https://myserver.example.com/data/foo/bar'} + end + end + + describe "in solo mode" do + before do + Chef::Config[:solo] = true + Chef::Config[:data_bag_path] = '/var/chef/data_bags' + end + + after do + Chef::Config[:solo] = false + end + + it "should get the data bag from the data_bag_path" do + File.should_receive(:directory?).with('/var/chef/data_bags').and_return(true) + Dir.should_receive(:glob).with('/var/chef/data_bags/foo/*.json').and_return([]) + Chef::DataBag.load('foo') + end + + it "should get the data bag from the data_bag_path by symbolic name" do + File.should_receive(:directory?).with('/var/chef/data_bags').and_return(true) + Dir.should_receive(:glob).with('/var/chef/data_bags/foo/*.json').and_return([]) + Chef::DataBag.load(:foo) + end + + it "should return the data bag" do + File.should_receive(:directory?).with('/var/chef/data_bags').and_return(true) + Dir.stub!(:glob).and_return(["/var/chef/data_bags/foo/bar.json", "/var/chef/data_bags/foo/baz.json"]) + IO.should_receive(:read).with('/var/chef/data_bags/foo/bar.json').and_return('{"id": "bar", "name": "Bob Bar" }') + IO.should_receive(:read).with('/var/chef/data_bags/foo/baz.json').and_return('{"id": "baz", "name": "John Baz" }') + data_bag = Chef::DataBag.load('foo') + data_bag.should == { 'bar' => { 'id' => 'bar', 'name' => 'Bob Bar' }, 'baz' => { 'id' => 'baz', 'name' => 'John Baz' }} + end + + it 'should raise an error if the configured data_bag_path is invalid' do + File.should_receive(:directory?).with('/var/chef/data_bags').and_return(false) + + lambda { + Chef::DataBag.load('foo') + }.should raise_error Chef::Exceptions::InvalidDataBagPath, "Data bag path '/var/chef/data_bags' is invalid" + end + + end + end + +end diff --git a/spec/unit/dsl/data_query_spec.rb b/spec/unit/dsl/data_query_spec.rb new file mode 100644 index 0000000000..8960ad9957 --- /dev/null +++ b/spec/unit/dsl/data_query_spec.rb @@ -0,0 +1,66 @@ +# +# Author:: Seth Falcon (<seth@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 'chef/dsl/data_query' + +class DataQueryDSLTester + include Chef::DSL::DataQuery +end + +describe Chef::DSL::DataQuery do + before(:each) do + @language = DataQueryDSLTester.new + @node = Hash.new + @language.stub!(:node).and_return(@node) + end + + describe "when loading data bags and items" do + it "lists the items in a data bag" do + Chef::DataBag.should_receive(:load).with("bag_name").and_return("item_1" => "http://url_for/item_1", "item_2" => "http://url_for/item_2") + @language.data_bag("bag_name").sort.should == %w[item_1 item_2] + end + + it "validates the name of the data bag you're trying to load" do + lambda {@language.data_bag("!# %^&& ")}.should raise_error(Chef::Exceptions::InvalidDataBagName) + end + + it "fetches a data bag item" do + @item = Chef::DataBagItem.new + @item.data_bag("bag_name") + @item.raw_data = {"id" => "item_name", "FUU" => "FUU"} + Chef::DataBagItem.should_receive(:load).with("bag_name", "item_name").and_return(@item) + @language.data_bag_item("bag_name", "item_name").should == @item + end + + it "validates the name of the data bag you're trying to load an item from" do + lambda {@language.data_bag_item(" %%^& ", "item_name")}.should raise_error(Chef::Exceptions::InvalidDataBagName) + end + + it "validates the id of the data bag item you're trying to load" do + lambda {@language.data_bag_item("bag_name", " 987 (*&()")}.should raise_error(Chef::Exceptions::InvalidDataBagItemID) + end + + it "validates that the id of the data bag item is not nil" do + lambda {@language.data_bag_item("bag_name", nil)}.should raise_error(Chef::Exceptions::InvalidDataBagItemID) + end + + end + +end + diff --git a/spec/unit/dsl/platfrom_introspection_spec.rb b/spec/unit/dsl/platfrom_introspection_spec.rb new file mode 100644 index 0000000000..e6cc7ad9ff --- /dev/null +++ b/spec/unit/dsl/platfrom_introspection_spec.rb @@ -0,0 +1,272 @@ +# +# Author:: Seth Falcon (<seth@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 'chef/dsl/platform_introspection' + +class LanguageTester + include Chef::DSL::PlatformIntrospection +end + +describe Chef::DSL::PlatformIntrospection do + before(:each) do + @language = LanguageTester.new + @node = Hash.new + @language.stub!(:node).and_return(@node) + @platform_hash = {} + %w{openbsd freebsd}.each do |x| + @platform_hash[x] = { + "default" => x, + "1.2.3" => "#{x}-1.2.3" + } + end + @platform_hash["debian"] = {["5", "6"] => "debian-5/6", "default" => "debian"} + @platform_hash["default"] = "default" + + @platform_family_hash = { + "debian" => "debian value", + [:rhel, :fedora] => "redhatty value", + "suse" => "suse value", + :default => "default value" + } + end + + it "returns a default value when there is no known platform" do + @node = Hash.new + @language.value_for_platform(@platform_hash).should == "default" + end + + it "returns a default value when there is no known platform family" do + @language.value_for_platform_family(@platform_family_hash).should == "default value" + end + + it "returns a default value when the current platform doesn't match" do + @node[:platform] = "not-a-known-platform" + @language.value_for_platform(@platform_hash).should == "default" + end + + it "returns a default value when current platform_family doesn't match" do + @node[:platform_family] = "ultra-derived-linux" + @language.value_for_platform_family(@platform_family_hash).should == "default value" + end + + it "returns a value based on the current platform" do + @node[:platform] = "openbsd" + @language.value_for_platform(@platform_hash).should == "openbsd" + end + + it "returns a value based on the current platform family" do + @node[:platform_family] = "debian" + @language.value_for_platform_family(@platform_family_hash).should == "debian value" + end + + it "returns a version-specific value based on the current platform" do + @node[:platform] = "openbsd" + @node[:platform_version] = "1.2.3" + @language.value_for_platform(@platform_hash).should == "openbsd-1.2.3" + end + + it "returns a value based on the current platform if version not found" do + @node[:platform] = "openbsd" + @node[:platform_version] = "0.0.0" + @language.value_for_platform(@platform_hash).should == "openbsd" + end + + describe "when platform versions is an array" do + it "returns a version-specific value based on the current platform" do + @node[:platform] = "debian" + @node[:platform_version] = "6" + @language.value_for_platform(@platform_hash).should == "debian-5/6" + end + + it "returns a value based on the current platform if version not found" do + @node[:platform] = "debian" + @node[:platform_version] = "0.0.0" + @language.value_for_platform(@platform_hash).should == "debian" + end + end + + describe "when checking platform?" do + before(:each) do + @language = LanguageTester.new + @node = Hash.new + @language.stub!(:node).and_return(@node) + end + + it "returns true if the node is a provided platform and platforms are provided as symbols" do + @node[:platform] = 'ubuntu' + @language.platform?([:redhat, :ubuntu]).should == true + end + + it "returns true if the node is a provided platform and platforms are provided as strings" do + @node[:platform] = 'ubuntu' + @language.platform?(["redhat", "ubuntu"]).should == true + end + + it "returns false if the node is not of the provided platforms" do + @node[:platform] = 'ubuntu' + @language.platform?(:splatlinux).should == false + end + end + + describe "when checking platform_family?" do + before(:each) do + @language = LanguageTester.new + @node = Hash.new + @language.stub!(:node).and_return(@node) + end + + it "returns true if the node is in a provided platform family and families are provided as symbols" do + @node[:platform_family] = 'debian' + @language.platform_family?([:rhel, :debian]).should == true + end + + it "returns true if the node is a provided platform and platforms are provided as strings" do + @node[:platform_family] = 'rhel' + @language.platform_family?(["rhel", "debian"]).should == true + end + + it "returns false if the node is not of the provided platforms" do + @node[:platform_family] = 'suse' + @language.platform_family?(:splatlinux).should == false + end + + it "returns false if the node is not of the provided platforms and platform_family is not set" do + @language.platform_family?(:splatlinux).should == false + end + + end + # NOTE: this is a regression test for bug CHEF-1514 + describe "when the value is an array" do + before do + @platform_hash = { + "debian" => { "4.0" => [ :restart, :reload ], "default" => [ :restart, :reload, :status ] }, + "ubuntu" => { "default" => [ :restart, :reload, :status ] }, + "centos" => { "default" => [ :restart, :reload, :status ] }, + "redhat" => { "default" => [ :restart, :reload, :status ] }, + "fedora" => { "default" => [ :restart, :reload, :status ] }, + "default" => { "default" => [:restart, :reload ] }} + end + + it "returns the correct default for a given platform" do + @node[:platform] = "debian" + @node[:platform_version] = '9000' + @language.value_for_platform(@platform_hash).should == [ :restart, :reload, :status ] + end + + it "returns the correct platform+version specific value " do + @node[:platform] = "debian" + @node[:platform_version] = '4.0' + @language.value_for_platform(@platform_hash).should == [:restart, :reload] + end + end + +end + +describe Chef::DSL::PlatformIntrospection::PlatformDependentValue do + before do + platform_hash = { + :openbsd => {:default => 'free, functional, secure'}, + [:redhat, :centos, :fedora, :scientific] => {:default => '"stable"'}, + :ubuntu => {'10.04' => 'using upstart more', :default => 'using init more'}, + :default => 'bork da bork' + } + @platform_specific_value = Chef::DSL::PlatformIntrospection::PlatformDependentValue.new(platform_hash) + end + + it "returns the default value when the platform doesn't match" do + @platform_specific_value.value_for_node(:platform => :dos).should == 'bork da bork' + end + + it "returns a value for a platform set as a group" do + @platform_specific_value.value_for_node(:platform => :centos).should == '"stable"' + end + + it "returns a value for the platform when it was set as a symbol but fetched as a string" do + @platform_specific_value.value_for_node(:platform => "centos").should == '"stable"' + end + + it "returns a value for a specific platform version" do + node = {:platform => 'ubuntu', :platform_version => '10.04'} + @platform_specific_value.value_for_node(node).should == 'using upstart more' + end + + it "returns a platform-default value if the platform version doesn't match an explicit one" do + node = {:platform => 'ubuntu', :platform_version => '9.10' } + @platform_specific_value.value_for_node(node).should == 'using init more' + end + + it "returns nil if there is no default and no platforms match" do + # this matches the behavior in the original implementation. + # whether or not it's correct is another matter. + platform_specific_value = Chef::DSL::PlatformIntrospection::PlatformDependentValue.new({}) + platform_specific_value.value_for_node(:platform => 'foo').should be_nil + end + + it "raises an argument error if the platform hash is not correctly structured" do + bad_hash = {:ubuntu => :foo} # should be :ubuntu => {:default => 'foo'} + lambda {Chef::DSL::PlatformIntrospection::PlatformDependentValue.new(bad_hash)}.should raise_error(ArgumentError) + end + +end +describe Chef::DSL::PlatformIntrospection::PlatformFamilyDependentValue do + before do + @array_values = [:stop, :start, :reload] + + @platform_family_hash = { + "debian" => "debian value", + [:rhel, "fedora"] => "redhatty value", + "suse" => @array_values, + :gentoo => "gentoo value", + :default => "default value" + } + + @platform_family_value = Chef::DSL::PlatformIntrospection::PlatformFamilyDependentValue.new(@platform_family_hash) + end + + it "returns the default value when the platform family doesn't match" do + @platform_family_value.value_for_node(:platform_family => :os2).should == 'default value' + end + + + it "returns a value for the platform family when it was set as a string but fetched as a symbol" do + @platform_family_value.value_for_node(:platform_family => :debian).should == "debian value" + end + + it "returns a value for the platform family when it was set as a symbol but fetched as a string" do + @platform_family_value.value_for_node(:platform_family => "gentoo").should == "gentoo value" + end + + it "returns an array value stored for a platform family" do + @platform_family_value.value_for_node(:platform_family => "suse").should == @array_values + end + + it "returns a value for the platform family when it was set within an array hash key as a symbol" do + @platform_family_value.value_for_node(:platform_family => :rhel).should == "redhatty value" + end + + it "returns a value for the platform family when it was set within an array hash key as a string" do + @platform_family_value.value_for_node(:platform_family => "fedora").should == "redhatty value" + end + + it "returns nil if there is no default and no platforms match" do + platform_specific_value = Chef::DSL::PlatformIntrospection::PlatformFamilyDependentValue.new({}) + platform_specific_value.value_for_node(:platform_family => 'foo').should be_nil + end + +end diff --git a/spec/unit/encrypted_data_bag_item_spec.rb b/spec/unit/encrypted_data_bag_item_spec.rb new file mode 100644 index 0000000000..0b052b56c6 --- /dev/null +++ b/spec/unit/encrypted_data_bag_item_spec.rb @@ -0,0 +1,122 @@ +# +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright 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' +require 'chef/encrypted_data_bag_item' + +describe Chef::EncryptedDataBagItem do + before(:each) do + @secret = "abc123SECRET" + @plain_data = { + "id" => "item_name", + "greeting" => "hello", + "nested" => { "a1" => [1, 2, 3], "a2" => { "b1" => true }} + } + @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data, + @secret) + end + + describe "encrypting" do + + it "should not encrypt the 'id' key" do + @enc_data["id"].should == "item_name" + end + + it "should encrypt 'greeting'" do + @enc_data["greeting"].should_not == @plain_data["greeting"] + end + + it "should encrypt 'nested'" do + nested = @enc_data["nested"] + nested.class.should == String + nested.should_not == @plain_data["nested"] + end + + it "from_plain_hash" do + eh1 = Chef::EncryptedDataBagItem.from_plain_hash(@plain_data, @secret) + eh1.class.should == Chef::EncryptedDataBagItem + end + end + + describe "decrypting" do + before(:each) do + @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data, + @secret) + @eh = Chef::EncryptedDataBagItem.new(@enc_data, @secret) + end + + it "doesn't try to decrypt 'id'" do + @eh["id"].should == @plain_data["id"] + end + + it "decrypts 'greeting'" do + @eh["greeting"].should == @plain_data["greeting"] + end + + it "decrypts 'nested'" do + @eh["nested"].should == @plain_data["nested"] + end + + it "decrypts everyting via to_hash" do + @eh.to_hash.should == @plain_data + end + + it "handles missing keys gracefully" do + @eh["no-such-key"].should be_nil + end + end + + describe "loading" do + it "should defer to Chef::DataBagItem.load" do + Chef::DataBagItem.stub(:load).with(:the_bag, "my_codes").and_return(@enc_data) + edbi = Chef::EncryptedDataBagItem.load(:the_bag, "my_codes", @secret) + edbi["greeting"].should == @plain_data["greeting"] + end + end + + describe "load_secret" do + it "should read from the default path" do + default_path = "/etc/chef/encrypted_data_bag_secret" + ::File.stub(:exists?).with(default_path).and_return(true) + IO.stub(:read).with(default_path).and_return("opensesame") + Chef::EncryptedDataBagItem.load_secret().should == "opensesame" + end + + it "should read from Chef::Config[:encrypted_data_bag_secret]" do + path = "/var/mysecret" + Chef::Config[:encrypted_data_bag_secret] = path + ::File.stub(:exists?).with(path).and_return(true) + IO.stub(:read).with(path).and_return("opensesame") + Chef::EncryptedDataBagItem.load_secret().should == "opensesame" + end + + it "should read from a specified path" do + path = "/var/mysecret" + ::File.stub(:exists?).with(path).and_return(true) + IO.stub(:read).with(path).and_return("opensesame") + Chef::EncryptedDataBagItem.load_secret(path).should == "opensesame" + end + + it "should read from a URL" do + path = "http://www.opscode.com/" + fake_file = StringIO.new("opensesame") + Kernel.stub(:open).with(path).and_return(fake_file) + Chef::EncryptedDataBagItem.load_secret(path).should == "opensesame" + end + end +end diff --git a/spec/unit/environment_spec.rb b/spec/unit/environment_spec.rb new file mode 100644 index 0000000000..97f0c3395e --- /dev/null +++ b/spec/unit/environment_spec.rb @@ -0,0 +1,362 @@ +# +# Author:: Stephen Delano (<stephen@ospcode.com>) +# Author:: Seth Falcon (<seth@ospcode.com>) +# Author:: John Keiser (<jkeiser@ospcode.com>) +# Copyright:: Copyright 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' +require 'chef/environment' + +describe Chef::Environment do + before(:each) do + @environment = Chef::Environment.new + end + + describe "initialize" do + it "should be a Chef::Environment" do + @environment.should be_a_kind_of(Chef::Environment) + end + end + + describe "name" do + it "should let you set the name to a string" do + @environment.name("production").should == "production" + end + + it "should return the current name" do + @environment.name("production") + @environment.name.should == "production" + end + + it "should not accept spaces" do + lambda { @environment.name("production environment") }.should raise_error(ArgumentError) + end + + it "should not accept anything but strings" do + lambda { @environment.name(Array.new) }.should raise_error(ArgumentError) + lambda { @environment.name(Hash.new) }.should raise_error(ArgumentError) + lambda { @environment.name(2) }.should raise_error(ArgumentError) + end + end + + describe "description" do + it "should let you set the description to a string" do + @environment.description("this is my test environment").should == "this is my test environment" + end + + it "should return the correct description" do + @environment.description("I like running tests") + @environment.description.should == "I like running tests" + end + + it "should not accept anything but strings" do + lambda { @environment.description(Array.new) }.should raise_error(ArgumentError) + lambda { @environment.description(Hash.new) }.should raise_error(ArgumentError) + lambda { @environment.description(42) }.should raise_error(ArgumentError) + end + end + + describe "default attributes" do + it "should let you set the attributes hash explicitly" do + @environment.default_attributes({ :one => 'two' }).should == { :one => 'two' } + end + + it "should let you return the attributes hash" do + @environment.default_attributes({ :one => 'two' }) + @environment.default_attributes.should == { :one => 'two' } + end + + it "should throw an ArgumentError if we aren't a kind of hash" do + lambda { @environment.default_attributes(Array.new) }.should raise_error(ArgumentError) + end + end + + describe "override attributes" do + it "should let you set the attributes hash explicitly" do + @environment.override_attributes({ :one => 'two' }).should == { :one => 'two' } + end + + it "should let you return the attributes hash" do + @environment.override_attributes({ :one => 'two' }) + @environment.override_attributes.should == { :one => 'two' } + end + + it "should throw an ArgumentError if we aren't a kind of hash" do + lambda { @environment.override_attributes(Array.new) }.should raise_error(ArgumentError) + end + end + + describe "cookbook_versions" do + before(:each) do + @cookbook_versions = { + "apt" => "= 1.0.0", + "god" => "= 2.0.0", + "apache2" => "= 4.2.0" + } + end + + it "should let you set the cookbook versions in a hash" do + @environment.cookbook_versions(@cookbook_versions).should == @cookbook_versions + end + + it "should return the cookbook versions" do + @environment.cookbook_versions(@cookbook_versions) + @environment.cookbook_versions.should == @cookbook_versions + end + + it "should not accept anything but a hash" do + lambda { @environment.cookbook_versions("I am a string!") }.should raise_error(ArgumentError) + lambda { @environment.cookbook_versions(Array.new) }.should raise_error(ArgumentError) + lambda { @environment.cookbook_versions(42) }.should raise_error(ArgumentError) + end + + it "should validate the hash" do + Chef::Environment.should_receive(:validate_cookbook_versions).with(@cookbook_versions).and_return true + @environment.cookbook_versions(@cookbook_versions) + end + end + + describe "cookbook" do + it "should set the version of the cookbook in the cookbook_versions hash" do + @environment.cookbook("apt", "~> 1.2.3") + @environment.cookbook_versions["apt"].should == "~> 1.2.3" + end + + it "should validate the cookbook version it is passed" do + Chef::Environment.should_receive(:validate_cookbook_version).with(">= 1.2.3").and_return true + @environment.cookbook("apt", ">= 1.2.3") + end + end + + describe "update_from!" do + before(:each) do + @environment.name("prod") + @environment.description("this is prod") + @environment.cookbook_versions({ "apt" => "= 1.2.3" }) + + @example = Chef::Environment.new + @example.name("notevenprod") + @example.description("this is pre-prod") + @example.cookbook_versions({ "apt" => "= 2.3.4" }) + end + + it "should update everything but name" do + @environment.update_from!(@example) + @environment.name.should == "prod" + @environment.description.should == @example.description + @environment.cookbook_versions.should == @example.cookbook_versions + end + end + + describe "to_hash" do + before(:each) do + @environment.name("spec") + @environment.description("Where we run the spec tests") + @environment.cookbook_versions({:apt => "= 1.2.3"}) + @hash = @environment.to_hash + end + + %w{name description cookbook_versions}.each do |t| + it "should include '#{t}'" do + @hash[t].should == @environment.send(t.to_sym) + end + end + + it "should include 'json_class'" do + @hash["json_class"].should == "Chef::Environment" + end + + it "should include 'chef_type'" do + @hash["chef_type"].should == "environment" + end + end + + describe "to_json" do + before(:each) do + @environment.name("spec") + @environment.description("Where we run the spec tests") + @environment.cookbook_versions({:apt => "= 1.2.3"}) + @json = @environment.to_json + end + + %w{name description cookbook_versions}.each do |t| + it "should include '#{t}'" do + @json.should =~ /"#{t}":#{Regexp.escape(@environment.send(t.to_sym).to_json)}/ + end + end + + it "should include 'json_class'" do + @json.should =~ /"json_class":"Chef::Environment"/ + end + + it "should include 'chef_type'" do + @json.should =~ /"chef_type":"environment"/ + end + end + + describe "from_json" do + before(:each) do + @data = { + "name" => "production", + "description" => "We are productive", + "cookbook_versions" => { + "apt" => "= 1.2.3", + "god" => ">= 4.2.0", + "apache2" => "= 2.0.0" + }, + "json_class" => "Chef::Environment", + "chef_type" => "environment" + } + @environment = Chef::JSONCompat.from_json(@data.to_json) + end + + it "should return a Chef::Environment" do + @environment.should be_a_kind_of(Chef::Environment) + end + + %w{name description cookbook_versions}.each do |t| + it "should match '#{t}'" do + @environment.send(t.to_sym).should == @data[t] + end + end + end + + describe "self.validate_cookbook_versions" do + before(:each) do + @cookbook_versions = { + "apt" => "= 1.0.0", + "god" => "= 2.0.0", + "apache2" => "= 4.2.0" + } + end + + it "should validate the version string of each cookbook" do + @cookbook_versions.each do |cookbook, version| + Chef::Environment.should_receive(:validate_cookbook_version).with(version).and_return true + end + Chef::Environment.validate_cookbook_versions(@cookbook_versions) + end + + it "should return false if anything other than a hash is passed as the argument" do + Chef::Environment.validate_cookbook_versions(Array.new).should == false + Chef::Environment.validate_cookbook_versions(42).should == false + Chef::Environment.validate_cookbook_versions(Chef::CookbookVersion.new("meta")).should == false + Chef::Environment.validate_cookbook_versions("cookbook => 1.2.3").should == false + end + end + + describe "self.validate_cookbook_version" do + it "should validate correct version numbers" do + Chef::Environment.validate_cookbook_version("= 1.2.3").should == true + Chef::Environment.validate_cookbook_version(">= 0.0.3").should == true + # A lone version is allowed, interpreted as implicit '=' + Chef::Environment.validate_cookbook_version("1.2.3").should == true + end + + it "should return false when an invalid version is given" do + Chef::Environment.validate_cookbook_version(Chef::CookbookVersion.new("meta")).should == false + Chef::Environment.validate_cookbook_version("= 1.2.3a").should == false + Chef::Environment.validate_cookbook_version("= 1").should == false + Chef::Environment.validate_cookbook_version("= 1.2.3.4").should == false + end + end + + describe "when updating from a parameter hash" do + before do + @environment = Chef::Environment.new + end + + it "updates the name from parameters[:name]" do + @environment.update_from_params(:name => "kurrupt") + @environment.name.should == "kurrupt" + end + + it "validates the name given in the params" do + @environment.update_from_params(:name => "@$%^&*()").should be_false + @environment.invalid_fields[:name].should == %q|Option name's value @$%^&*() does not match regular expression /^[\-[:alnum:]_]+$/| + end + + it "updates the description from parameters[:description]" do + @environment.update_from_params(:description => "wow, writing your own object mapper is kinda painful") + @environment.description.should == "wow, writing your own object mapper is kinda painful" + end + + it "updates cookbook version constraints from the hash in parameters[:cookbook_version_constraints]" do + # NOTE: I'm only choosing this (admittedly weird) structure for the hash b/c the better more obvious + # one, i.e, {:cookbook_version_constraints => {COOKBOOK_NAME => CONSTRAINT}} is difficult to implement + # the way merb does params + params = {:name=>"superbowl", :cookbook_version => {"0" => "apache2 ~> 1.0.0", "1" => "nginx < 2.0.0"}} + @environment.update_from_params(params) + @environment.cookbook_versions.should == {"apache2" => "~> 1.0.0", "nginx" => "< 2.0.0"} + end + + it "validates the cookbook constraints" do + params = {:cookbook_version => {"0" => "apache2 >>> 1.0.0"}} + @environment.update_from_params(params).should be_false + err_msg = @environment.invalid_fields[:cookbook_version]["0"] + err_msg.should == "apache2 >>> 1.0.0 is not a valid cookbook constraint" + end + + it "is not valid if the name is not present" do + @environment.validate_required_attrs_present.should be_false + @environment.invalid_fields[:name].should == "name cannot be empty" + end + + it "is not valid after updating from params if the name is not present" do + @environment.update_from_params({}).should be_false + @environment.invalid_fields[:name].should == "name cannot be empty" + end + + it "updates default attributes from a JSON string in params[:attributes]" do + @environment.update_from_params(:name => "fuuu", :default_attributes => %q|{"fuuu":"RAGE"}|) + @environment.default_attributes.should == {"fuuu" => "RAGE"} + end + + it "updates override attributes from a JSON string in params[:attributes]" do + @environment.update_from_params(:name => "fuuu", :override_attributes => %q|{"foo":"override"}|) + @environment.override_attributes.should == {"foo" => "override"} + end + + end + + describe "api model" do + before(:each) do + @rest = mock("Chef::REST") + Chef::REST.stub!(:new).and_return(@rest) + @query = mock("Chef::Search::Query") + Chef::Search::Query.stub!(:new).and_return(@query) + end + + describe "list" do + describe "inflated" do + it "should return a hash of environment names and objects" do + e1 = mock("Chef::Environment", :name => "one") + @query.should_receive(:search).with(:environment).and_yield(e1) + r = Chef::Environment.list(true) + r["one"].should == e1 + end + end + + it "should return a hash of environment names and urls" do + @rest.should_receive(:get_rest).and_return({ "one" => "http://foo" }) + r = Chef::Environment.list + r["one"].should == "http://foo" + end + end + end + +end diff --git a/spec/unit/exceptions_spec.rb b/spec/unit/exceptions_spec.rb new file mode 100644 index 0000000000..a979d2f6b9 --- /dev/null +++ b/spec/unit/exceptions_spec.rb @@ -0,0 +1,73 @@ +# +# Author:: Thomas Bishop (<bishop.thomas@gmail.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Copyright:: Copyright (c) 2010 Thomas Bishop +# 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::Exceptions do + exception_to_super_class = { + Chef::Exceptions::Application => RuntimeError, + Chef::Exceptions::Cron => RuntimeError, + Chef::Exceptions::Env => RuntimeError, + Chef::Exceptions::Exec => RuntimeError, + Chef::Exceptions::FileNotFound => RuntimeError, + Chef::Exceptions::Package => RuntimeError, + Chef::Exceptions::Service => RuntimeError, + Chef::Exceptions::Route => RuntimeError, + Chef::Exceptions::SearchIndex => RuntimeError, + Chef::Exceptions::Override => RuntimeError, + Chef::Exceptions::UnsupportedAction => RuntimeError, + Chef::Exceptions::MissingLibrary => RuntimeError, + Chef::Exceptions::MissingRole => RuntimeError, + Chef::Exceptions::CannotDetermineNodeName => RuntimeError, + Chef::Exceptions::User => RuntimeError, + Chef::Exceptions::Group => RuntimeError, + Chef::Exceptions::Link => RuntimeError, + Chef::Exceptions::Mount => RuntimeError, + Chef::Exceptions::PrivateKeyMissing => RuntimeError, + Chef::Exceptions::CannotWritePrivateKey => RuntimeError, + Chef::Exceptions::RoleNotFound => RuntimeError, + Chef::Exceptions::ValidationFailed => ArgumentError, + Chef::Exceptions::InvalidPrivateKey => ArgumentError, + Chef::Exceptions::ConfigurationError => ArgumentError, + Chef::Exceptions::RedirectLimitExceeded => RuntimeError, + Chef::Exceptions::AmbiguousRunlistSpecification => ArgumentError, + Chef::Exceptions::CookbookNotFound => RuntimeError, + Chef::Exceptions::AttributeNotFound => RuntimeError, + Chef::Exceptions::InvalidCommandOption => RuntimeError, + Chef::Exceptions::CommandTimeout => RuntimeError, + Mixlib::ShellOut::ShellCommandFailed => RuntimeError, + Chef::Exceptions::RequestedUIDUnavailable => RuntimeError, + Chef::Exceptions::InvalidHomeDirectory => ArgumentError, + Chef::Exceptions::DsclCommandFailed => RuntimeError, + Chef::Exceptions::UserIDNotFound => ArgumentError, + Chef::Exceptions::GroupIDNotFound => ArgumentError, + Chef::Exceptions::InvalidResourceReference => RuntimeError, + Chef::Exceptions::ResourceNotFound => RuntimeError, + Chef::Exceptions::InvalidResourceSpecification => ArgumentError, + Chef::Exceptions::SolrConnectionError => RuntimeError, + Chef::Exceptions::InvalidDataBagPath => ArgumentError + } + + exception_to_super_class.each do |exception, expected_super_class| + it "should have an exception class of #{exception} which inherits from #{expected_super_class}" do + lambda{ raise exception }.should raise_error(expected_super_class) + end + end +end diff --git a/spec/unit/file_access_control_spec.rb b/spec/unit/file_access_control_spec.rb new file mode 100644 index 0000000000..491505c692 --- /dev/null +++ b/spec/unit/file_access_control_spec.rb @@ -0,0 +1,282 @@ +# +# 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::FileAccessControl do + describe "Unix" do + before do + platform_mock :unix do + # we have to re-load the file so the proper + # platform specific module is mixed in + @node = Chef::Node.new + load File.join(File.dirname(__FILE__), "..", "..", "lib", "chef", "file_access_control.rb") + @resource = Chef::Resource::File.new('/tmp/a_file.txt') + @resource.owner('toor') + @resource.group('wheel') + @resource.mode('0400') + + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @current_resource = Chef::Resource::File.new('/tmp/different_file.txt') + @provider_requirements = Chef::Provider::ResourceRequirements.new(@resource, @run_context) + @provider = mock("File provider", :requirements => @provider_requirements) + + @fac = Chef::FileAccessControl.new(@current_resource, @resource, @provider) + end + end + + it "has a resource" do + @fac.resource.should equal(@resource) + end + + it "has a file to manage" do + @fac.file.should == '/tmp/different_file.txt' + end + + it "is not modified yet" do + @fac.should_not be_modified + end + + it "determines the uid of the owner specified by the resource" do + Etc.should_receive(:getpwnam).with('toor').and_return(OpenStruct.new(:uid => 2342)) + @fac.target_uid.should == 2342 + end + + it "raises a Chef::Exceptions::UserIDNotFound error when Etc can't find the user's name" do + Etc.should_receive(:getpwnam).with('toor').and_raise(ArgumentError) + lambda { @fac.target_uid ; @provider_requirements.run(:create) }.should raise_error(Chef::Exceptions::UserIDNotFound, "cannot determine user id for 'toor', does the user exist on this system?") + end + + it "does not attempt to resolve the uid if the user is not specified" do + resource = Chef::Resource::File.new("a file") + fac = Chef::FileAccessControl.new(@current_resource, resource, @provider) + fac.target_uid.should be_nil + end + + it "does not want to update the owner if none is specified" do + resource = Chef::Resource::File.new("a file") + fac = Chef::FileAccessControl.new(@current_resource, resource, @provider) + fac.should_update_owner?.should be_false + end + + it "raises an ArgumentError if the resource's owner is set to something wack" do + @resource.instance_variable_set(:@owner, :diaf) + lambda { @fac.target_uid ; @provider_requirements.run(:create) }.should raise_error(ArgumentError) + end + + it "uses the resource's uid for the target uid when the resource's owner is specified by an integer" do + @resource.owner(2342) + @fac.target_uid.should == 2342 + end + + it "wraps uids to their negative complements to correctly handle negative uids" do + # More: Mac OS X (at least) has negative UIDs for 'nobody' and some other + # users. Ruby doesn't believe in negative UIDs so you get the diminished radix + # complement (i.e., it wraps around the maximum size of C unsigned int) of these + # uids. So we have to get ruby and negative uids to smoke the peace pipe + # with each other. + @resource.owner('nobody') + Etc.should_receive(:getpwnam).with('nobody').and_return(OpenStruct.new(:uid => (4294967294))) + @fac.target_uid.should == -2 + end + + it "does not wrap uids to their negative complements beyond -9" do + # More: when OSX userIDs are created by ActiveDirectory sync, it tends to use huge numbers + # which had been incorrectly wrapped. It does not look like the OSX IDs go below -2 + @resource.owner('bigdude') + Etc.should_receive(:getpwnam).with('bigdude').and_return(OpenStruct.new(:uid => (4294967286))) + @fac.target_uid.should == 4294967286 + end + + it "wants to update the owner when the current owner doesn't match desired" do + @resource.owner(2342) + @fac.should_update_owner?.should be_true + end + + it "includes updating ownership in its list of desired changes" do + resource = Chef::Resource::File.new("a file") + resource.owner(2342) + @current_resource.owner(100) + fac = Chef::FileAccessControl.new(@current_resource, resource, @provider) + fac.describe_changes.should == ["change owner from '100' to '2342'"] + end + + it "sets the file's owner as specified in the resource when the current owner is incorrect" do + @resource.owner(2342) + File.should_receive(:chown).with(2342, nil, '/tmp/different_file.txt') + @fac.set_owner + @fac.should be_modified + end + + it "doesn't set the file's owner if it already matches" do + @resource.owner(2342) + @current_resource.owner(2342) + File.should_not_receive(:chown) + @fac.set_owner + @fac.should_not be_modified + end + + it "doesn't want to update a file's owner when it's already correct" do + @resource.owner(2342) + @current_resource.owner(2342) + @fac.should_update_owner?.should be_false + end + + it "determines the gid of the group specified by the resource" do + Etc.should_receive(:getgrnam).with('wheel').and_return(OpenStruct.new(:gid => 2342)) + @fac.target_gid.should == 2342 + end + + it "uses a user specified gid as the gid" do + @resource.group(2342) + @fac.target_gid.should == 2342 + end + + it "raises a Chef::Exceptions::GroupIDNotFound error when Etc can't find the user's name" do + Etc.should_receive(:getgrnam).with('wheel').and_raise(ArgumentError) + lambda { @fac.target_gid; @provider_requirements.run(:create) }.should raise_error(Chef::Exceptions::GroupIDNotFound, "cannot determine group id for 'wheel', does the group exist on this system?") + end + + it "does not attempt to resolve a gid when none is supplied" do + resource = Chef::Resource::File.new('crab') + fac = Chef::FileAccessControl.new(@current_resource, resource, @provider) + fac.target_gid.should be_nil + end + + it "does not want to update the group when no target group is specified" do + resource = Chef::Resource::File.new('crab') + fac = Chef::FileAccessControl.new(@current_resource, resource, @provider) + fac.should_update_group?.should be_false + end + + it "raises an error when the supplied group name is an alien" do + @resource.instance_variable_set(:@group, :failburger) + lambda { @fac.target_gid; @provider_requirements.run(:create) }.should raise_error(ArgumentError) + end + + it "wants to update the group when the current group doesn't match the target group" do + @resource.group(2342) + @current_resource.group(815) + @fac.should_update_group?.should be_true + end + + it "includes updating the group in the list of changes" do + resource = Chef::Resource::File.new('crab') + resource.group(2342) + @current_resource.group(815) + fac = Chef::FileAccessControl.new(@current_resource, resource, @provider) + fac.describe_changes.should == ["change group from '815' to '2342'"] + end + + it "sets the file's group as specified in the resource when the group is not correct" do + @resource.group(2342) + @current_resource.group(815) + + File.should_receive(:chown).with(nil, 2342, '/tmp/different_file.txt') + @fac.set_group + @fac.should be_modified + end + + it "doesn't want to modify the file's group when the current group is correct" do + @resource.group(2342) + @current_resource.group(2342) + @fac.should_update_group?.should be_false + end + + it "doesnt set the file's group if it is already correct" do + @resource.group(2342) + @current_resource.group(2342) + + # @fac.stub!(:stat).and_return(OpenStruct.new(:gid => 2342)) + File.should_not_receive(:chown) + @fac.set_group + @fac.should_not be_modified + end + + it "uses the supplied mode as octal when it's a string" do + @resource.mode('444') + @fac.target_mode.should == 292 # octal 444 => decimal 292 + end + + it "uses the supplied mode verbatim when it's an integer" do + @resource.mode(00444) + @fac.target_mode.should == 292 + end + + it "does not try to determine the mode when none is given" do + resource = Chef::Resource::File.new('blahblah') + fac = Chef::FileAccessControl.new(@current_resource, resource, @provider) + fac.target_mode.should be_nil + end + + it "doesn't want to update the mode when no target mode is given" do + resource = Chef::Resource::File.new('blahblah') + fac = Chef::FileAccessControl.new(@current_resource, resource, @provider) + fac.should_update_mode?.should be_false + end + + it "wants to update the mode when the desired mode does not match the current mode" do + @current_resource.mode("0644") + @fac.should_update_mode?.should be_true + end + + it "includes changing the mode in the list of desired changes" do + resource = Chef::Resource::File.new('blahblah') + resource.mode("0750") + @current_resource.mode("0444") + fac = Chef::FileAccessControl.new(@current_resource, resource, @provider) + fac.describe_changes.should == ["change mode from '0444' to '0750'"] + end + + it "sets the file's mode as specified in the resource when the current modes are incorrect" do + # stat returns modes like 0100644 (octal) => 33188 (decimal) + #@fac.stub!(:stat).and_return(OpenStruct.new(:mode => 33188)) + @current_resource.mode("0644") + File.should_receive(:chmod).with(256, '/tmp/different_file.txt') + @fac.set_mode + @fac.should be_modified + end + + it "does not want to update the mode when the current mode is correct" do + @current_resource.mode("0400") + @fac.should_update_mode?.should be_false + end + + it "does not set the file's mode when the current modes are correct" do + #@fac.stub!(:stat).and_return(OpenStruct.new(:mode => 0100400)) + @current_resource.mode("0400") + File.should_not_receive(:chmod) + @fac.set_mode + @fac.should_not be_modified + end + + it "sets all access controls on a file" do + @fac.stub!(:stat).and_return(OpenStruct.new(:owner => 99, :group => 99, :mode => 0100444)) + @resource.mode(0400) + @resource.owner(0) + @resource.group(0) + File.should_receive(:chmod).with(0400, '/tmp/different_file.txt') + File.should_receive(:chown).with(0, nil, '/tmp/different_file.txt') + File.should_receive(:chown).with(nil, 0, '/tmp/different_file.txt') + @fac.set_all + @fac.should be_modified + end + end +end diff --git a/spec/unit/file_cache_spec.rb b/spec/unit/file_cache_spec.rb new file mode 100644 index 0000000000..6596326565 --- /dev/null +++ b/spec/unit/file_cache_spec.rb @@ -0,0 +1,114 @@ +# +# 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::FileCache do + before do + @file_cache_path = Dir.mktmpdir + Chef::Config[:file_cache_path] = @file_cache_path + @io = StringIO.new + end + + after do + FileUtils.rm_rf(Chef::Config[:file_cache_path]) + end + + describe "when the relative path to the cache file doesn't exist" do + it "creates intermediate directories as needed" do + Chef::FileCache.store("whiz/bang", "I found a poop") + File.should exist(File.join(@file_cache_path, 'whiz')) + end + + it "creates the cached file at the correct relative path" do + File.should_receive(:open).with(File.join(@file_cache_path, 'whiz', 'bang'), "w",416).and_yield(@io) + Chef::FileCache.store("whiz/bang", "borkborkbork") + end + + end + + describe "when storing a file" do + before do + File.stub!(:open).and_yield(@io) + end + + it "should print the contents to the file" do + Chef::FileCache.store("whiz/bang", "borkborkbork") + @io.string.should == "borkborkbork" + end + + end + + describe "when loading cached files" do + it "finds and reads the cached file" do + FileUtils.mkdir_p(File.join(@file_cache_path, 'whiz')) + File.open(File.join(@file_cache_path, 'whiz', 'bang'), 'w') { |f| f.print("borkborkbork") } + Chef::FileCache.load('whiz/bang').should == 'borkborkbork' + end + + it "should raise a Chef::Exceptions::FileNotFound if the file doesn't exist" do + lambda { Chef::FileCache.load('whiz/bang') }.should raise_error(Chef::Exceptions::FileNotFound) + end + end + + describe "when deleting cached files" do + before(:each) do + FileUtils.mkdir_p(File.join(@file_cache_path, 'whiz')) + File.open(File.join(@file_cache_path, 'whiz', 'bang'), 'w') { |f| f.print("borkborkbork") } + end + + it "unlinks the file" do + Chef::FileCache.delete("whiz/bang") + File.should_not exist(File.join(@file_cache_path, 'whiz', 'bang')) + end + + end + + describe "when listing files in the cache" do + before(:each) do + FileUtils.mkdir_p(File.join(@file_cache_path, 'whiz')) + FileUtils.touch(File.join(@file_cache_path, 'whiz', 'bang')) + FileUtils.mkdir_p(File.join(@file_cache_path, 'snappy')) + FileUtils.touch(File.join(@file_cache_path, 'snappy', 'patter')) + end + + it "should return the relative paths" do + Chef::FileCache.list.sort.should == %w{snappy/patter whiz/bang} + end + + it "searches for cached files by globbing" do + Chef::FileCache.find('snappy/**/*').should == %w{snappy/patter} + end + + end + + describe "when checking for the existence of a file" do + before do + FileUtils.mkdir_p(File.join(@file_cache_path, 'whiz')) + end + + it "has a key if the corresponding cache file exists" do + FileUtils.touch(File.join(@file_cache_path, 'whiz', 'bang')) + Chef::FileCache.should have_key("whiz/bang") + end + + it "doesn't have a key if the corresponding cache file doesn't exist" do + Chef::FileCache.should_not have_key("whiz/bang") + end + end +end diff --git a/spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb b/spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb new file mode 100644 index 0000000000..e1f8f28bb9 --- /dev/null +++ b/spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb @@ -0,0 +1,202 @@ +# +# Author:: Daniel DeLeo (<dan@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' + +BAD_RECIPE=<<-E +# +# Cookbook Name:: syntax-err +# Recipe:: default +# +# Copyright 2012, YOUR_COMPANY_NAME +# +# All rights reserved - Do Not Redistribute +# + + +file "/tmp/explode-me" do + mode 0655 + owner "root" + this_is_not_a_valid_method +end +E + +describe Chef::Formatters::ErrorInspectors::CompileErrorInspector do + before do + @node_name = "test-node.example.com" + @description = Chef::Formatters::ErrorDescription.new("Error Evaluating File:") + @exception = NoMethodError.new("undefined method `this_is_not_a_valid_method' for Chef::Resource::File") + + @outputter = Chef::Formatters::Outputter.new(StringIO.new, STDERR) + #@outputter = Chef::Formatters::Outputter.new(STDOUT, STDERR) + end + + describe "when scrubbing backtraces" do + it "shows backtrace lines from cookbook files" do + # Error inspector originally used file_cache_path which is incorrect on + # chef-solo. Using cookbook_path should do the right thing for client and + # solo. + Chef::Config.stub!(:cookbook_path).and_return([ "/home/someuser/dev-laptop/cookbooks" ]) + @trace = [ + "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:14:in `from_file'", + "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:11:in `from_file'", + "/home/someuser/.multiruby/gems/chef/lib/chef/client.rb:123:in `run'" + ] + @exception.set_backtrace(@trace) + @path = "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb" + @inspector = described_class.new(@path, @exception) + + @expected_filtered_trace = [ + "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:14:in `from_file'", + "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:11:in `from_file'", + ] + @inspector.filtered_bt.should == @expected_filtered_trace + end + end + + describe "when explaining an error in the compile phase" do + before do + Chef::Config.stub!(:cookbook_path).and_return([ "/var/chef/cache/cookbooks" ]) + recipe_lines = BAD_RECIPE.split("\n").map {|l| l << "\n" } + IO.should_receive(:readlines).with("/var/chef/cache/cookbooks/syntax-err/recipes/default.rb").and_return(recipe_lines) + @trace = [ + "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb:14:in `from_file'", + "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb:11:in `from_file'", + "/usr/local/lib/ruby/gems/chef/lib/chef/client.rb:123:in `run'" # should not display + ] + @exception.set_backtrace(@trace) + @path = "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb" + @inspector = described_class.new(@path, @exception) + @inspector.add_explanation(@description) + end + + it "finds the line number of the error from the stacktrace" do + @inspector.culprit_line.should == 14 + end + + it "prints a pretty message" do + @description.display(@outputter) + end + end + + describe "when explaining an error on windows" do + before do + Chef::Config.stub!(:cookbook_path).and_return([ "C:/opscode/chef/var/cache/cookbooks" ]) + recipe_lines = BAD_RECIPE.split("\n").map {|l| l << "\n" } + IO.should_receive(:readlines).at_least(1).times.with(/:\/opscode\/chef\/var\/cache\/cookbooks\/foo\/recipes\/default.rb/).and_return(recipe_lines) + @trace = [ + "C:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb:14 in `from_file'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:144:in `rescue in block in load_libraries'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:138:in `block in load_libraries'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:230:in `call'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:230:in `block (2 levels) in foreach_cookbook_load_segment'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:229:in `each'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:229:in `block in foreach_cookbook_load_segment'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:227:in `each'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:227:in `foreach_cookbook_load_segment'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:137:in `load_libraries'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:62:in `load'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/client.rb:198:in `setup_run_context'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/client.rb:418:in `do_run'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/client.rb:176:in `run'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application/client.rb:283:in `block in run_application'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application/client.rb:270:in `loop'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application/client.rb:270:in `run_application'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application.rb:70:in `run'", + "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/bin/chef-client:26:in `<top (required)>'", + "C:/opscode/chef/bin/chef-client:19:in `load'", + "C:/opscode/chef/bin/chef-client:19:in `<main>'" + ] + @exception.set_backtrace(@trace) + @path = "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb" + @inspector = described_class.new(@path, @exception) + @inspector.add_explanation(@description) + end + + + describe "and examining the stack trace for a recipe" do + it "find the culprit recipe name when the drive letter is upper case" do + @inspector.culprit_file.should == "C:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb" + end + + it "find the culprit recipe name when the drive letter is lower case" do + @trace.each { |line| line.gsub!(/^C:/, "c:") } + @exception.set_backtrace(@trace) + @inspector = described_class.new(@path, @exception) + @inspector.add_explanation(@description) + @inspector.culprit_file.should == "c:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb" + end + end + + it "finds the line number of the error from the stack trace" do + @inspector.culprit_line.should == 14 + end + + it "prints a pretty message" do + @description.display(@outputter) + end + end + + describe "when explaining an error on windows, and the backtrace lowercases the drive letter" do + before do + Chef::Config.stub!(:cookbook_path).and_return([ "C:/opscode/chef/var/cache/cookbooks" ]) + recipe_lines = BAD_RECIPE.split("\n").map {|l| l << "\n" } + IO.should_receive(:readlines).with("c:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb").and_return(recipe_lines) + @trace = [ + "c:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb:14 in `from_file'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:144:in `rescue in block in load_libraries'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:138:in `block in load_libraries'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:230:in `call'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:230:in `block (2 levels) in foreach_cookbook_load_segment'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:229:in `each'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:229:in `block in foreach_cookbook_load_segment'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:227:in `each'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:227:in `foreach_cookbook_load_segment'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:137:in `load_libraries'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:62:in `load'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/client.rb:198:in `setup_run_context'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/client.rb:418:in `do_run'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/client.rb:176:in `run'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application/client.rb:283:in `block in run_application'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application/client.rb:270:in `loop'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application/client.rb:270:in `run_application'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application.rb:70:in `run'", + "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/bin/chef-client:26:in `<top (required)>'", + "c:/opscode/chef/bin/chef-client:19:in `load'", + "c:/opscode/chef/bin/chef-client:19:in `<main>'" + ] + @exception.set_backtrace(@trace) + @path = "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb" + @inspector = described_class.new(@path, @exception) + @inspector.add_explanation(@description) + end + + it "finds the culprit recipe name from the stacktrace" do + @inspector.culprit_file.should == "c:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb" + end + + it "finds the line number of the error from the stack trace" do + @inspector.culprit_line.should == 14 + end + + it "prints a pretty message" do + @description.display(@outputter) + end + end + +end diff --git a/spec/unit/formatters/error_inspectors/cookbook_resolve_error_inspector_spec.rb b/spec/unit/formatters/error_inspectors/cookbook_resolve_error_inspector_spec.rb new file mode 100644 index 0000000000..bb694f8e5c --- /dev/null +++ b/spec/unit/formatters/error_inspectors/cookbook_resolve_error_inspector_spec.rb @@ -0,0 +1,93 @@ +#-- +# Author:: Daniel DeLeo (<dan@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' + +describe Chef::Formatters::ErrorInspectors::CookbookResolveErrorInspector do + + before do + @expanded_run_list = Chef::RunList.new("recipe[annoyances]", "recipe[apache2]", "recipe[users]", "recipe[chef::client]") + + @description = Chef::Formatters::ErrorDescription.new("Error Resolving Cookbooks for Run List:") + @outputter = Chef::Formatters::Outputter.new(StringIO.new, STDERR) + #@outputter = Chef::Formatters::Outputter.new(STDOUT, STDERR) + end + + describe "when explaining a 403 error" do + before do + + @response_body = %Q({"error": [{"message": "gtfo"}]) + @response = Net::HTTPForbidden.new("1.1", "403", "(response) forbidden") + @response.stub!(:body).and_return(@response_body) + @exception = Net::HTTPServerException.new("(exception) forbidden", @response) + + @inspector = Chef::Formatters::ErrorInspectors::CookbookResolveErrorInspector.new(@expanded_run_list, @exception) + @inspector.add_explanation(@description) + end + + it "prints a nice message" do + lambda { @description.display(@outputter) }.should_not raise_error + end + + end + + describe "when explaining a PreconditionFailed (412) error with current error message style" do + # Chef currently returns error messages with some fields as JSON strings, + # which must be re-parsed to get the actual data. + + before do + + @response_body = "{\"error\":[\"{\\\"non_existent_cookbooks\\\":[\\\"apache2\\\"],\\\"cookbooks_with_no_versions\\\":[\\\"users\\\"],\\\"message\\\":\\\"Run list contains invalid items: no such cookbook nope.\\\"}\"]}" + @response = Net::HTTPPreconditionFailed.new("1.1", "412", "(response) unauthorized") + @response.stub!(:body).and_return(@response_body) + @exception = Net::HTTPServerException.new("(exception) precondition failed", @response) + + @inspector = Chef::Formatters::ErrorInspectors::CookbookResolveErrorInspector.new(@expanded_run_list, @exception) + @inspector.add_explanation(@description) + end + + it "prints a pretty message" do + @description.display(@outputter) + end + + end + + describe "when explaining a PreconditionFailed (412) error with single encoded JSON" do + # Chef currently returns error messages with some fields as JSON strings, + # which must be re-parsed to get the actual data. + + before do + + @response_body = "{\"error\":[{\"non_existent_cookbooks\":[\"apache2\"],\"cookbooks_with_no_versions\":[\"users\"],\"message\":\"Run list contains invalid items: no such cookbook nope.\"}]}" + @response = Net::HTTPPreconditionFailed.new("1.1", "412", "(response) unauthorized") + @response.stub!(:body).and_return(@response_body) + @exception = Net::HTTPServerException.new("(exception) precondition failed", @response) + + @inspector = Chef::Formatters::ErrorInspectors::CookbookResolveErrorInspector.new(@expanded_run_list, @exception) + @inspector.add_explanation(@description) + end + + it "prints a pretty message" do + @description.display(@outputter) + end + + end +end + + + diff --git a/spec/unit/formatters/error_inspectors/cookbook_sync_error_inspector_spec.rb b/spec/unit/formatters/error_inspectors/cookbook_sync_error_inspector_spec.rb new file mode 100644 index 0000000000..6db7aaaa0d --- /dev/null +++ b/spec/unit/formatters/error_inspectors/cookbook_sync_error_inspector_spec.rb @@ -0,0 +1,43 @@ +#-- +# Author:: Daniel DeLeo (<dan@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' + +describe Chef::Formatters::ErrorInspectors::CookbookSyncErrorInspector do + before do + @description = Chef::Formatters::ErrorDescription.new("Error Expanding RunList:") + @outputter = Chef::Formatters::Outputter.new(StringIO.new, STDERR) + #@outputter = Chef::Formatters::Outputter.new(STDOUT, STDERR) + end + + describe "when explaining a 502 error" do + before do + @response_body = "sad trombone orchestra" + @response = Net::HTTPBadGateway.new("1.1", "502", "(response) bad gateway") + @response.stub!(:body).and_return(@response_body) + @exception = Net::HTTPFatalError.new("(exception) bad gateway", @response) + @inspector = described_class.new({}, @exception) + @inspector.add_explanation(@description) + end + + it "prints a nice message" do + @description.display(@outputter) + end + + end +end diff --git a/spec/unit/formatters/error_inspectors/node_load_error_inspector_spec.rb b/spec/unit/formatters/error_inspectors/node_load_error_inspector_spec.rb new file mode 100644 index 0000000000..bd3cc6b764 --- /dev/null +++ b/spec/unit/formatters/error_inspectors/node_load_error_inspector_spec.rb @@ -0,0 +1,27 @@ +# +# Author:: Daniel DeLeo (<dan@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' + +# spec_helper loads the shared examples already. +#require 'support/shared/unit/api_error_inspector_spec' + + +describe Chef::Formatters::ErrorInspectors::NodeLoadErrorInspector do + it_behaves_like "an api error inspector" +end diff --git a/spec/unit/formatters/error_inspectors/registration_error_inspector_spec.rb b/spec/unit/formatters/error_inspectors/registration_error_inspector_spec.rb new file mode 100644 index 0000000000..4fcf034d80 --- /dev/null +++ b/spec/unit/formatters/error_inspectors/registration_error_inspector_spec.rb @@ -0,0 +1,27 @@ +# +# Author:: Daniel DeLeo (<dan@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' + +# spec_helper loads the shared examples already. +#require 'support/shared/unit/api_error_inspector_spec' + + +describe Chef::Formatters::ErrorInspectors::RegistrationErrorInspector do + it_behaves_like "an api error inspector" +end diff --git a/spec/unit/formatters/error_inspectors/resource_failure_inspector_spec.rb b/spec/unit/formatters/error_inspectors/resource_failure_inspector_spec.rb new file mode 100644 index 0000000000..52c719d2fb --- /dev/null +++ b/spec/unit/formatters/error_inspectors/resource_failure_inspector_spec.rb @@ -0,0 +1,162 @@ +# +# Author:: Daniel DeLeo (<dan@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' + +describe Chef::Formatters::ErrorInspectors::ResourceFailureInspector do + include Chef::DSL::Recipe + + def run_context + node = Chef::Node.new + node.automatic_attrs[:platform] = "ubuntu" + node.automatic_attrs[:platform_version] = "10.04" + Chef::RunContext.new(node, {}, nil) + end + + def cookbook_name + "rspec-example" + end + + before do + @description = Chef::Formatters::ErrorDescription.new("Error Converging Resource:") + @stdout = StringIO.new + @outputter = Chef::Formatters::Outputter.new(@stdout, STDERR) + #@outputter = Chef::Formatters::Outputter.new(STDOUT, STDERR) + + Chef::Config.stub!(:cookbook_path).and_return([ "/var/chef/cache" ]) + end + + describe "when explaining an error converging a resource" do + before do + source_line = caller(0)[0] + @resource = package("non-existing-package") do + + only_if do + true + end + + not_if("/bin/false") + action :upgrade + end + + @trace = [ + "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb:14:in `from_file'", + "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb:11:in `from_file'", + "/usr/local/lib/ruby/gems/chef/lib/chef/client.rb:123:in `run'" # should not display + ] + @exception = Chef::Exceptions::Package.new("No such package 'non-existing-package'") + @exception.set_backtrace(@trace) + @inspector = Chef::Formatters::ErrorInspectors::ResourceFailureInspector.new(@resource, :create, @exception) + @inspector.add_explanation(@description) + end + + it "filters chef core code from the backtrace" do + @expected_filtered_trace = [ + "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb:14:in `from_file'", + "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb:11:in `from_file'", + ] + + @inspector.filtered_bt.should == @expected_filtered_trace + end + + it "prints a pretty message" do + @description.display(@outputter) + end + + describe "and the error is a template error" do + before do + @description = Chef::Formatters::ErrorDescription.new("Error Converging Resource:") + @template_class = Class.new { include Chef::Mixin::Template } + @template = @template_class.new + @context = {:chef => "cool"} + + @resource = template("/tmp/foo.txt") do + mode "0644" + end + + @error = begin + @template.render_template("foo\nbar\nbaz\n<%= this_is_not_defined %>\nquin\nqunx\ndunno", @context) {|r| r} + rescue Chef::Mixin::Template::TemplateError => e + e + end + + @inspector = Chef::Formatters::ErrorInspectors::ResourceFailureInspector.new(@resource, :create, @error) + @inspector.add_explanation(@description) + end + + it "includes contextual info from the template error in the output" do + @description.display(@outputter) + @stdout.string.should include(@error.source_listing) + end + + + end + + describe "recipe_snippet" do + before do + # fake code to run through #recipe_snippet + source_file = [ "if true", "var = non_existant", "end" ] + IO.stub!(:readlines).and_return(source_file) + end + + it "parses a Windows path" do + source_line = "C:/Users/btm/chef/chef/spec/unit/fake_file.rb:2: undefined local variable or method `non_existant' for main:Object (NameError)" + @resource.source_line = source_line + @inspector = Chef::Formatters::ErrorInspectors::ResourceFailureInspector.new(@resource, :create, @exception) + @inspector.recipe_snippet.should match(/^# In C:\/Users\/btm/) + end + + it "parses a unix path" do + source_line = "/home/btm/src/chef/chef/spec/unit/fake_file.rb:2: undefined local variable or method `non_existant' for main:Object (NameError)" + @resource.source_line = source_line + @inspector = Chef::Formatters::ErrorInspectors::ResourceFailureInspector.new(@resource, :create, @exception) + @inspector.recipe_snippet.should match(/^# In \/home\/btm/) + end + end + + describe "when examining a resource that confuses the parser" do + before do + angry_bash_recipe = File.expand_path("cookbooks/angrybash/recipes/default.rb", CHEF_SPEC_DATA) + source_line = "#{angry_bash_recipe}:1:in `<main>'" + + # source_line = caller(0)[0]; @resource = bash "go off the rails" do + # code <<-END + # for i in localhost 127.0.0.1 #{Socket.gethostname()} + # do + # echo "grant all on *.* to root@'$i' identified by 'a_password'; flush privileges;" | mysql -u root -h 127.0.0.1 + # done + # END + # end + @resource = eval(IO.read(angry_bash_recipe)) + @resource.source_line = source_line + @inspector = Chef::Formatters::ErrorInspectors::ResourceFailureInspector.new(@resource, :create, @exception) + + @exception.set_backtrace(@trace) + @inspector = Chef::Formatters::ErrorInspectors::ResourceFailureInspector.new(@resource, :create, @exception) + end + + it "does not generate an error" do + lambda { @inspector.add_explanation(@description) }.should_not raise_error(TypeError) + @description.display(@outputter) + end + end + + end + + +end diff --git a/spec/unit/formatters/error_inspectors/run_list_expansion_error_inspector_spec.rb b/spec/unit/formatters/error_inspectors/run_list_expansion_error_inspector_spec.rb new file mode 100644 index 0000000000..4b6751a120 --- /dev/null +++ b/spec/unit/formatters/error_inspectors/run_list_expansion_error_inspector_spec.rb @@ -0,0 +1,93 @@ +#-- +# Author:: Daniel DeLeo (<dan@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' + +describe Chef::Formatters::ErrorInspectors::RunListExpansionErrorInspector do + before do + @node = Chef::Node.new.tap do |n| + n.name("unit-test.example.com") + n.run_list("role[base]") + end + + @description = Chef::Formatters::ErrorDescription.new("Error Expanding RunList:") + @outputter = Chef::Formatters::Outputter.new(StringIO.new, STDERR) + #@outputter = Chef::Formatters::Outputter.new(STDOUT, STDERR) + end + + describe "when explaining a missing role error" do + + before do + @run_list_expansion = Chef::RunList::RunListExpansion.new("_default", @node.run_list) + @run_list_expansion.missing_roles_with_including_role << [ "role[missing-role]", "role[base]" ] + @run_list_expansion.missing_roles_with_including_role << [ "role[another-missing-role]", "role[base]" ] + + @exception = Chef::Exceptions::MissingRole.new(@run_list_expansion) + + + @inspector = Chef::Formatters::ErrorInspectors::RunListExpansionErrorInspector.new(@node, @exception) + @inspector.add_explanation(@description) + end + + it "prints a pretty message" do + @description.display(@outputter) + end + + end + + describe "when explaining an HTTP 403 error" do + before do + + @response_body = "forbidden" + @response = Net::HTTPForbidden.new("1.1", "403", "(response) forbidden") + @response.stub!(:body).and_return(@response_body) + @exception = Net::HTTPServerException.new("(exception) forbidden", @response) + @inspector = Chef::Formatters::ErrorInspectors::RunListExpansionErrorInspector.new(@node, @exception) + @inspector.stub!(:config).and_return(:node_name => "unit-test.example.com") + + @inspector.add_explanation(@description) + end + + it "prints a pretty message" do + @description.display(@outputter) + end + + end + + describe "when explaining an HTTP 401 error" do + before do + @response_body = "check your key and node name" + @response = Net::HTTPUnauthorized.new("1.1", "401", "(response) unauthorized") + @response.stub!(:body).and_return(@response_body) + @exception = Net::HTTPServerException.new("(exception) unauthorized", @response) + + @inspector = Chef::Formatters::ErrorInspectors::RunListExpansionErrorInspector.new(@node, @exception) + @inspector.stub!(:config).and_return(:node_name => "unit-test.example.com", + :client_key => "/etc/chef/client.pem", + :chef_server_url => "http://chef.example.com") + + @inspector.add_explanation(@description) + end + + it "prints a pretty message" do + @description.display(@outputter) + end + end + +end + diff --git a/spec/unit/handler/json_file_spec.rb b/spec/unit/handler/json_file_spec.rb new file mode 100644 index 0000000000..1f47c40937 --- /dev/null +++ b/spec/unit/handler/json_file_spec.rb @@ -0,0 +1,64 @@ +# +# Author:: Adam Jacob (<adam@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' + +describe Chef::Handler::JsonFile do + before(:each) do + @handler = Chef::Handler::JsonFile.new(:the_sun => "will rise", :path => '/tmp/foobarbazqux') + end + + it "accepts arbitrary config options" do + @handler.config[:the_sun].should == "will rise" + end + + it "creates the directory where the reports will be saved" do + FileUtils.should_receive(:mkdir_p).with('/tmp/foobarbazqux') + File.should_receive(:chmod).with(00700, '/tmp/foobarbazqux') + @handler.build_report_dir + end + + describe "when reporting success" do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_status = Chef::RunStatus.new(@node, @events) + @expected_time = Time.now + Time.stub(:now).and_return(@expected_time, @expected_time + 5) + @run_status.start_clock + @run_status.stop_clock + @run_context = Chef::RunContext.new(@node, {}, @events) + @run_status.run_context = @run_context + @run_status.exception = Exception.new("Boy howdy!") + @file_mock = StringIO.new + File.stub!(:open).and_yield(@file_mock) + end + + + it "saves run status data to a file as JSON" do + @handler.should_receive(:build_report_dir) + @handler.run_report_unsafe(@run_status) + reported_data = Chef::JSONCompat.from_json(@file_mock.string) + reported_data['exception'].should == "Exception: Boy howdy!" + reported_data['start_time'].should == @expected_time.to_s + reported_data['end_time'].should == (@expected_time + 5).to_s + reported_data['elapsed_time'].should == 5 + end + + end +end diff --git a/spec/unit/handler_spec.rb b/spec/unit/handler_spec.rb new file mode 100644 index 0000000000..9eeba478ac --- /dev/null +++ b/spec/unit/handler_spec.rb @@ -0,0 +1,216 @@ +# +# Author:: Adam Jacob (<adam@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' + +describe Chef::Handler do + before(:each) do + @handler = Chef::Handler.new + + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_status = Chef::RunStatus.new(@node, @events) + + @handler.instance_variable_set(:@run_status, @run_status) + end + + describe "when accessing the run status" do + before do + @backtrace = caller + @exception = Exception.new("epic_fail") + @exception.set_backtrace(@backtrace) + @run_status.exception = @exception + @run_context = Chef::RunContext.new(@node, {}, @events) + @all_resources = [Chef::Resource::Cat.new('lolz'), Chef::Resource::ZenMaster.new('tzu')] + @all_resources.first.updated = true + @run_context.resource_collection.all_resources.replace(@all_resources) + @run_status.run_context = @run_context + @start_time = Time.now + @end_time = @start_time + 4.2 + Time.stub!(:now).and_return(@start_time, @end_time) + @run_status.start_clock + @run_status.stop_clock + end + + it "has a shortcut for the exception" do + @handler.exception.should == @exception + end + + it "has a shortcut for the backtrace" do + @handler.backtrace.should == @backtrace + end + + it "has a shortcut for all resources" do + @handler.all_resources.should == @all_resources + end + + it "has a shortcut for just the updated resources" do + @handler.updated_resources.should == [@all_resources.first] + end + + it "has a shortcut for the start time" do + @handler.start_time.should == @start_time + end + + it "has a shortcut for the end time" do + @handler.end_time.should == @end_time + end + + it "has a shortcut for the elapsed time" do + @handler.elapsed_time.should == 4.2 + end + + it "has a shortcut for the node" do + @handler.node.should == @node + end + + it "has a shortcut for the run context" do + @handler.run_context.should == @run_context + end + + it "has a shortcut for the success? and failed? predicates" do + @handler.success?.should be_false # becuase there's an exception + @handler.failed?.should be_true + end + + it "has a shortcut to the hash representation of the run status" do + @handler.data.should == @run_status.to_hash + end + end + + describe "when running the report" do + it "does not fail if the report handler raises an exception" do + $report_ran = false + def @handler.report + $report_ran = true + raise Exception, "I died the deth" + end + lambda {@handler.run_report_safely(@run_status)}.should_not raise_error + $report_ran.should be_true + end + it "does not fail if the report handler does not raise an exception" do + $report_ran = false + def @handler.report + $report_ran = true + puts "I'm AOK here." + end + lambda {@handler.run_report_safely(@run_status)}.should_not raise_error + $report_ran.should be_true + end + end + + # Hmm, no tests for report handlers, looks like + describe "when running a report handler" do + before do + @run_context = Chef::RunContext.new(@node, {}, @events) + @all_resources = [Chef::Resource::Cat.new('foo'), Chef::Resource::ZenMaster.new('moo')] + @all_resources.first.updated = true + @run_context.resource_collection.all_resources.replace(@all_resources) + @run_status.run_context = @run_context + @start_time = Time.now + @end_time = @start_time + 4.2 + Time.stub!(:now).and_return(@start_time, @end_time) + @run_status.start_clock + @run_status.stop_clock + end + + it "has a shortcut for all resources" do + @handler.all_resources.should == @all_resources + end + + it "has a shortcut for just the updated resources" do + @handler.updated_resources.should == [@all_resources.first] + end + + it "has a shortcut for the start time" do + @handler.start_time.should == @start_time + end + + it "has a shortcut for the end time" do + @handler.end_time.should == @end_time + end + + it "has a shortcut for the elapsed time" do + @handler.elapsed_time.should == 4.2 + end + + it "has a shortcut for the node" do + @handler.node.should == @node + end + + it "has a shortcut for the run context" do + @handler.run_context.should == @run_context + end + + it "has a shortcut for the success? and failed? predicates" do + @handler.success?.should be_true + @handler.failed?.should be_false + end + + it "has a shortcut to the hash representation of the run status" do + @handler.data.should == @run_status.to_hash + end + end + + # and this would test the start handler + describe "when running a start handler" do + before do + @start_time = Time.now + Time.stub!(:now).and_return(@start_time) + @run_status.start_clock + end + + it "should not have all resources" do + @handler.all_resources.should be_false + end + + it "should not have updated resources" do + @handler.updated_resources.should be_false + end + + it "has a shortcut for the start time" do + @handler.start_time.should == @start_time + end + + it "does not have a shortcut for the end time" do + @handler.end_time.should be_false + end + + it "does not have a shortcut for the elapsed time" do + @handler.elapsed_time.should be_false + end + + it "has a shortcut for the node" do + @handler.node.should == @node + end + + it "does not have a shortcut for the run context" do + @handler.run_context.should be_false + end + + it "has a shortcut for the success? and failed? predicates" do + @handler.success?.should be_true # for some reason this is true + @handler.failed?.should be_false + end + + it "has a shortcut to the hash representation of the run status" do + @handler.data.should == @run_status.to_hash + end + end + +end diff --git a/spec/unit/json_compat_spect.rb b/spec/unit/json_compat_spect.rb new file mode 100644 index 0000000000..aa688c0791 --- /dev/null +++ b/spec/unit/json_compat_spect.rb @@ -0,0 +1,53 @@ +# +# Author:: Juanje Ojeda (<juanje.ojeda@gmail.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 File.expand_path('../../spec_helper', __FILE__) +require 'chef/json_compat' + +describe Chef::JSONCompat do + describe "with a file with 1000 or less nested entries" do + before(:all) do + @json = IO.read(File.join(CHEF_SPEC_DATA, 'big_json.json')) + @hash = Chef::JSONCompat.from_json(@json) + end + + describe "when a big json file is loaded" do + it "should create a Hash from the file" do + @hash.should be_kind_of(Hash) + end + it "should has 'test' as a 1000th nested value" do + @hash['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key'].should == 'test' + end + end + end + describe "with a file with more than 1000 nested entries" do + before(:all) do + @json = IO.read(File.join(CHEF_SPEC_DATA, 'big_json_plus_one.json')) + @hash = Chef::JSONCompat.from_json(@json, {:max_nesting => 1001}) + end + + describe "when a big json file is loaded" do + it "should create a Hash from the file" do + @hash.should be_kind_of(Hash) + end + it "should has 'test' as a 1001th nested value" do + @hash['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key'].should == 'test' + end + end + end +end diff --git a/spec/unit/knife/bootstrap_spec.rb b/spec/unit/knife/bootstrap_spec.rb new file mode 100644 index 0000000000..b2247bb5a4 --- /dev/null +++ b/spec/unit/knife/bootstrap_spec.rb @@ -0,0 +1,214 @@ +# +# Author:: Ian Meyer (<ianmmeyer@gmail.com>) +# Copyright:: Copyright (c) 2010 Ian Meyer +# 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' + +Chef::Knife::Bootstrap.load_deps +require 'net/ssh' + +describe Chef::Knife::Bootstrap do + before(:each) do + Chef::Log.logger = Logger.new(StringIO.new) + @knife = Chef::Knife::Bootstrap.new + @knife.config[:template_file] = File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "test.erb")) + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + @stderr = StringIO.new + @knife.ui.stub!(:stderr).and_return(@stderr) + end + + it "should return a name of default bootstrap template" do + @knife.find_template.should be_a_kind_of(String) + end + + it "should error if template can not be found" do + @knife.config[:template_file] = false + @knife.config[:distro] = 'penultimate' + lambda { @knife.find_template }.should raise_error + end + + it "should look for templates early in the run" do + File.stub(:exists?).and_return(true) + @knife.name_args = ['shatner'] + @knife.stub!(:read_template).and_return("") + @knife.stub!(:knife_ssh).and_return(true) + @knife_ssh = @knife.knife_ssh + @knife.should_receive(:find_template).ordered + @knife.should_receive(:knife_ssh).ordered + @knife_ssh.should_receive(:run) # rspec appears to keep order per object + @knife.run + end + + it "should load the specified template" do + @knife.config[:distro] = 'fedora13-gems' + lambda { @knife.find_template }.should_not raise_error + end + + it "should load the specified template from a Ruby gem" do + @knife.config[:template_file] = false + Gem.stub(:find_files).and_return(["/Users/schisamo/.rvm/gems/ruby-1.9.2-p180@chef-0.10/gems/knife-windows-0.5.4/lib/chef/knife/bootstrap/fake-bootstrap-template.erb"]) + File.stub(:exists?).and_return(true) + IO.stub(:read).and_return('random content') + @knife.config[:distro] = 'fake-bootstrap-template' + lambda { @knife.find_template }.should_not raise_error + end + + it "should return an empty run_list" do + @knife.instance_variable_set("@template_file", @knife.config[:template_file]) + template_string = @knife.read_template + @knife.render_template(template_string).should == '{"run_list":[]}' + end + + it "should have role[base] in the run_list" do + @knife.instance_variable_set("@template_file", @knife.config[:template_file]) + template_string = @knife.read_template + @knife.parse_options(["-r","role[base]"]) + @knife.render_template(template_string).should == '{"run_list":["role[base]"]}' + end + + it "should have role[base] and recipe[cupcakes] in the run_list" do + @knife.instance_variable_set("@template_file", @knife.config[:template_file]) + template_string = @knife.read_template + @knife.parse_options(["-r", "role[base],recipe[cupcakes]"]) + @knife.render_template(template_string).should == '{"run_list":["role[base]","recipe[cupcakes]"]}' + end + + it "should have foo => {bar => baz} in the first_boot" do + @knife.instance_variable_set("@template_file", @knife.config[:template_file]) + template_string = @knife.read_template + @knife.parse_options(["-j", '{"foo":{"bar":"baz"}}']) + expected_hash = Yajl::Parser.new.parse('{"foo":{"bar":"baz"},"run_list":[]}') + actual_hash = Yajl::Parser.new.parse(@knife.render_template(template_string)) + actual_hash.should == expected_hash + end + + it "should create a hint file when told to" do + @knife.config[:template_file] = File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "test-hints.erb")) + @knife.instance_variable_set("@template_file", @knife.config[:template_file]) + template_string = @knife.read_template + @knife.parse_options(["--hint", "openstack"]) + @knife.render_template(template_string).should match /\/etc\/chef\/ohai\/hints\/openstack.json/ + end + + it "should populate a hint file with JSON when given a file to read" do + @knife.stub(:find_template).and_return(true) + @knife.config[:template_file] = File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "test-hints.erb")) + ::File.stub!(:read).and_return('{ "foo" : "bar" }') + @knife.instance_variable_set("@template_file", @knife.config[:template_file]) + template_string = @knife.read_template + @knife.stub!(:read_template).and_return('{ "foo" : "bar" }') + @knife.parse_options(["--hint", "openstack=hints/openstack.json"]) + @knife.render_template(template_string).should match /\{\"foo\":\"bar\"\}/ + end + + + it "should take the node name from ARGV" do + @knife.name_args = ['barf'] + @knife.name_args.first.should == "barf" + end + + describe "when configuring the underlying knife ssh command" do + before do + @knife.name_args = ["foo.example.com"] + @knife.config[:ssh_user] = "rooty" + @knife.config[:ssh_password] = "open_sesame" + Chef::Config[:knife][:ssh_port] = "4001" + @knife.config[:identity_file] = "~/.ssh/me.rsa" + @knife.stub!(:read_template).and_return("") + @knife_ssh = @knife.knife_ssh + end + + it "configures the hostname" do + @knife_ssh.name_args.first.should == "foo.example.com" + end + + it "configures the ssh user" do + @knife_ssh.config[:ssh_user].should == 'rooty' + end + + it "configures the ssh password" do + @knife_ssh.config[:ssh_password].should == 'open_sesame' + end + + it "configures the ssh port" do + @knife_ssh.config[:ssh_port].should == '4001' + end + + it "configures the ssh identity file" do + @knife_ssh.config[:identity_file].should == '~/.ssh/me.rsa' + end + end + + describe "when falling back to password auth when host key auth fails" do + before do + @knife.name_args = ["foo.example.com"] + @knife.config[:ssh_user] = "rooty" + @knife.config[:identity_file] = "~/.ssh/me.rsa" + @knife.stub!(:read_template).and_return("") + @knife_ssh = @knife.knife_ssh + end + + it "prompts the user for a password " do + @knife.stub!(:knife_ssh).and_return(@knife_ssh) + @knife_ssh.stub!(:get_password).and_return('typed_in_password') + alternate_knife_ssh = @knife.knife_ssh_with_password_auth + alternate_knife_ssh.config[:ssh_password].should == 'typed_in_password' + end + + it "configures knife not to use the identity file that didn't work previously" do + @knife.stub!(:knife_ssh).and_return(@knife_ssh) + @knife_ssh.stub!(:get_password).and_return('typed_in_password') + alternate_knife_ssh = @knife.knife_ssh_with_password_auth + alternate_knife_ssh.config[:identity_file].should be_nil + end + end + + describe "when running the bootstrap" do + before do + @knife.name_args = ["foo.example.com"] + @knife.config[:ssh_user] = "rooty" + @knife.config[:identity_file] = "~/.ssh/me.rsa" + @knife.stub!(:read_template).and_return("") + @knife_ssh = @knife.knife_ssh + @knife.stub!(:knife_ssh).and_return(@knife_ssh) + end + + it "verifies that a server to bootstrap was given as a command line arg" do + @knife.name_args = nil + lambda { @knife.run }.should raise_error(SystemExit) + @stderr.string.should match /ERROR:.+FQDN or ip/ + end + + it "configures the underlying ssh command and then runs it" do + @knife_ssh.should_receive(:run) + @knife.run + end + + it "falls back to password based auth when auth fails the first time" do + @knife.stub!(:puts) + + @fallback_knife_ssh = @knife_ssh.dup + @knife_ssh.should_receive(:run).and_raise(Net::SSH::AuthenticationFailed.new("no ssh for you")) + @knife.stub!(:knife_ssh_with_password_auth).and_return(@fallback_knife_ssh) + @fallback_knife_ssh.should_receive(:run) + @knife.run + end + + end + +end diff --git a/spec/unit/knife/client_bulk_delete_spec.rb b/spec/unit/knife/client_bulk_delete_spec.rb new file mode 100644 index 0000000000..55351554d1 --- /dev/null +++ b/spec/unit/knife/client_bulk_delete_spec.rb @@ -0,0 +1,78 @@ +# +# Author:: Stephen Delano (<stephen@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' + +describe Chef::Knife::ClientBulkDelete do + before(:each) do + Chef::Log.logger = Logger.new(StringIO.new) + + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::ClientBulkDelete.new + @knife.name_args = ["."] + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + @knife.ui.stub!(:confirm).and_return(true) + @clients = Hash.new + %w{tim dan stephen}.each do |client_name| + client = Chef::ApiClient.new() + client.name(client_name) + client.stub!(:destroy).and_return(true) + @clients[client_name] = client + end + Chef::ApiClient.stub!(:list).and_return(@clients) + end + + describe "run" do + + it "should get the list of the clients" do + Chef::ApiClient.should_receive(:list).and_return(@clients) + @knife.run + end + + it "should print the clients you are about to delete" do + @knife.run + @stdout.string.should match(/#{@knife.ui.list(@clients.keys.sort, :columns_down)}/) + end + + it "should confirm you really want to delete them" do + @knife.ui.should_receive(:confirm) + @knife.run + end + + it "should delete each client" do + @clients.each_value do |c| + c.should_receive(:destroy) + end + @knife.run + end + + it "should only delete clients that match the regex" do + @knife.name_args = ["tim"] + @clients["tim"].should_receive(:destroy) + @clients["stephen"].should_not_receive(:destroy) + @clients["dan"].should_not_receive(:destroy) + @knife.run + end + + it "should exit if the regex is not provided" do + @knife.name_args = [] + lambda { @knife.run }.should raise_error(SystemExit) + end + end +end diff --git a/spec/unit/knife/client_create_spec.rb b/spec/unit/knife/client_create_spec.rb new file mode 100644 index 0000000000..c049748074 --- /dev/null +++ b/spec/unit/knife/client_create_spec.rb @@ -0,0 +1,74 @@ +# +# 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' + +Chef::Knife::ClientCreate.load_deps + +describe Chef::Knife::ClientCreate do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::ClientCreate.new + @knife.config = { + :file => nil + } + @knife.name_args = [ "adam" ] + @client = Chef::ApiClient.new + @client.stub!(:save).and_return({ 'private_key' => '' }) + @knife.stub!(:edit_data).and_return(@client) + @knife.stub!(:puts) + Chef::ApiClient.stub!(:new).and_return(@client) + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + describe "run" do + it "should create a new Client" do + Chef::ApiClient.should_receive(:new).and_return(@client) + @knife.run + @stdout.string.should match /created client.+adam/i + end + + it "should set the Client name" do + @client.should_receive(:name).with("adam") + @knife.run + end + + it "should allow you to edit the data" do + @knife.should_receive(:edit_data).with(@client) + @knife.run + end + + it "should save the Client" do + @client.should_receive(:save) + @knife.run + end + + describe "with -f or --file" do + it "should write the private key to a file" do + @knife.config[:file] = "/tmp/monkeypants" + @client.stub!(:save).and_return({ 'private_key' => "woot" }) + filehandle = mock("Filehandle") + filehandle.should_receive(:print).with('woot') + File.should_receive(:open).with("/tmp/monkeypants", "w").and_yield(filehandle) + @knife.run + end + end + + end +end diff --git a/spec/unit/knife/client_delete_spec.rb b/spec/unit/knife/client_delete_spec.rb new file mode 100644 index 0000000000..865f19f713 --- /dev/null +++ b/spec/unit/knife/client_delete_spec.rb @@ -0,0 +1,40 @@ +# +# Author:: Thomas Bishop (<bishop.thomas@gmail.com>) +# Copyright:: Copyright (c) 2011 Thomas Bishop +# 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::Knife::ClientDelete do + before(:each) do + @knife = Chef::Knife::ClientDelete.new + @knife.name_args = [ 'adam' ] + end + + describe 'run' do + it 'should delete the client' do + @knife.should_receive(:delete_object).with(Chef::ApiClient, 'adam') + @knife.run + end + + it 'should print usage and exit when a client name is not provided' do + @knife.name_args = [] + @knife.should_receive(:show_usage) + @knife.ui.should_receive(:fatal) + lambda { @knife.run }.should raise_error(SystemExit) + end + end +end diff --git a/spec/unit/knife/client_edit_spec.rb b/spec/unit/knife/client_edit_spec.rb new file mode 100644 index 0000000000..1308d14fd5 --- /dev/null +++ b/spec/unit/knife/client_edit_spec.rb @@ -0,0 +1,40 @@ +# +# Author:: Thomas Bishop (<bishop.thomas@gmail.com>) +# Copyright:: Copyright (c) 2011 Thomas Bishop +# 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::Knife::ClientEdit do + before(:each) do + @knife = Chef::Knife::ClientEdit.new + @knife.name_args = [ 'adam' ] + end + + describe 'run' do + it 'should edit the client' do + @knife.should_receive(:edit_object).with(Chef::ApiClient, 'adam') + @knife.run + end + + it 'should print usage and exit when a client name is not provided' do + @knife.name_args = [] + @knife.should_receive(:show_usage) + @knife.ui.should_receive(:fatal) + lambda { @knife.run }.should raise_error(SystemExit) + end + end +end diff --git a/spec/unit/knife/client_list_spec.rb b/spec/unit/knife/client_list_spec.rb new file mode 100644 index 0000000000..6237a0d14c --- /dev/null +++ b/spec/unit/knife/client_list_spec.rb @@ -0,0 +1,34 @@ +# +# Author:: Thomas Bishop (<bishop.thomas@gmail.com>) +# Copyright:: Copyright (c) 2011 Thomas Bishop +# 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::Knife::ClientList do + before(:each) do + @knife = Chef::Knife::ClientList.new + @knife.name_args = [ 'adam' ] + end + + describe 'run' do + it 'should list the clients' do + Chef::ApiClient.should_receive(:list) + @knife.should_receive(:format_list_for_display) + @knife.run + end + end +end diff --git a/spec/unit/knife/client_reregister_spec.rb b/spec/unit/knife/client_reregister_spec.rb new file mode 100644 index 0000000000..0d284c0f58 --- /dev/null +++ b/spec/unit/knife/client_reregister_spec.rb @@ -0,0 +1,61 @@ +# +# Author:: Thomas Bishop (<bishop.thomas@gmail.com>) +# Copyright:: Copyright (c) 2011 Thomas Bishop +# 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::Knife::ClientReregister do + before(:each) do + @knife = Chef::Knife::ClientReregister.new + @knife.name_args = [ 'adam' ] + @client_mock = mock('client_mock') + @client_mock.stub!(:save).and_return({ 'private_key' => 'foo_key' }) + Chef::ApiClient.stub!(:load).and_return(@client_mock) + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + describe 'run' do + it 'should load and save the client' do + Chef::ApiClient.should_receive(:load).with('adam').and_return(@client_mock) + @client_mock.should_receive(:save).with(true).and_return({'private_key' => 'foo_key'}) + @knife.run + end + + it 'should output the private key' do + @knife.run + @stdout.string.should match /foo_key/ + end + + it 'should print usage and exit when a client name is not provided' do + @knife.name_args = [] + @knife.should_receive(:show_usage) + @knife.ui.should_receive(:fatal) + lambda { @knife.run }.should raise_error(SystemExit) + end + + describe 'with -f or --file' do + it 'should write the private key to a file' do + @knife.config[:file] = '/tmp/monkeypants' + filehandle = mock('Filehandle') + filehandle.should_receive(:print).with('foo_key') + File.should_receive(:open).with('/tmp/monkeypants', 'w').and_yield(filehandle) + @knife.run + end + end + end +end diff --git a/spec/unit/knife/client_show_spec.rb b/spec/unit/knife/client_show_spec.rb new file mode 100644 index 0000000000..5bac3b4af6 --- /dev/null +++ b/spec/unit/knife/client_show_spec.rb @@ -0,0 +1,42 @@ +# +# Author:: Thomas Bishop (<bishop.thomas@gmail.com>) +# Copyright:: Copyright (c) 2011 Thomas Bishop +# 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::Knife::ClientShow do + before(:each) do + @knife = Chef::Knife::ClientShow.new + @knife.name_args = [ 'adam' ] + @client_mock = mock('client_mock') + end + + describe 'run' do + it 'should list the client' do + Chef::ApiClient.should_receive(:load).with('adam').and_return(@client_mock) + @knife.should_receive(:format_for_display).with(@client_mock) + @knife.run + end + + it 'should print usage and exit when a client name is not provided' do + @knife.name_args = [] + @knife.should_receive(:show_usage) + @knife.ui.should_receive(:fatal) + lambda { @knife.run }.should raise_error(SystemExit) + end + end +end diff --git a/spec/unit/knife/config_file_selection_spec.rb b/spec/unit/knife/config_file_selection_spec.rb new file mode 100644 index 0000000000..3ee18d82d0 --- /dev/null +++ b/spec/unit/knife/config_file_selection_spec.rb @@ -0,0 +1,118 @@ +# +# Author:: Nicolas Vinot (<aeris@imirhil.fr>) +# 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 File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') +require 'tmpdir' + +describe Chef::Knife do + before :each do + Chef::Config.stub!(:from_file).and_return(true) + end + + it "configure knife from KNIFE_HOME env variable" do + env_config = File.expand_path(File.join(Dir.tmpdir, 'knife.rb')) + File.stub!(:exist?).and_return(false) + File.stub!(:exist?).with(env_config).and_return(true) + + ENV['KNIFE_HOME'] = Dir.tmpdir + @knife = Chef::Knife.new + @knife.configure_chef + @knife.config[:config_file].should == env_config + end + + it "configure knife from PWD" do + pwd_config = "#{Dir.pwd}/knife.rb" + File.stub!(:exist?).and_return do | arg | + [ pwd_config ].include? arg + end + + @knife = Chef::Knife.new + @knife.configure_chef + @knife.config[:config_file].should == pwd_config + end + + it "configure knife from UPWARD" do + upward_dir = File.expand_path "#{Dir.pwd}/.chef" + upward_config = File.expand_path "#{upward_dir}/knife.rb" + File.stub!(:exist?).and_return do | arg | + [ upward_config ].include? arg + end + Chef::Knife.stub!(:chef_config_dir).and_return(upward_dir) + + @knife = Chef::Knife.new + @knife.configure_chef + @knife.config[:config_file].should == upward_config + end + + it "configure knife from HOME" do + home_config = File.expand_path(File.join("#{ENV['HOME']}", "/.chef/knife.rb")) + File.stub!(:exist?).and_return do | arg | + [ home_config ].include? arg + end + + @knife = Chef::Knife.new + @knife.configure_chef + @knife.config[:config_file].should == home_config + end + + it "configure knife from nothing" do + ::File.stub!(:exist?).and_return(false) + @knife = Chef::Knife.new + @knife.ui.should_receive(:warn).with("No knife configuration file found") + @knife.configure_chef + @knife.config[:config_file].should be_nil + end + + it "configure knife precedence" do + env_config = File.join(Dir.tmpdir, 'knife.rb') + pwd_config = "#{Dir.pwd}/knife.rb" + upward_dir = File.expand_path "#{Dir.pwd}/.chef" + upward_config = File.expand_path "#{upward_dir}/knife.rb" + home_config = File.expand_path(File.join("#{ENV['HOME']}", "/.chef/knife.rb")) + configs = [ env_config, pwd_config, upward_config, home_config ] + File.stub!(:exist?).and_return do | arg | + configs.include? arg + end + Chef::Knife.stub!(:chef_config_dir).and_return(upward_dir) + ENV['KNIFE_HOME'] = Dir.tmpdir + + @knife = Chef::Knife.new + @knife.configure_chef + @knife.config[:config_file].should == env_config + + configs.delete env_config + @knife.config.delete :config_file + @knife.configure_chef + @knife.config[:config_file].should == pwd_config + + configs.delete pwd_config + @knife.config.delete :config_file + @knife.configure_chef + @knife.config[:config_file].should == upward_config + + configs.delete upward_config + @knife.config.delete :config_file + @knife.configure_chef + @knife.config[:config_file].should == home_config + + configs.delete home_config + @knife.config.delete :config_file + @knife.configure_chef + @knife.config[:config_file].should be_nil + end +end diff --git a/spec/unit/knife/configure_client_spec.rb b/spec/unit/knife/configure_client_spec.rb new file mode 100644 index 0000000000..ba832103bc --- /dev/null +++ b/spec/unit/knife/configure_client_spec.rb @@ -0,0 +1,83 @@ +# +# Author:: Thomas Bishop (<bishop.thomas@gmail.com>) +# Copyright:: Copyright (c) 2011 Thomas Bishop +# 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::Knife::ConfigureClient do + before do + @knife = Chef::Knife::ConfigureClient.new + Chef::Config[:chef_server_url] = 'https://chef.example.com' + Chef::Config[:validation_client_name] = 'chef-validator' + Chef::Config[:validation_key] = '/etc/chef/validation.pem' + + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + describe 'run' do + it 'should print usage and exit when a directory is not provided' do + @knife.should_receive(:show_usage) + @knife.ui.should_receive(:fatal).with(/must provide the directory/) + lambda { + @knife.run + }.should raise_error SystemExit + end + + describe 'when specifing a directory' do + before do + @knife.name_args = ['/home/bob/.chef'] + @client_file = StringIO.new + @validation_file = StringIO.new + File.should_receive(:open).with('/home/bob/.chef/client.rb', 'w'). + and_yield(@client_file) + File.should_receive(:open).with('/home/bob/.chef/validation.pem', 'w'). + and_yield(@validation_file) + IO.should_receive(:read).and_return('foo_bar_baz') + end + + it 'should recursively create the directory' do + FileUtils.should_receive(:mkdir_p).with('/home/bob/.chef') + @knife.run + end + + it 'should write out the config file' do + FileUtils.stub!(:mkdir_p) + @knife.run + @client_file.string.should match /log_level\s+\:info/ + @client_file.string.should match /log_location\s+STDOUT/ + @client_file.string.should match /chef_server_url\s+'https\:\/\/chef\.example\.com'/ + @client_file.string.should match /validation_client_name\s+'chef-validator'/ + end + + it 'should write out the validation.pem file' do + FileUtils.stub!(:mkdir_p) + @knife.run + @validation_file.string.should match /foo_bar_baz/ + end + + it 'should print information on what is being configured' do + FileUtils.stub!(:mkdir_p) + @knife.run + @stdout.string.should match /creating client configuration/i + @stdout.string.should match /writing client\.rb/i + @stdout.string.should match /writing validation\.pem/i + end + end + end + +end diff --git a/spec/unit/knife/configure_spec.rb b/spec/unit/knife/configure_spec.rb new file mode 100644 index 0000000000..85ee996dd5 --- /dev/null +++ b/spec/unit/knife/configure_spec.rb @@ -0,0 +1,229 @@ +require 'spec_helper' + +describe Chef::Knife::Configure do + before do + @original_config = Chef::Config.configuration.dup + + Chef::Log.logger = Logger.new(StringIO.new) + + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::Configure.new + @rest_client = mock("null rest client", :post_rest => { :result => :true }) + @knife.stub!(:rest).and_return(@rest_client) + + @out = StringIO.new + @knife.ui.stub!(:stdout).and_return(@out) + @knife.config[:config_file] = '/home/you/.chef/knife.rb' + + @in = StringIO.new("\n" * 7) + @knife.ui.stub!(:stdin).and_return(@in) + + @err = StringIO.new + @knife.ui.stub!(:stderr).and_return(@err) + + @ohai = Ohai::System.new + @ohai.stub(:require_plugin) + @ohai[:fqdn] = "foo.example.org" + Ohai::System.stub!(:new).and_return(@ohai) + end + + after do + Chef::Config.configuration.replace(@original_config) + end + + it "asks the user for the URL of the chef server" do + @knife.ask_user_for_config + @out.string.should match(Regexp.escape('Please enter the chef server URL: [http://foo.example.org:4000]')) + @knife.chef_server.should == 'http://foo.example.org:4000' + end + + it "asks the user for the clientname they want for the new client if -i is specified" do + @knife.config[:initial] = true + Etc.stub!(:getlogin).and_return("a-new-user") + @knife.ask_user_for_config + @out.string.should match(Regexp.escape("Please enter a clientname for the new client: [a-new-user]")) + @knife.new_client_name.should == Etc.getlogin + end + + it "should not ask the user for the clientname they want for the new client if -i and --node_name are specified" do + @knife.config[:initial] = true + @knife.config[:node_name] = 'testnode' + Etc.stub!(:getlogin).and_return("a-new-user") + @knife.ask_user_for_config + @out.string.should_not match(Regexp.escape("Please enter a clientname for the new client")) + @knife.new_client_name.should == 'testnode' + end + + it "asks the user for the existing API username or clientname if -i is not specified" do + Etc.stub!(:getlogin).and_return("a-new-user") + @knife.ask_user_for_config + @out.string.should match(Regexp.escape("Please enter an existing username or clientname for the API: [a-new-user]")) + @knife.new_client_name.should == Etc.getlogin + end + + it "asks the user for the existing admin client's name if -i is specified" do + @knife.config[:initial] = true + @knife.ask_user_for_config + @out.string.should match(Regexp.escape("Please enter the existing admin clientname: [chef-webui]")) + @knife.admin_client_name.should == 'chef-webui' + end + + it "should not ask the user for the existing admin client's name if -i and --admin-client_name are specified" do + @knife.config[:initial] = true + @knife.config[:admin_client_name] = 'my-webui' + @knife.ask_user_for_config + @out.string.should_not match(Regexp.escape("Please enter the existing admin clientname:")) + @knife.admin_client_name.should == 'my-webui' + end + + it "should not ask the user for the existing admin client's name if -i is not specified" do + @knife.ask_user_for_config + @out.string.should_not match(Regexp.escape("Please enter the existing admin clientname: [chef-webui]")) + @knife.admin_client_name.should_not == 'chef-webui' + end + + it "asks the user for the location of the existing admin key if -i is specified" do + @knife.config[:initial] = true + @knife.ask_user_for_config + @out.string.should match(Regexp.escape("Please enter the location of the existing admin client's private key: [/etc/chef/webui.pem]")) + if windows? + @knife.admin_client_key.should == 'C:/etc/chef/webui.pem' + else + @knife.admin_client_key.should == '/etc/chef/webui.pem' + end + end + + it "should not ask the user for the location of the existing admin key if -i and --admin_client_key are specified" do + @knife.config[:initial] = true + @knife.config[:admin_client_key] = '/home/you/.chef/my-webui.pem' + @knife.ask_user_for_config + @out.string.should_not match(Regexp.escape("Please enter the location of the existing admin client's private key:")) + if windows? + @knife.admin_client_key.should == 'C:/home/you/.chef/my-webui.pem' + else + @knife.admin_client_key.should == '/home/you/.chef/my-webui.pem' + end + end + + it "should not ask the user for the location of the existing admin key if -i is not specified" do + @knife.ask_user_for_config + @out.string.should_not match(Regexp.escape("Please enter the location of the existing admin client's private key: [/etc/chef/webui.pem]")) + if windows? + @knife.admin_client_key.should_not == 'C:/etc//chef/webui.pem' + else + @knife.admin_client_key.should_not == '/etc/chef/webui.pem' + end + end + + it "asks the user for the location of a chef repo" do + @knife.ask_user_for_config + @out.string.should match(Regexp.escape("Please enter the path to a chef repository (or leave blank):")) + @knife.chef_repo.should == '' + end + + it "asks the users for the name of the validation client" do + @knife.ask_user_for_config + @out.string.should match(Regexp.escape("Please enter the validation clientname: [chef-validator]")) + @knife.validation_client_name.should == 'chef-validator' + end + + it "should not ask the users for the name of the validation client if --validation_client_name is specified" do + @knife.config[:validation_client_name] = 'my-validator' + @knife.ask_user_for_config + @out.string.should_not match(Regexp.escape("Please enter the validation clientname:")) + @knife.validation_client_name.should == 'my-validator' + end + + it "asks the users for the location of the validation key" do + @knife.ask_user_for_config + @out.string.should match(Regexp.escape("Please enter the location of the validation key: [/etc/chef/validation.pem]")) + if windows? + @knife.validation_key.should == 'C:/etc/chef/validation.pem' + else + @knife.validation_key.should == '/etc/chef/validation.pem' + end + end + + it "should not ask the users for the location of the validation key if --validation_key is specified" do + @knife.config[:validation_key] = '/home/you/.chef/my-validation.pem' + @knife.ask_user_for_config + @out.string.should_not match(Regexp.escape("Please enter the location of the validation key:")) + if windows? + @knife.validation_key.should == 'C:/home/you/.chef/my-validation.pem' + else + @knife.validation_key.should == '/home/you/.chef/my-validation.pem' + end + end + + it "should not ask the user for anything if -i and all other properties are specified" do + @knife.config[:initial] = true + @knife.config[:chef_server_url] = 'http://localhost:5000' + @knife.config[:node_name] = 'testnode' + @knife.config[:admin_client_name] = 'my-webui' + @knife.config[:admin_client_key] = '/home/you/.chef/my-webui.pem' + @knife.config[:validation_client_name] = 'my-validator' + @knife.config[:validation_key] = '/home/you/.chef/my-validation.pem' + @knife.config[:repository] = '' + @knife.config[:client_key] = '/home/you/a-new-user.pem' + Etc.stub!(:getlogin).and_return('a-new-user') + + @knife.ask_user_for_config + @out.string.should match(/\s*/) + + @knife.new_client_name.should == 'testnode' + @knife.chef_server.should == 'http://localhost:5000' + @knife.admin_client_name.should == 'my-webui' + if windows? + @knife.admin_client_key.should == 'C:/home/you/.chef/my-webui.pem' + @knife.validation_key.should == 'C:/home/you/.chef/my-validation.pem' + @knife.new_client_key.should == 'C:/home/you/a-new-user.pem' + else + @knife.admin_client_key.should == '/home/you/.chef/my-webui.pem' + @knife.validation_key.should == '/home/you/.chef/my-validation.pem' + @knife.new_client_key.should == '/home/you/a-new-user.pem' + end + @knife.validation_client_name.should == 'my-validator' + @knife.chef_repo.should == '' + end + + it "writes the new data to a config file" do + File.stub!(:expand_path).with("/home/you/.chef/knife.rb").and_return("/home/you/.chef/knife.rb") + File.stub!(:expand_path).with("/home/you/.chef/#{Etc.getlogin}.pem").and_return("/home/you/.chef/#{Etc.getlogin}.pem") + File.stub!(:expand_path).with("/etc/chef/validation.pem").and_return("/etc/chef/validation.pem") + File.stub!(:expand_path).with("/etc/chef/webui.pem").and_return("/etc/chef/webui.pem") + FileUtils.should_receive(:mkdir_p).with("/home/you/.chef") + config_file = StringIO.new + ::File.should_receive(:open).with("/home/you/.chef/knife.rb", "w").and_yield config_file + @knife.config[:repository] = '/home/you/chef-repo' + @knife.run + config_file.string.should match(/^node_name[\s]+'#{Etc.getlogin}'$/) + config_file.string.should match(%r{^client_key[\s]+'/home/you/.chef/#{Etc.getlogin}.pem'$}) + config_file.string.should match(/^validation_client_name\s+'chef-validator'$/) + config_file.string.should match(%r{^validation_key\s+'/etc/chef/validation.pem'$}) + config_file.string.should match(%r{^chef_server_url\s+'http://foo.example.org:4000'$}) + config_file.string.should match(%r{cookbook_path\s+\[ '/home/you/chef-repo/cookbooks' \]}) + end + + it "creates a new client when given the --initial option" do + File.stub!(:expand_path).with("/home/you/.chef/knife.rb").and_return("/home/you/.chef/knife.rb") + File.stub!(:expand_path).with("/home/you/.chef/a-new-user.pem").and_return("/home/you/.chef/a-new-user.pem") + File.stub!(:expand_path).with("/etc/chef/validation.pem").and_return("/etc/chef/validation.pem") + File.stub!(:expand_path).with("/etc/chef/webui.pem").and_return("/etc/chef/webui.pem") + Chef::Config[:node_name] = "webmonkey.example.com" + client_command = Chef::Knife::ClientCreate.new + client_command.should_receive(:run) + + Etc.stub!(:getlogin).and_return("a-new-user") + + Chef::Knife::ClientCreate.stub!(:new).and_return(client_command) + FileUtils.should_receive(:mkdir_p).with("/home/you/.chef") + ::File.should_receive(:open).with("/home/you/.chef/knife.rb", "w") + @knife.config[:initial] = true + @knife.run + client_command.name_args.should == Array("a-new-user") + client_command.config[:admin].should be_true + client_command.config[:file].should == "/home/you/.chef/a-new-user.pem" + client_command.config[:yes].should be_true + client_command.config[:disable_editing].should be_true + end +end diff --git a/spec/unit/knife/cookbook_bulk_delete_spec.rb b/spec/unit/knife/cookbook_bulk_delete_spec.rb new file mode 100644 index 0000000000..ced2a9a4e4 --- /dev/null +++ b/spec/unit/knife/cookbook_bulk_delete_spec.rb @@ -0,0 +1,87 @@ +# +# Author:: Stephen Delano (<stephen@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' + +describe Chef::Knife::CookbookBulkDelete do + before(:each) do + Chef::Log.logger = Logger.new(StringIO.new) + + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::CookbookBulkDelete.new + @knife.config = {:print_after => nil} + @knife.name_args = ["."] + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + @knife.ui.stub!(:confirm).and_return(true) + @cookbooks = Hash.new + %w{cheezburger pizza lasagna}.each do |cookbook_name| + cookbook = Chef::CookbookVersion.new(cookbook_name) + @cookbooks[cookbook_name] = cookbook + end + @rest = mock("Chef::REST") + @rest.stub!(:get_rest).and_return(@cookbooks) + @rest.stub!(:delete_rest).and_return(true) + @knife.stub!(:rest).and_return(@rest) + Chef::CookbookVersion.stub!(:list).and_return(@cookbooks) + + end + + + + describe "when there are several cookbooks on the server" do + before do + @cheezburger = {'cheezburger' => {"url" => "file:///dev/null", "versions" => [{"url" => "file:///dev/null-cheez", "version" => "1.0.0"}]}} + @rest.stub!(:get_rest).with('cookbooks/cheezburger').and_return(@cheezburger) + @pizza = {'pizza' => {"url" => "file:///dev/null", "versions" => [{"url" => "file:///dev/null-pizza", "version" => "2.0.0"}]}} + @rest.stub!(:get_rest).with('cookbooks/pizza').and_return(@pizza) + @lasagna = {'lasagna' => {"url" => "file:///dev/null", "versions" => [{"url" => "file:///dev/null-lasagna", "version" => "3.0.0"}]}} + @rest.stub!(:get_rest).with('cookbooks/lasagna').and_return(@lasagna) + end + + it "should print the cookbooks you are about to delete" do + expected = @knife.ui.list(@cookbooks.keys.sort, :columns_down) + @knife.run + @stdout.string.should match(/#{expected}/) + end + + it "should confirm you really want to delete them" do + @knife.ui.should_receive(:confirm) + @knife.run + end + + it "should delete each cookbook" do + {"cheezburger" => "1.0.0", "pizza" => "2.0.0", "lasagna" => '3.0.0'}.each do |cookbook_name, version| + @rest.should_receive(:delete_rest).with("cookbooks/#{cookbook_name}/#{version}") + end + @knife.run + end + + it "should only delete cookbooks that match the regex" do + @knife.name_args = ["cheezburger"] + @rest.should_receive(:delete_rest).with('cookbooks/cheezburger/1.0.0') + @knife.run + end + end + + it "should exit if the regex is not provided" do + @knife.name_args = [] + lambda { @knife.run }.should raise_error(SystemExit) + end + +end diff --git a/spec/unit/knife/cookbook_create_spec.rb b/spec/unit/knife/cookbook_create_spec.rb new file mode 100644 index 0000000000..dc1d7d7f79 --- /dev/null +++ b/spec/unit/knife/cookbook_create_spec.rb @@ -0,0 +1,271 @@ +# +# Author:: Nuo Yan (<nuo@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 'tmpdir' + +describe Chef::Knife::CookbookCreate do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::CookbookCreate.new + @knife.config = {} + @knife.name_args = ["foobar"] + @stdout = StringIO.new + @knife.stub!(:stdout).and_return(@stdout) + end + + describe "run" do + + # Fixes CHEF-2579 + it "should expand the path of the cookbook directory" do + File.should_receive(:expand_path).with("~/tmp/monkeypants") + @knife.config = {:cookbook_path => "~/tmp/monkeypants"} + @knife.stub!(:create_cookbook) + @knife.stub!(:create_readme) + @knife.stub!(:create_changelog) + @knife.stub!(:create_metadata) + @knife.run + end + + it "should create a new cookbook with default values to copyright name, email, readme format and license if those are not supplied" do + @dir = Dir.tmpdir + @knife.config = {:cookbook_path => @dir} + @knife.should_receive(:create_cookbook).with(@dir, @knife.name_args.first, "YOUR_COMPANY_NAME", "none") + @knife.should_receive(:create_readme).with(@dir, @knife.name_args.first, "md") + @knife.should_receive(:create_changelog).with(@dir, @knife.name_args.first) + @knife.should_receive(:create_metadata).with(@dir, @knife.name_args.first, "YOUR_COMPANY_NAME", "YOUR_EMAIL", "none", "md") + @knife.run + end + + it "should create a new cookbook with specified company name in the copyright section if one is specified" do + @dir = Dir.tmpdir + @knife.config = { + :cookbook_path => @dir, + :cookbook_copyright => "Opscode, Inc" + } + @knife.name_args=["foobar"] + @knife.should_receive(:create_cookbook).with(@dir, @knife.name_args.first, "Opscode, Inc", "none") + @knife.should_receive(:create_readme).with(@dir, @knife.name_args.first, "md") + @knife.should_receive(:create_changelog).with(@dir, @knife.name_args.first) + @knife.should_receive(:create_metadata).with(@dir, @knife.name_args.first, "Opscode, Inc", "YOUR_EMAIL", "none", "md") + @knife.run + end + + it "should create a new cookbook with specified copyright name and email if they are specified" do + @dir = Dir.tmpdir + @knife.config = { + :cookbook_path => @dir, + :cookbook_copyright => "Opscode, Inc", + :cookbook_email => "nuo@opscode.com" + } + @knife.name_args=["foobar"] + @knife.should_receive(:create_cookbook).with(@dir, @knife.name_args.first, "Opscode, Inc", "none") + @knife.should_receive(:create_readme).with(@dir, @knife.name_args.first, "md") + @knife.should_receive(:create_changelog).with(@dir, @knife.name_args.first) + @knife.should_receive(:create_metadata).with(@dir, @knife.name_args.first, "Opscode, Inc", "nuo@opscode.com", "none", "md") + @knife.run + end + + it "should create a new cookbook with specified copyright name and email and license information (true) if they are specified" do + @dir = Dir.tmpdir + @knife.config = { + :cookbook_path => @dir, + :cookbook_copyright => "Opscode, Inc", + :cookbook_email => "nuo@opscode.com", + :cookbook_license => "apachev2" + } + @knife.name_args=["foobar"] + @knife.should_receive(:create_cookbook).with(@dir, @knife.name_args.first, "Opscode, Inc", "apachev2") + @knife.should_receive(:create_readme).with(@dir, @knife.name_args.first, "md") + @knife.should_receive(:create_changelog).with(@dir, @knife.name_args.first) + @knife.should_receive(:create_metadata).with(@dir, @knife.name_args.first, "Opscode, Inc", "nuo@opscode.com", "apachev2", "md") + @knife.run + end + + it "should create a new cookbook with specified copyright name and email and license information (false) if they are specified" do + @dir = Dir.tmpdir + @knife.config = { + :cookbook_path => @dir, + :cookbook_copyright => "Opscode, Inc", + :cookbook_email => "nuo@opscode.com", + :cookbook_license => false + } + @knife.name_args=["foobar"] + @knife.should_receive(:create_cookbook).with(@dir, @knife.name_args.first, "Opscode, Inc", "none") + @knife.should_receive(:create_readme).with(@dir, @knife.name_args.first, "md") + @knife.should_receive(:create_changelog).with(@dir, @knife.name_args.first) + @knife.should_receive(:create_metadata).with(@dir, @knife.name_args.first, "Opscode, Inc", "nuo@opscode.com", "none", "md") + @knife.run + end + + it "should create a new cookbook with specified copyright name and email and license information ('false' as string) if they are specified" do + @dir = Dir.tmpdir + @knife.config = { + :cookbook_path => @dir, + :cookbook_copyright => "Opscode, Inc", + :cookbook_email => "nuo@opscode.com", + :cookbook_license => "false" + } + @knife.name_args=["foobar"] + @knife.should_receive(:create_cookbook).with(@dir, @knife.name_args.first, "Opscode, Inc", "none") + @knife.should_receive(:create_readme).with(@dir, @knife.name_args.first, "md") + @knife.should_receive(:create_changelog).with(@dir, @knife.name_args.first) + @knife.should_receive(:create_metadata).with(@dir, @knife.name_args.first, "Opscode, Inc", "nuo@opscode.com", "none", "md") + @knife.run + end + + it "should allow specifying a gpl2 license" do + @dir = Dir.tmpdir + @knife.config = { + :cookbook_path => @dir, + :cookbook_copyright => "Opscode, Inc", + :cookbook_email => "nuo@opscode.com", + :cookbook_license => "gplv2" + } + @knife.name_args=["foobar"] + @knife.should_receive(:create_cookbook).with(@dir, @knife.name_args.first, "Opscode, Inc", "gplv2") + @knife.should_receive(:create_readme).with(@dir, @knife.name_args.first, "md") + @knife.should_receive(:create_changelog).with(@dir, @knife.name_args.first) + @knife.should_receive(:create_metadata).with(@dir, @knife.name_args.first, "Opscode, Inc", "nuo@opscode.com", "gplv2", "md") + @knife.run + end + + it "should allow specifying a gplv3 license" do + @dir = Dir.tmpdir + @knife.config = { + :cookbook_path => @dir, + :cookbook_copyright => "Opscode, Inc", + :cookbook_email => "nuo@opscode.com", + :cookbook_license => "gplv3" + } + @knife.name_args=["foobar"] + @knife.should_receive(:create_cookbook).with(@dir, @knife.name_args.first, "Opscode, Inc", "gplv3") + @knife.should_receive(:create_readme).with(@dir, @knife.name_args.first, "md") + @knife.should_receive(:create_changelog).with(@dir, @knife.name_args.first) + @knife.should_receive(:create_metadata).with(@dir, @knife.name_args.first, "Opscode, Inc", "nuo@opscode.com", "gplv3", "md") + @knife.run + end + + it "should allow specifying the mit license" do + @dir = Dir.tmpdir + @knife.config = { + :cookbook_path => @dir, + :cookbook_copyright => "Opscode, Inc", + :cookbook_email => "nuo@opscode.com", + :cookbook_license => "mit" + } + @knife.name_args=["foobar"] + @knife.should_receive(:create_cookbook).with(@dir, @knife.name_args.first, "Opscode, Inc", "mit") + @knife.should_receive(:create_readme).with(@dir, @knife.name_args.first, "md") + @knife.should_receive(:create_changelog).with(@dir, @knife.name_args.first) + @knife.should_receive(:create_metadata).with(@dir, @knife.name_args.first, "Opscode, Inc", "nuo@opscode.com", "mit", "md") + @knife.run + end + + it "should allow specifying the rdoc readme format" do + @dir = Dir.tmpdir + @knife.config = { + :cookbook_path => @dir, + :cookbook_copyright => "Opscode, Inc", + :cookbook_email => "nuo@opscode.com", + :cookbook_license => "mit", + :readme_format => "rdoc" + } + @knife.name_args=["foobar"] + @knife.should_receive(:create_cookbook).with(@dir, @knife.name_args.first, "Opscode, Inc", "mit") + @knife.should_receive(:create_readme).with(@dir, @knife.name_args.first, "rdoc") + @knife.should_receive(:create_changelog).with(@dir, @knife.name_args.first) + @knife.should_receive(:create_metadata).with(@dir, @knife.name_args.first, "Opscode, Inc", "nuo@opscode.com", "mit", "rdoc") + @knife.run + end + + it "should allow specifying the mkd readme format" do + @dir = Dir.tmpdir + @knife.config = { + :cookbook_path => @dir, + :cookbook_copyright => "Opscode, Inc", + :cookbook_email => "nuo@opscode.com", + :cookbook_license => "mit", + :readme_format => "mkd" + } + @knife.name_args=["foobar"] + @knife.should_receive(:create_cookbook).with(@dir, @knife.name_args.first, "Opscode, Inc", "mit") + @knife.should_receive(:create_readme).with(@dir, @knife.name_args.first, "mkd") + @knife.should_receive(:create_changelog).with(@dir, @knife.name_args.first) + @knife.should_receive(:create_metadata).with(@dir, @knife.name_args.first, "Opscode, Inc", "nuo@opscode.com", "mit", "mkd") + @knife.run + end + + it "should allow specifying the txt readme format" do + @dir = Dir.tmpdir + @knife.config = { + :cookbook_path => @dir, + :cookbook_copyright => "Opscode, Inc", + :cookbook_email => "nuo@opscode.com", + :cookbook_license => "mit", + :readme_format => "txt" + } + @knife.name_args=["foobar"] + @knife.should_receive(:create_cookbook).with(@dir, @knife.name_args.first, "Opscode, Inc", "mit") + @knife.should_receive(:create_readme).with(@dir, @knife.name_args.first, "txt") + @knife.should_receive(:create_changelog).with(@dir, @knife.name_args.first) + @knife.should_receive(:create_metadata).with(@dir, @knife.name_args.first, "Opscode, Inc", "nuo@opscode.com", "mit", "txt") + @knife.run + end + + it "should allow specifying an arbitrary readme format" do + @dir = Dir.tmpdir + @knife.config = { + :cookbook_path => @dir, + :cookbook_copyright => "Opscode, Inc", + :cookbook_email => "nuo@opscode.com", + :cookbook_license => "mit", + :readme_format => "foo" + } + @knife.name_args=["foobar"] + @knife.should_receive(:create_cookbook).with(@dir, @knife.name_args.first, "Opscode, Inc", "mit") + @knife.should_receive(:create_readme).with(@dir, @knife.name_args.first, "foo") + @knife.should_receive(:create_changelog).with(@dir, @knife.name_args.first) + @knife.should_receive(:create_metadata).with(@dir, @knife.name_args.first, "Opscode, Inc", "nuo@opscode.com", "mit", "foo") + @knife.run + end + + it "should create a CHANGELOG file" do + @dir = Dir.tmpdir + @knife.should_receive(:create_changelog).with(@dir, @knife.name_args.first) + @knife.run + end + + context "when the cookbooks path is not specified in the config file nor supplied via parameter" do + before do + @old_cookbook_path = Chef::Config[:cookbook_path] + Chef::Config[:cookbook_path] = nil + end + + it "should throw an argument error" do + @dir = Dir.tmpdir + lambda{@knife.run}.should raise_error(ArgumentError) + end + + after do + Chef::Config[:cookbook_path] = @old_cookbook_path + end + end + + end +end diff --git a/spec/unit/knife/cookbook_delete_spec.rb b/spec/unit/knife/cookbook_delete_spec.rb new file mode 100644 index 0000000000..afaa3b69a5 --- /dev/null +++ b/spec/unit/knife/cookbook_delete_spec.rb @@ -0,0 +1,239 @@ +# +# Author:: Thomas Bishop (<bishop.thomas@gmail.com>) +# Copyright:: Copyright (c) 2011 Thomas Bishop +# 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::Knife::CookbookDelete do + before(:each) do + @knife = Chef::Knife::CookbookDelete.new + @knife.name_args = ['foobar'] + @knife.cookbook_name = 'foobar' + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + @stderr = StringIO.new + @knife.ui.stub!(:stderr).and_return(@stderr) + end + + describe 'run' do + it 'should print usage and exit when a cookbook name is not provided' do + @knife.name_args = [] + @knife.should_receive(:show_usage) + @knife.ui.should_receive(:fatal) + lambda { @knife.run }.should raise_error(SystemExit) + end + + describe 'when specifying a cookbook name' do + it 'should delete the cookbook without a specific version' do + @knife.should_receive(:delete_without_explicit_version) + @knife.run + end + + describe 'and a version' do + it 'should delete the specific version of the cookbook' do + @knife.name_args << '1.0.0' + @knife.should_receive(:delete_explicit_version) + @knife.run + end + end + + describe 'with -a or --all' do + it 'should delete all versions of the cookbook' do + @knife.config[:all] = true + @knife.should_receive(:delete_all_versions) + @knife.run + end + end + + describe 'with -p or --purge' do + it 'should prompt to purge the files' do + @knife.config[:purge] = true + @knife.should_receive(:confirm). + with(/.+Are you sure you want to purge files.+/) + @knife.should_receive(:delete_without_explicit_version) + @knife.run + end + end + end + end + + describe 'delete_explicit_version' do + it 'should delete the specific cookbook version' do + @knife.cookbook_name = 'foobar' + @knife.version = '1.0.0' + @knife.should_receive(:delete_object).with(Chef::CookbookVersion, + 'foobar version 1.0.0', + 'cookbook').and_yield() + @knife.should_receive(:delete_request).with('cookbooks/foobar/1.0.0') + @knife.delete_explicit_version + end + end + + describe 'delete_all_versions' do + it 'should prompt to delete all versions of the cookbook' do + @knife.cookbook_name = 'foobar' + @knife.should_receive(:confirm).with('Do you really want to delete all versions of foobar') + @knife.should_receive(:delete_all_without_confirmation) + @knife.delete_all_versions + end + end + + describe 'delete_all_without_confirmation' do + it 'should delete all versions without confirmation' do + versions = ['1.0.0', '1.1.0'] + @knife.should_receive(:available_versions).and_return(versions) + versions.each do |v| + @knife.should_receive(:delete_version_without_confirmation).with(v) + end + @knife.delete_all_without_confirmation + end + end + + describe 'delete_without_explicit_version' do + it 'should exit if there are no available versions' do + @knife.should_receive(:available_versions).and_return(nil) + lambda { @knife.delete_without_explicit_version }.should raise_error(SystemExit) + end + + it 'should delete the version if only one is found' do + @knife.should_receive(:available_versions).at_least(:once).and_return(['1.0.0']) + @knife.should_receive(:delete_explicit_version) + @knife.delete_without_explicit_version + end + + it 'should ask which version(s) to delete if multiple are found' do + @knife.should_receive(:available_versions).at_least(:once).and_return(['1.0.0', '1.1.0']) + @knife.should_receive(:ask_which_versions_to_delete).and_return(['1.0.0', '1.1.0']) + @knife.should_receive(:delete_versions_without_confirmation).with(['1.0.0', '1.1.0']) + @knife.delete_without_explicit_version + end + end + + describe 'available_versions' do + before(:each) do + @rest_mock = mock('rest') + @knife.should_receive(:rest).and_return(@rest_mock) + @cookbook_data = { 'foobar' => { 'versions' => [{'version' => '1.0.0'}, + {'version' => '1.1.0'}, + {'version' => '2.0.0'} ]} + } + end + + it 'should return the list of versions of the cookbook' do + @rest_mock.should_receive(:get_rest).with('cookbooks/foobar').and_return(@cookbook_data) + @knife.available_versions.should == ['1.0.0', '1.1.0', '2.0.0'] + end + + it 'should raise if an error other than HTTP 404 is returned' do + exception = Net::HTTPServerException.new('500 Internal Server Error', '500') + @rest_mock.should_receive(:get_rest).and_raise(exception) + lambda { @knife.available_versions }.should raise_error Net::HTTPServerException + end + + describe "if the cookbook can't be found" do + before(:each) do + @rest_mock.should_receive(:get_rest). + and_raise(Net::HTTPServerException.new('404 Not Found', '404')) + end + + it 'should print an error' do + @knife.available_versions + @stderr.string.should match /error.+cannot find a cookbook named foobar/i + end + + it 'should return nil' do + @knife.available_versions.should == nil + end + end + end + + describe 'ask_which_version_to_delete' do + before(:each) do + @knife.stub!(:available_versions).and_return(['1.0.0', '1.1.0', '2.0.0']) + end + + it 'should prompt the user to select a version' do + prompt = /Which version\(s\) do you want to delete\?.+1\. foobar 1\.0\.0.+2\. foobar 1\.1\.0.+3\. foobar 2\.0\.0.+4\. All versions.+/m + @knife.should_receive(:ask_question).with(prompt).and_return('1') + @knife.ask_which_versions_to_delete + end + + it "should print an error and exit if a version wasn't specified" do + @knife.should_receive(:ask_question).and_return('') + @knife.ui.should_receive(:error).with(/no versions specified/i) + lambda { @knife.ask_which_versions_to_delete }.should raise_error(SystemExit) + end + + it 'should print an error if an invalid choice was selected' do + @knife.should_receive(:ask_question).and_return('100') + @knife.ui.should_receive(:error).with(/100 is not a valid choice/i) + @knife.ask_which_versions_to_delete + end + + it 'should return the selected versions' do + @knife.should_receive(:ask_question).and_return('1, 3') + @knife.ask_which_versions_to_delete.should == ['1.0.0', '2.0.0'] + end + + it "should return all of the versions if 'all' was selected" do + @knife.should_receive(:ask_question).and_return('4') + @knife.ask_which_versions_to_delete.should == [:all] + end + end + + describe 'delete_version_without_confirmation' do + it 'should delete the cookbook version' do + @knife.should_receive(:delete_request).with('cookbooks/foobar/1.0.0') + @knife.delete_version_without_confirmation('1.0.0') + end + + it 'should output that the cookbook was deleted' do + @knife.stub!(:delete_request) + @knife.delete_version_without_confirmation('1.0.0') + @stdout.string.should match /deleted cookbook\[foobar\]\[1.0.0\]/im + end + + describe 'with --print-after' do + it 'should display the cookbook data' do + object = '' + @knife.config[:print_after] = true + @knife.stub!(:delete_request).and_return(object) + @knife.should_receive(:format_for_display).with(object) + @knife.delete_version_without_confirmation('1.0.0') + end + end + end + + describe 'delete_versions_without_confirmation' do + it 'should delete each version without confirmation' do + versions = ['1.0.0', '1.1.0'] + versions.each do |v| + @knife.should_receive(:delete_version_without_confirmation).with(v) + end + @knife.delete_versions_without_confirmation(versions) + end + + describe 'with -a or --all' do + it 'should delete all versions without confirmation' do + versions = [:all] + @knife.should_receive(:delete_all_without_confirmation) + @knife.delete_versions_without_confirmation(versions) + end + end + end + +end diff --git a/spec/unit/knife/cookbook_download_spec.rb b/spec/unit/knife/cookbook_download_spec.rb new file mode 100644 index 0000000000..6ae3fbecd2 --- /dev/null +++ b/spec/unit/knife/cookbook_download_spec.rb @@ -0,0 +1,217 @@ +# +# Author:: Thomas Bishop (<bishop.thomas@gmail.com>) +# Copyright:: Copyright (c) 2011 Thomas Bishop +# 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::Knife::CookbookDownload do + before(:each) do + @knife = Chef::Knife::CookbookDownload.new + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + describe 'run' do + it 'should print usage and exit when a cookbook name is not provided' do + @knife.name_args = [] + @knife.should_receive(:show_usage) + @knife.ui.should_receive(:fatal).with(/must specify a cookbook name/) + lambda { @knife.run }.should raise_error(SystemExit) + end + + describe 'with a cookbook name' do + before(:each) do + @knife.name_args = ['foobar'] + @knife.config[:download_directory] = '/var/tmp/chef' + @rest_mock = mock('rest') + @knife.stub(:rest).and_return(@rest_mock) + + @manifest_data = { + :recipes => [ + {'path' => 'recipes/foo.rb', + 'url' => 'http://example.org/files/foo.rb'}, + {'path' => 'recipes/bar.rb', + 'url' => 'http://example.org/files/bar.rb'} + ], + :templates => [ + {'path' => 'templates/default/foo.erb', + 'url' => 'http://example.org/files/foo.erb'}, + {'path' => 'templates/default/bar.erb', + 'url' => 'http://example.org/files/bar.erb'} + ], + :attributes => [ + {'path' => 'attributes/default.rb', + 'url' => 'http://example.org/files/default.rb'} + ] + } + + @cookbook_mock = mock('cookbook') + @cookbook_mock.stub!(:version).and_return('1.0.0') + @cookbook_mock.stub!(:manifest).and_return(@manifest_data) + @rest_mock.should_receive(:get_rest).with('cookbooks/foobar/1.0.0'). + and_return(@cookbook_mock) + end + + it 'should determine which version if one was not explicitly specified'do + @cookbook_mock.stub!(:manifest).and_return({}) + @knife.should_receive(:determine_version).and_return('1.0.0') + File.should_receive(:exists?).with('/var/tmp/chef/foobar-1.0.0').and_return(false) + Chef::CookbookVersion.stub!(:COOKBOOK_SEGEMENTS).and_return([]) + @knife.run + end + + describe 'and a version' do + before(:each) do + @knife.name_args << '1.0.0' + @files = @manifest_data.values.map { |v| v.map { |i| i['path'] } }.flatten.uniq + @files_mocks = {} + @files.map { |f| File.basename(f) }.flatten.uniq.each do |f| + @files_mocks[f] = mock("#{f}_mock") + @files_mocks[f].stub!(:path).and_return("/var/tmp/#{f}") + end + end + + it 'should print an error and exit if the cookbook download directory already exists' do + File.should_receive(:exists?).with('/var/tmp/chef/foobar-1.0.0').and_return(true) + @knife.ui.should_receive(:fatal).with(/\/var\/tmp\/chef\/foobar-1\.0\.0 exists/i) + lambda { @knife.run }.should raise_error(SystemExit) + end + + describe 'when downloading the cookbook' do + before(:each) do + @files.map { |f| File.dirname(f) }.flatten.uniq.each do |dir| + FileUtils.should_receive(:mkdir_p).with("/var/tmp/chef/foobar-1.0.0/#{dir}"). + at_least(:once) + end + + @files_mocks.each_pair do |file, mock| + @rest_mock.should_receive(:get_rest).with("http://example.org/files/#{file}", true). + and_return(mock) + end + + @rest_mock.should_receive(:sign_on_redirect=).with(false).at_least(:once) + @files.each do |f| + FileUtils.should_receive(:mv). + with("/var/tmp/#{File.basename(f)}", "/var/tmp/chef/foobar-1.0.0/#{f}") + end + end + + it "should download the cookbook when the cookbook download directory doesn't exist" do + File.should_receive(:exists?).with('/var/tmp/chef/foobar-1.0.0').and_return(false) + @knife.run + ['attributes', 'recipes', 'templates'].each do |segment| + @stdout.string.should match /downloading #{segment}/im + end + @stdout.string.should match /downloading foobar cookbook version 1\.0\.0/im + @stdout.string.should match /cookbook downloaded to \/var\/tmp\/chef\/foobar-1\.0\.0/im + end + + describe 'with -f or --force' do + it 'should remove the existing the cookbook download directory if it exists' do + @knife.config[:force] = true + File.should_receive(:exists?).with('/var/tmp/chef/foobar-1.0.0').and_return(true) + FileUtils.should_receive(:rm_rf).with('/var/tmp/chef/foobar-1.0.0') + @knife.run + end + end + end + + end + end + + end + + describe 'determine_version' do + it 'should return and set the version if there is only one version' do + @knife.should_receive(:available_versions).at_least(:once).and_return(['1.0.0']) + @knife.determine_version.should == '1.0.0' + @knife.version.should == '1.0.0' + end + + it 'should ask which version to download and return it if there is more than one' do + @knife.should_receive(:available_versions).and_return(['1.0.0', '2.0.0']) + @knife.should_receive(:ask_which_version).and_return('1.0.0') + @knife.determine_version.should == '1.0.0' + end + + describe 'with -N or --latest' do + it 'should return and set the version to the latest version' do + @knife.config[:latest] = true + @knife.should_receive(:available_versions).at_least(:once). + and_return(['1.0.0', '2.0.0', '1.1.0']) + @knife.determine_version + @knife.version.to_s.should == '2.0.0' + end + end + end + + describe 'available_versions' do + before(:each) do + @knife.cookbook_name = 'foobar' + end + + it 'should return the available vesions' do + Chef::CookbookVersion.should_receive(:available_versions). + with('foobar'). + and_return(['1.1.0', '2.0.0', '1.0.0']) + @knife.available_versions.should == [Chef::Version.new('1.0.0'), + Chef::Version.new('1.1.0'), + Chef::Version.new('2.0.0')] + end + + it 'should avoid multiple API calls to the server' do + Chef::CookbookVersion.should_receive(:available_versions). + once. + with('foobar'). + and_return(['1.1.0', '2.0.0', '1.0.0']) + @knife.available_versions + @knife.available_versions + end + end + + describe 'ask_which_version' do + before(:each) do + @knife.cookbook_name = 'foobar' + @knife.stub!(:available_versions).and_return(['1.0.0', '1.1.0', '2.0.0']) + end + + it 'should prompt the user to select a version' do + prompt = /Which version do you want to download\?.+1\. foobar 1\.0\.0.+2\. foobar 1\.1\.0.+3\. foobar 2\.0\.0.+/m + @knife.should_receive(:ask_question).with(prompt).and_return('1') + @knife.ask_which_version + end + + it "should set the version to the user's selection" do + @knife.should_receive(:ask_question).and_return('1') + @knife.ask_which_version + @knife.version.should == '1.0.0' + end + + it "should print an error and exit if a version wasn't specified" do + @knife.should_receive(:ask_question).and_return('') + @knife.ui.should_receive(:error).with(/is not a valid value/i) + lambda { @knife.ask_which_version }.should raise_error(SystemExit) + end + + it 'should print an error if an invalid choice was selected' do + @knife.should_receive(:ask_question).and_return('100') + @knife.ui.should_receive(:error).with(/'100' is not a valid value/i) + lambda { @knife.ask_which_version }.should raise_error(SystemExit) + end + end + +end diff --git a/spec/unit/knife/cookbook_list_spec.rb b/spec/unit/knife/cookbook_list_spec.rb new file mode 100644 index 0000000000..db6f061bd1 --- /dev/null +++ b/spec/unit/knife/cookbook_list_spec.rb @@ -0,0 +1,88 @@ +# +# Author:: Thomas Bishop (<bishop.thomas@gmail.com>) +# Copyright:: Copyright (c) 2011 Thomas Bishop +# 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::Knife::CookbookList do + before do + @knife = Chef::Knife::CookbookList.new + @rest_mock = mock('rest') + @knife.stub!(:rest).and_return(@rest_mock) + @cookbook_names = ['apache2', 'mysql'] + @base_url = 'https://server.example.com/cookbooks' + @cookbook_data = {} + @cookbook_names.each do |item| + @cookbook_data[item] = {'url' => "#{@base_url}/#{item}", + 'versions' => [{'version' => '1.0.1', + 'url' => "#{@base_url}/#{item}/1.0.1"}]} + end + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + describe 'run' do + it 'should display the latest version of the cookbooks' do + @rest_mock.should_receive(:get_rest).with('/cookbooks?num_versions=1'). + and_return(@cookbook_data) + @knife.run + @cookbook_names.each do |item| + @stdout.string.should match /#{item}\s+1\.0\.1/ + end + end + + it 'should query cookbooks for the configured environment' do + @knife.config[:environment] = 'production' + @rest_mock.should_receive(:get_rest). + with('/environments/production/cookbooks?num_versions=1'). + and_return(@cookbook_data) + @knife.run + end + + describe 'with -w or --with-uri' do + it 'should display the cookbook uris' do + @knife.config[:with_uri] = true + @rest_mock.stub(:get_rest).and_return(@cookbook_data) + @knife.run + @cookbook_names.each do |item| + pattern = /#{Regexp.escape(@cookbook_data[item]['versions'].first['url'])}/ + @stdout.string.should match pattern + end + end + end + + describe 'with -a or --all' do + before do + @cookbook_names.each do |item| + @cookbook_data[item]['versions'] << {'version' => '1.0.0', + 'url' => "#{@base_url}/#{item}/1.0.0"} + end + end + + it 'should display all versions of the cookbooks' do + @knife.config[:all_versions] = true + @rest_mock.should_receive(:get_rest).with('/cookbooks?num_versions=all'). + and_return(@cookbook_data) + @knife.run + @cookbook_names.each do |item| + @stdout.string.should match /#{item}\s+1\.0\.1\s+1\.0\.0/ + end + end + end + + end +end diff --git a/spec/unit/knife/cookbook_metadata_from_file_spec.rb b/spec/unit/knife/cookbook_metadata_from_file_spec.rb new file mode 100644 index 0000000000..60555d89dc --- /dev/null +++ b/spec/unit/knife/cookbook_metadata_from_file_spec.rb @@ -0,0 +1,65 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Matthew Kent (<mkent@magoazul.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# 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::Knife::CookbookMetadataFromFile do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @src = File.expand_path(File.join(CHEF_SPEC_DATA, "metadata", "quick_start", "metadata.rb")) + @tgt = File.expand_path(File.join(CHEF_SPEC_DATA, "metadata", "quick_start", "metadata.json")) + @knife = Chef::Knife::CookbookMetadataFromFile.new + @knife.name_args = [ @src ] + @knife.stub!(:to_json_pretty).and_return(true) + @md = Chef::Cookbook::Metadata.new + Chef::Cookbook::Metadata.stub(:new).and_return(@md) + $stdout.stub!(:write) + end + + after do + if File.exists?(@tgt) + File.unlink(@tgt) + end + end + + describe "run" do + it "should determine cookbook name from path" do + @md.should_receive(:name).with() + @md.should_receive(:name).with("quick_start") + @knife.run + end + + it "should load the metadata source" do + @md.should_receive(:from_file).with(@src) + @knife.run + end + + it "should write out the metadata to the correct location" do + File.should_receive(:open).with(@tgt, "w") + @knife.run + end + + it "should generate json from the metadata" do + Chef::JSONCompat.should_receive(:to_json_pretty).with(@md) + @knife.run + end + + end +end diff --git a/spec/unit/knife/cookbook_metadata_spec.rb b/spec/unit/knife/cookbook_metadata_spec.rb new file mode 100644 index 0000000000..c664326a3d --- /dev/null +++ b/spec/unit/knife/cookbook_metadata_spec.rb @@ -0,0 +1,179 @@ +# +# Author:: Thomas Bishop (<bishop.thomas@gmail.com>) +# Copyright:: Copyright (c) 2011 Thomas Bishop +# 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::Knife::CookbookMetadata do + before(:each) do + @knife = Chef::Knife::CookbookMetadata.new + @knife.name_args = ['foobar'] + @cookbook_dir = Dir.mktmpdir + @json_data = '{ "version": "1.0.0" }' + @stdout = StringIO.new + @stderr = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + @knife.ui.stub!(:stderr).and_return(@stderr) + end + + describe 'run' do + it 'should print an error and exit if a cookbook name was not provided' do + @knife.name_args = [] + @knife.ui.should_receive(:error).with(/you must specify the cookbook.+use the --all/i) + lambda { @knife.run }.should raise_error(SystemExit) + end + + it 'should print an error and exit if an empty cookbook name was provided' do + @knife.name_args = [''] + @knife.ui.should_receive(:error).with(/you must specify the cookbook.+use the --all/i) + lambda { @knife.run }.should raise_error(SystemExit) + end + + it 'should generate the metadata for the cookbook' do + @knife.should_receive(:generate_metadata).with('foobar') + @knife.run + end + + describe 'with -a or --all' do + before(:each) do + @knife.config[:all] = true + @foo = Chef::CookbookVersion.new('foo') + @foo.version = '1.0.0' + @bar = Chef::CookbookVersion.new('bar') + @bar.version = '2.0.0' + @cookbook_loader = { + "foo" => @foo, + "bar" => @bar + } + @cookbook_loader.should_receive(:load_cookbooks).and_return(@cookbook_loader) + @knife.should_receive(:generate_metadata).with('foo') + @knife.should_receive(:generate_metadata).with('bar') + end + + it 'should generate the metadata for each cookbook' do + Chef::Config[:cookbook_path] = @cookbook_dir + Chef::CookbookLoader.should_receive(:new).with(@cookbook_dir).and_return(@cookbook_loader) + @knife.run + end + + describe 'and with -o or --cookbook-path' do + it 'should look in the provided path and generate cookbook metadata' do + @knife.config[:cookbook_path] = '/opt/chef/cookbooks' + Chef::CookbookLoader.should_receive(:new).with('/opt/chef/cookbooks').and_return(@cookbook_loader) + @knife.run + end + end + end + + end + + describe 'generate_metadata' do + before(:each) do + @knife.config[:cookbook_path] = @cookbook_dir + File.stub!(:expand_path).with("#{@cookbook_dir}/foobar/metadata.rb"). + and_return("#{@cookbook_dir}/foobar/metadata.rb") + end + + it 'should generate the metadata from metadata.rb if it exists' do + File.should_receive(:exists?).with("#{@cookbook_dir}/foobar/metadata.rb"). + and_return(true) + @knife.should_receive(:generate_metadata_from_file).with('foobar', "#{@cookbook_dir}/foobar/metadata.rb") + @knife.run + end + + it 'should validate the metadata json if metadata.rb does not exist' do + File.should_receive(:exists?).with("#{@cookbook_dir}/foobar/metadata.rb"). + and_return(false) + @knife.should_receive(:validate_metadata_json).with(@cookbook_dir, 'foobar') + @knife.run + end + end + + describe 'generate_metadata_from_file' do + before(:each) do + @metadata_mock = mock('metadata') + @json_file_mock = mock('json_file') + end + + it 'should generate the metatdata json from metatdata.rb' do + Chef::Cookbook::Metadata.stub!(:new).and_return(@metadata_mock) + @metadata_mock.should_receive(:name).with('foobar') + @metadata_mock.should_receive(:from_file).with("#{@cookbook_dir}/foobar/metadata.rb") + File.should_receive(:open).with("#{@cookbook_dir}/foobar/metadata.json", 'w'). + and_yield(@json_file_mock) + @json_file_mock.should_receive(:write).with(@json_data) + Chef::JSONCompat.should_receive(:to_json_pretty).with(@metadata_mock). + and_return(@json_data) + @knife.generate_metadata_from_file('foobar', "#{@cookbook_dir}/foobar/metadata.rb") + @stdout.string.should match /generating metadata for foobar from #{@cookbook_dir}\/foobar\/metadata\.rb/im + end + + { Chef::Exceptions::ObsoleteDependencySyntax => 'obsolote dependency', + Chef::Exceptions::InvalidVersionConstraint => 'invalid version constraint' + }.each_pair do |klass, description| + it "should print an error and exit when an #{description} syntax exception is encountered" do + exception = klass.new("#{description} blah") + Chef::Cookbook::Metadata.stub!(:new).and_raise(exception) + lambda { + @knife.generate_metadata_from_file('foobar', "#{@cookbook_dir}/foobar/metadata.rb") + }.should raise_error(SystemExit) + @stderr.string.should match /error: the cookbook 'foobar' contains invalid or obsolete metadata syntax/im + @stderr.string.should match /in #{@cookbook_dir}\/foobar\/metadata\.rb/im + @stderr.string.should match /#{description} blah/im + end + end + end + + describe 'validate_metadata_json' do + it 'should validate the metadata json' do + File.should_receive(:exist?).with("#{@cookbook_dir}/foobar/metadata.json"). + and_return(true) + IO.should_receive(:read).with("#{@cookbook_dir}/foobar/metadata.json"). + and_return(@json_data) + Chef::Cookbook::Metadata.should_receive(:validate_json).with(@json_data) + @knife.validate_metadata_json(@cookbook_dir, 'foobar') + end + + it 'should not try to validate the metadata json if the file does not exist' do + File.should_receive(:exist?).with("#{@cookbook_dir}/foobar/metadata.json"). + and_return(false) + IO.should_not_receive(:read) + Chef::Cookbook::Metadata.should_not_receive(:validate_json) + @knife.validate_metadata_json(@cookbook_dir, 'foobar') + end + + { Chef::Exceptions::ObsoleteDependencySyntax => 'obsolote dependency', + Chef::Exceptions::InvalidVersionConstraint => 'invalid version constraint' + }.each_pair do |klass, description| + it "should print an error and exit when an #{description} syntax exception is encountered" do + File.should_receive(:exist?).with("#{@cookbook_dir}/foobar/metadata.json"). + and_return(true) + IO.should_receive(:read).with("#{@cookbook_dir}/foobar/metadata.json"). + and_return(@json_data) + exception = klass.new("#{description} blah") + Chef::Cookbook::Metadata.stub!(:validate_json).and_raise(exception) + lambda { + @knife.validate_metadata_json(@cookbook_dir, 'foobar') + }.should raise_error(SystemExit) + @stderr.string.should match /error: the cookbook 'foobar' contains invalid or obsolete metadata syntax/im + @stderr.string.should match /in #{@cookbook_dir}\/foobar\/metadata\.json/im + @stderr.string.should match /#{description} blah/im + end + end + end + +end diff --git a/spec/unit/knife/cookbook_show_spec.rb b/spec/unit/knife/cookbook_show_spec.rb new file mode 100644 index 0000000000..2f2d841fea --- /dev/null +++ b/spec/unit/knife/cookbook_show_spec.rb @@ -0,0 +1,223 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, eersion 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. +# + +# rename to cookbook not coookbook +require 'spec_helper' + +describe Chef::Knife::CookbookShow do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::CookbookShow.new + @knife.config = { } + @knife.name_args = [ "cookbook_name" ] + @rest = mock(Chef::REST) + @knife.stub!(:rest).and_return(@rest) + @knife.stub!(:pretty_print).and_return(true) + @knife.stub!(:output).and_return(true) + end + + describe "run" do + describe "with 0 arguments: help" do + it 'should should print usage and exit when given no arguments' do + @knife.name_args = [] + @knife.should_receive(:show_usage) + @knife.ui.should_receive(:fatal) + lambda { @knife.run }.should raise_error(SystemExit) + end + end + + describe "with 1 argument: versions" do + before(:each) do + @response = { + "cookbook_name" => { + "url" => "http://url/cookbooks/cookbook_name", + "versions" => [ + { "version" => "0.10.0", "url" => "http://url/cookbooks/cookbook_name/0.10.0" }, + { "version" => "0.9.0", "url" => "http://url/cookbookx/cookbook_name/0.9.0" }, + { "version" => "0.8.0", "url" => "http://url/cookbooks/cookbook_name/0.8.0" } + ] + } + } + end + + it "should show the raw cookbook data" do + @rest.should_receive(:get_rest).with("cookbooks/cookbook_name").and_return(@response) + @knife.should_receive(:format_cookbook_list_for_display).with(@response) + @knife.run + end + + it "should respect the user-supplied environment" do + @knife.config[:environment] = "foo" + @rest.should_receive(:get_rest).with("environments/foo/cookbooks/cookbook_name").and_return(@response) + @knife.should_receive(:format_cookbook_list_for_display).with(@response) + @knife.run + end + end + + describe "with 2 arguments: name and version" do + before(:each) do + @knife.name_args << "0.1.0" + @response = { "0.1.0" => { "recipes" => {"default.rb" => ""} } } + end + + it "should show the specific part of a cookbook" do + @rest.should_receive(:get_rest).with("cookbooks/cookbook_name/0.1.0").and_return(@response) + @knife.should_receive(:output).with(@response) + @knife.run + end + end + + describe "with 3 arguments: name, version, and segment" do + before(:each) do + @knife.name_args = [ "cookbook_name", "0.1.0", "recipes" ] + @cookbook_response = Chef::CookbookVersion.new("cookbook_name") + @manifest = { + "recipes" => [ + { + :name => "default.rb", + :path => "recipes/default.rb", + :checksum => "1234", + :url => "http://example.org/files/default.rb" + } + ] + } + @cookbook_response.manifest = @manifest + @response = {"name"=>"default.rb", "url"=>"http://example.org/files/default.rb", "checksum"=>"1234", "path"=>"recipes/default.rb"} + end + + it "should print the json of the part" do + @rest.should_receive(:get_rest).with("cookbooks/cookbook_name/0.1.0").and_return(@cookbook_response) + @knife.should_receive(:output).with(@cookbook_response.manifest["recipes"]) + @knife.run + end + end + + describe "with 4 arguments: name, version, segment and filename" do + before(:each) do + @knife.name_args = [ "cookbook_name", "0.1.0", "recipes", "default.rb" ] + @cookbook_response = Chef::CookbookVersion.new("cookbook_name") + @cookbook_response.manifest = { + "recipes" => [ + { + :name => "default.rb", + :path => "recipes/default.rb", + :checksum => "1234", + :url => "http://example.org/files/default.rb" + } + ] + } + @response = "Example recipe text" + end + + it "should print the raw result of the request (likely a file!)" do + @rest.should_receive(:get_rest).with("cookbooks/cookbook_name/0.1.0").and_return(@cookbook_response) + @rest.should_receive(:get_rest).with("http://example.org/files/default.rb", true).and_return(StringIO.new(@response)) + @knife.should_receive(:pretty_print).with(@response) + @knife.run + end + end + + describe "with 4 arguments: name, version, segment and filename -- with specificity" do + before(:each) do + @knife.name_args = [ "cookbook_name", "0.1.0", "files", "afile.rb" ] + @cookbook_response = Chef::CookbookVersion.new("cookbook_name") + @cookbook_response.manifest = { + "files" => [ + { + :name => "afile.rb", + :path => "files/host-examplehost.example.org/afile.rb", + :checksum => "1111", + :specificity => "host-examplehost.example.org", + :url => "http://example.org/files/1111" + }, + { + :name => "afile.rb", + :path => "files/ubuntu-9.10/afile.rb", + :checksum => "2222", + :specificity => "ubuntu-9.10", + :url => "http://example.org/files/2222" + }, + { + :name => "afile.rb", + :path => "files/ubuntu/afile.rb", + :checksum => "3333", + :specificity => "ubuntu", + :url => "http://example.org/files/3333" + }, + { + :name => "afile.rb", + :path => "files/default/afile.rb", + :checksum => "4444", + :specificity => "default", + :url => "http://example.org/files/4444" + }, + ] + } + + @response = "Example recipe text" + end + + describe "with --fqdn" do + it "should pass the fqdn" do + @knife.config[:platform] = "example_platform" + @knife.config[:platform_version] = "1.0" + @knife.config[:fqdn] = "examplehost.example.org" + @rest.should_receive(:get_rest).with("cookbooks/cookbook_name/0.1.0").and_return(@cookbook_response) + @rest.should_receive(:get_rest).with("http://example.org/files/1111", true).and_return(StringIO.new(@response)) + @knife.should_receive(:pretty_print).with(@response) + @knife.run + end + end + + describe "and --platform" do + it "should pass the platform" do + @knife.config[:platform] = "ubuntu" + @knife.config[:platform_version] = "1.0" + @knife.config[:fqdn] = "differenthost.example.org" + @rest.should_receive(:get_rest).with("cookbooks/cookbook_name/0.1.0").and_return(@cookbook_response) + @rest.should_receive(:get_rest).with("http://example.org/files/3333", true).and_return(StringIO.new(@response)) + @knife.should_receive(:pretty_print).with(@response) + @knife.run + end + end + + describe "and --platform-version" do + it "should pass the platform" do + @knife.config[:platform] = "ubuntu" + @knife.config[:platform_version] = "9.10" + @knife.config[:fqdn] = "differenthost.example.org" + @rest.should_receive(:get_rest).with("cookbooks/cookbook_name/0.1.0").and_return(@cookbook_response) + @rest.should_receive(:get_rest).with("http://example.org/files/2222", true).and_return(StringIO.new(@response)) + @knife.should_receive(:pretty_print).with(@response) + @knife.run + end + end + + describe "with none of the arguments, it should use the default" do + it "should pass them all" do + @rest.should_receive(:get_rest).with("cookbooks/cookbook_name/0.1.0").and_return(@cookbook_response) + @rest.should_receive(:get_rest).with("http://example.org/files/4444", true).and_return(StringIO.new(@response)) + @knife.should_receive(:pretty_print).with(@response) + @knife.run + end + end + + end + end +end + diff --git a/spec/unit/knife/cookbook_site_download_spec.rb b/spec/unit/knife/cookbook_site_download_spec.rb new file mode 100644 index 0000000000..a3d43c5b4a --- /dev/null +++ b/spec/unit/knife/cookbook_site_download_spec.rb @@ -0,0 +1,151 @@ +# +# Author:: Thomas Bishop (<bishop.thomas@gmail.com>) +# Copyright:: Copyright (c) 2012 Thomas Bishop +# 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.dirname(__FILE__) + '/../../spec_helper') + +describe Chef::Knife::CookbookSiteDownload do + + describe 'run' do + before do + @knife = Chef::Knife::CookbookSiteDownload.new + @knife.name_args = ['apache2'] + @noauth_rest = mock 'no auth rest' + @stdout = StringIO.new + @cookbook_api_url = 'http://cookbooks.opscode.com/api/v1/cookbooks' + @version = '1.0.2' + @version_us = @version.gsub '.', '_' + @current_data = { 'deprecated' => false, + 'latest_version' => "#{@cookbook_api_url}/apache2/versions/#{@version_us}", + 'replacement' => 'other_apache2' } + + @knife.ui.stub(:stdout).and_return(@stdout) + @knife.stub(:noauth_rest).and_return(@noauth_rest) + @noauth_rest.should_receive(:get_rest). + with("#{@cookbook_api_url}/apache2"). + and_return(@current_data) + end + + context 'when the cookbook is deprecated and not forced' do + before do + @current_data['deprecated'] = true + end + + it 'should warn with info about the replacement' do + @knife.ui.should_receive(:warn). + with(/.+deprecated.+replaced by other_apache2.+/i) + @knife.ui.should_receive(:warn). + with(/use --force.+download.+/i) + @knife.run + end + end + + context 'when' do + before do + @cookbook_data = { 'version' => @version, + 'file' => "http://example.com/apache2_#{@version_us}.tgz" } + @temp_file = stub :path => "/tmp/apache2_#{@version_us}.tgz" + @file = File.join(Dir.pwd, "apache2-#{@version}.tar.gz") + + @noauth_rest.should_receive(:sign_on_redirect=).with(false) + end + + context 'downloading the latest version' do + before do + @noauth_rest.should_receive(:get_rest). + with(@current_data['latest_version']). + and_return(@cookbook_data) + @noauth_rest.should_receive(:get_rest). + with(@cookbook_data['file'], true). + and_return(@temp_file) + end + + context 'and it is deprecated and with --force' do + before do + @current_data['deprecated'] = true + @knife.config[:force] = true + end + + it 'should download the latest version' do + @knife.ui.should_receive(:warn). + with(/.+deprecated.+replaced by other_apache2.+/i) + FileUtils.should_receive(:cp).with(@temp_file.path, @file) + @knife.run + @stdout.string.should match /downloading apache2.+version.+#{Regexp.escape(@version)}/i + @stdout.string.should match /cookbook save.+#{Regexp.escape(@file)}/i + end + + end + + it 'should download the latest version' do + FileUtils.should_receive(:cp).with(@temp_file.path, @file) + @knife.run + @stdout.string.should match /downloading apache2.+version.+#{Regexp.escape(@version)}/i + @stdout.string.should match /cookbook save.+#{Regexp.escape(@file)}/i + end + + context 'with -f or --file' do + before do + @file = '/opt/chef/cookbooks/apache2.tar.gz' + @knife.config[:file] = @file + FileUtils.should_receive(:cp).with(@temp_file.path, @file) + end + + it 'should download the cookbook to the desired file' do + @knife.run + @stdout.string.should match /downloading apache2.+version.+#{Regexp.escape(@version)}/i + @stdout.string.should match /cookbook save.+#{Regexp.escape(@file)}/i + end + end + + it 'should provide an accessor to the version' do + FileUtils.stub(:cp).and_return(true) + @knife.version.should == @version + @knife.run + end + end + + context 'downloading a cookbook of a specific version' do + before do + @version = '1.0.1' + @version_us = @version.gsub '.', '_' + @cookbook_data = { 'version' => @version, + 'file' => "http://example.com/apache2_#{@version_us}.tgz" } + @temp_file = stub :path => "/tmp/apache2_#{@version_us}.tgz" + @file = File.join(Dir.pwd, "apache2-#{@version}.tar.gz") + @knife.name_args << @version + end + + it 'should download the desired version' do + @noauth_rest.should_receive(:get_rest). + with("#{@cookbook_api_url}/apache2/versions/#{@version_us}"). + and_return(@cookbook_data) + @noauth_rest.should_receive(:get_rest). + with(@cookbook_data['file'], true). + and_return(@temp_file) + FileUtils.should_receive(:cp).with(@temp_file.path, @file) + @knife.run + @stdout.string.should match /downloading apache2.+version.+#{Regexp.escape(@version)}/i + @stdout.string.should match /cookbook save.+#{Regexp.escape(@file)}/i + end + end + + end + + end + +end diff --git a/spec/unit/knife/cookbook_site_install_spec.rb b/spec/unit/knife/cookbook_site_install_spec.rb new file mode 100644 index 0000000000..2ec87b8d16 --- /dev/null +++ b/spec/unit/knife/cookbook_site_install_spec.rb @@ -0,0 +1,138 @@ +# +# Author:: Steven Danna (<steve@opscode.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 File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "spec_helper")) + +describe Chef::Knife::CookbookSiteInstall do + before(:each) do + require 'chef/knife/core/cookbook_scm_repo' + @knife = Chef::Knife::CookbookSiteInstall.new + @knife.config = {} + if Chef::Platform.windows? + @install_path = 'C:/tmp/chef' + else + @install_path = '/var/tmp/chef' + end + @knife.config[:cookbook_path] = [ @install_path ] + + @stdout = StringIO.new + @stderr = StringIO.new + @knife.stub!(:stderr).and_return(@stdout) + @knife.stub!(:stdout).and_return(@stdout) + + #Assume all external commands would have succeed. :( + File.stub!(:unlink) + File.stub!(:rmtree) + @knife.stub!(:shell_out!).and_return(true) + + #CookbookSiteDownload Stup + @downloader = {} + @knife.stub!(:download_cookbook_to).and_return(@downloader) + @downloader.stub!(:version).and_return do + if @knife.name_args.size == 2 + @knife.name_args[1] + else + "0.3.0" + end + end + + #Stubs for CookbookSCMRepo + @repo = stub(:sanity_check => true, :reset_to_default_state => true, + :prepare_to_import => true, :finalize_updates_to => true, + :merge_updates_from => true) + Chef::Knife::CookbookSCMRepo.stub!(:new).and_return(@repo) + end + + + describe "run" do + + it "should return an error if a cookbook name is not provided" do + @knife.name_args = [] + @knife.ui.should_receive(:error).with("Please specify a cookbook to download and install.") + lambda { @knife.run }.should raise_error(SystemExit) + end + + it "should return an error if more than two arguments are given" do + @knife.name_args = ["foo", "bar", "baz"] + @knife.ui.should_receive(:error).with("Installing multiple cookbooks at once is not supported.") + lambda { @knife.run }.should raise_error(SystemExit) + end + + it "should return an error if the second argument is not a version" do + @knife.name_args = ["getting-started", "1pass"] + @knife.ui.should_receive(:error).with("Installing multiple cookbooks at once is not supported.") + lambda { @knife.run }.should raise_error(SystemExit) + end + + it "should return an error if the second argument is a four-digit version" do + @knife.name_args = ["getting-started", "0.0.0.1"] + @knife.ui.should_receive(:error).with("Installing multiple cookbooks at once is not supported.") + lambda { @knife.run }.should raise_error(SystemExit) + end + + it "should return an error if the second argument is a one-digit version" do + @knife.name_args = ["getting-started", "1"] + @knife.ui.should_receive(:error).with("Installing multiple cookbooks at once is not supported.") + lambda { @knife.run }.should raise_error(SystemExit) + end + + + it "should install the specified version if second argument is a three-digit version" do + @knife.name_args = ["getting-started", "0.1.0"] + @knife.config[:no_deps] = true + upstream_file = File.join(@install_path, "getting-started.tar.gz") + @knife.should_receive(:download_cookbook_to).with(upstream_file) + @knife.should_receive(:extract_cookbook).with(upstream_file, "0.1.0") + @knife.should_receive(:clear_existing_files).with(File.join(@install_path, "getting-started")) + @repo.should_receive(:merge_updates_from).with("getting-started", "0.1.0") + @knife.run + end + + it "should install the specified version if second argument is a two-digit version" do + @knife.name_args = ["getting-started", "0.1"] + @knife.config[:no_deps] = true + upstream_file = File.join(@install_path, "getting-started.tar.gz") + @knife.should_receive(:download_cookbook_to).with(upstream_file) + @knife.should_receive(:extract_cookbook).with(upstream_file, "0.1") + @knife.should_receive(:clear_existing_files).with(File.join(@install_path, "getting-started")) + @repo.should_receive(:merge_updates_from).with("getting-started", "0.1") + @knife.run + end + + it "should install the latest version if only a cookbook name is given" do + @knife.name_args = ["getting-started"] + @knife.config[:no_deps] = true + upstream_file = File.join(@install_path, "getting-started.tar.gz") + @knife.should_receive(:download_cookbook_to).with(upstream_file) + @knife.should_receive(:extract_cookbook).with(upstream_file, "0.3.0") + @knife.should_receive(:clear_existing_files).with(File.join(@install_path, "getting-started")) + @repo.should_receive(:merge_updates_from).with("getting-started", "0.3.0") + @knife.run + end + + it "should not create/reset git branches if use_current_branch is set" do + @knife.name_args = ["getting-started"] + @knife.config[:use_current_branch] = true + @knife.config[:no_deps] = true + upstream_file = File.join(@install_path, "getting-started.tar.gz") + @repo.should_not_receive(:prepare_to_import) + @repo.should_not_receive(:reset_to_default_state) + @knife.run + end + end +end diff --git a/spec/unit/knife/cookbook_site_share_spec.rb b/spec/unit/knife/cookbook_site_share_spec.rb new file mode 100644 index 0000000000..3b912af0c5 --- /dev/null +++ b/spec/unit/knife/cookbook_site_share_spec.rb @@ -0,0 +1,146 @@ +# +# Author:: Stephen Delano (<stephen@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 'chef/cookbook_uploader' +require 'chef/cookbook_site_streaming_uploader' + +describe Chef::Knife::CookbookSiteShare do + + before(:each) do + @knife = Chef::Knife::CookbookSiteShare.new + @knife.name_args = ['cookbook_name', 'AwesomeSausage'] + + @cookbook = Chef::CookbookVersion.new('cookbook_name') + + @cookbook_loader = mock('Chef::CookbookLoader') + @cookbook_loader.stub!(:cookbook_exists?).and_return(true) + @cookbook_loader.stub!(:[]).and_return(@cookbook) + Chef::CookbookLoader.stub!(:new).and_return(@cookbook_loader) + + @cookbook_uploader = Chef::CookbookUploader.new('herpderp', File.join(CHEF_SPEC_DATA, 'cookbooks'), :rest => "norest") + Chef::CookbookUploader.stub!(:new).and_return(@cookbook_uploader) + @cookbook_uploader.stub!(:validate_cookbooks).and_return(true) + Chef::CookbookSiteStreamingUploader.stub!(:create_build_dir).and_return(Dir.mktmpdir) + + Chef::Mixin::Command.stub(:run_command).and_return(true) + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + describe 'run' do + + before(:each) do + @knife.stub!(:do_upload).and_return(true) + end + + it 'should should print usage and exit when given no arguments' do + @knife.name_args = [] + @knife.should_receive(:show_usage) + @knife.ui.should_receive(:fatal) + lambda { @knife.run }.should raise_error(SystemExit) + end + + it 'should print usage and exit when given only 1 argument' do + @knife.name_args = ['cookbook_name'] + @knife.should_receive(:show_usage) + @knife.ui.should_receive(:fatal) + lambda { @knife.run }.should raise_error(SystemExit) + end + + it 'should check if the cookbook exists' do + @cookbook_loader.should_receive(:cookbook_exists?) + @knife.run + end + + it "should exit and log to error if the cookbook doesn't exist" do + @cookbook_loader.stub(:cookbook_exists?).and_return(false) + @knife.ui.should_receive(:error) + lambda { @knife.run }.should raise_error(SystemExit) + end + + it 'should make a tarball of the cookbook' do + Chef::Mixin::Command.should_receive(:run_command) { |args| + args[:command].should match /tar -czf/ + } + @knife.run + end + + it 'should exit and log to error when the tarball creation fails' do + Chef::Mixin::Command.stub!(:run_command).and_raise(Chef::Exceptions::Exec) + @knife.ui.should_receive(:error) + lambda { @knife.run }.should raise_error(SystemExit) + end + + it 'should upload the cookbook and clean up the tarball' do + @knife.should_receive(:do_upload) + FileUtils.should_receive(:rm_rf) + @knife.run + end + end + + describe 'do_upload' do + + before(:each) do + @upload_response = mock('Net::HTTPResponse') + Chef::CookbookSiteStreamingUploader.stub!(:post).and_return(@upload_response) + + @stdout = StringIO.new + @stderr = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + @knife.ui.stub!(:stderr).and_return(@stderr) + File.stub(:open).and_return(true) + end + + it 'should post the cookbook to "http://cookbooks.opscode.com"' do + response_text = {:uri => 'http://cookbooks.opscode.com/cookbooks/cookbook_name'}.to_json + @upload_response.stub!(:body).and_return(response_text) + @upload_response.stub!(:code).and_return(201) + Chef::CookbookSiteStreamingUploader.should_receive(:post).with(/cookbooks\.opscode\.com/, anything(), anything(), anything()) + @knife.run + end + + it 'should alert the user when a version already exists' do + response_text = {:error_messages => ['Version already exists']}.to_json + @upload_response.stub!(:body).and_return(response_text) + @upload_response.stub!(:code).and_return(409) + lambda { @knife.run }.should raise_error(SystemExit) + @stderr.string.should match(/ERROR(.+)cookbook already exists/) + end + + it 'should pass any errors on to the user' do + response_text = {:error_messages => ["You're holding it wrong"]}.to_json + @upload_response.stub!(:body).and_return(response_text) + @upload_response.stub!(:code).and_return(403) + lambda { @knife.run }.should raise_error(SystemExit) + @stderr.string.should match("ERROR(.*)You're holding it wrong") + end + + it 'should print the body if no errors are exposed on failure' do + response_text = {:system_error => "Your call was dropped", :reason => "There's a map for that"}.to_json + @upload_response.stub!(:body).and_return(response_text) + @upload_response.stub!(:code).and_return(500) + @knife.ui.should_receive(:error).with(/#{Regexp.escape(response_text)}/)#.ordered + @knife.ui.should_receive(:error).with(/Unknown error/)#.ordered + lambda { @knife.run }.should raise_error(SystemExit) + end + + end + +end diff --git a/spec/unit/knife/cookbook_site_unshare_spec.rb b/spec/unit/knife/cookbook_site_unshare_spec.rb new file mode 100644 index 0000000000..ffba2ec664 --- /dev/null +++ b/spec/unit/knife/cookbook_site_unshare_spec.rb @@ -0,0 +1,77 @@ +# +# Author:: Stephen Delano (<stephen@opscode.com>) +# Author:: Tim Hinderliter (<tim@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' + +describe Chef::Knife::CookbookSiteUnshare do + + before(:each) do + @knife = Chef::Knife::CookbookSiteUnshare.new + @knife.name_args = ['cookbook_name'] + @knife.stub!(:confirm).and_return(true) + + @rest = mock('Chef::REST') + @rest.stub!(:delete_rest).and_return(true) + @knife.stub!(:rest).and_return(@rest) + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + describe 'run' do + + describe 'with no cookbook argument' do + it 'should print the usage and exit' do + @knife.name_args = [] + @knife.ui.should_receive(:fatal) + @knife.should_receive(:show_usage) + lambda { @knife.run }.should raise_error(SystemExit) + end + end + + it 'should confirm you want to unshare the cookbook' do + @knife.should_receive(:confirm) + @knife.run + end + + it 'should send a delete request to the cookbook site' do + @rest.should_receive(:delete_rest) + @knife.run + end + + it 'should log an error and exit when forbidden' do + exception = mock('403 "Forbidden"', :code => '403') + @rest.stub!(:delete_rest).and_raise(Net::HTTPServerException.new('403 "Forbidden"', exception)) + @knife.ui.should_receive(:error) + lambda { @knife.run }.should raise_error(SystemExit) + end + + it 'should re-raise any non-forbidden errors on delete_rest' do + exception = mock('500 "Application Error"', :code => '500') + @rest.stub(:delete_rest).and_raise(Net::HTTPServerException.new('500 "Application Error"', exception)) + lambda { @knife.run }.should raise_error(Net::HTTPServerException) + end + + it 'should log a success message' do + @knife.ui.should_receive(:info) + @knife.run + end + + end + +end diff --git a/spec/unit/knife/cookbook_test_spec.rb b/spec/unit/knife/cookbook_test_spec.rb new file mode 100644 index 0000000000..24c658dc6c --- /dev/null +++ b/spec/unit/knife/cookbook_test_spec.rb @@ -0,0 +1,84 @@ +# +# Author:: Stephen Delano (<stephen@opscode.com>)$ +# Author:: Matthew Kent (<mkent@magoazul.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc.$ +# Copyright:: Copyright (c) 2010 Matthew Kent +# 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' +Chef::Knife::CookbookTest.load_deps + +describe Chef::Knife::CookbookTest do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::CookbookTest.new + @knife.config[:cookbook_path] = File.join(CHEF_SPEC_DATA,'cookbooks') + @knife.cookbook_loader.stub!(:cookbook_exists?).and_return(true) + @cookbooks = [] + %w{tats central_market jimmy_johns pho}.each do |cookbook_name| + @cookbooks << Chef::CookbookVersion.new(cookbook_name) + end + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + describe "run" do + it "should test the cookbook" do + @knife.stub!(:test_cookbook).and_return(true) + @knife.name_args = ["italian"] + @knife.should_receive(:test_cookbook).with("italian") + @knife.run + end + + it "should test multiple cookbooks when provided" do + @knife.stub!(:test_cookbook).and_return(true) + @knife.name_args = ["tats", "jimmy_johns"] + @knife.should_receive(:test_cookbook).with("tats") + @knife.should_receive(:test_cookbook).with("jimmy_johns") + @knife.should_not_receive(:test_cookbook).with("central_market") + @knife.should_not_receive(:test_cookbook).with("pho") + @knife.run + end + + it "should test both ruby and templates" do + @knife.name_args = ["example"] + @knife.config[:cookbook_path].should_not be_empty + Array(@knife.config[:cookbook_path]).reverse.each do |path| + @knife.should_receive(:test_ruby).with(an_instance_of(Chef::Cookbook::SyntaxCheck)) + @knife.should_receive(:test_templates).with(an_instance_of(Chef::Cookbook::SyntaxCheck)) + end + @knife.run + end + + describe "with -a or --all" do + it "should test all of the cookbooks" do + @knife.stub!(:test_cookbook).and_return(true) + @knife.config[:all] = true + @loader = {} + @loader.stub!(:load_cookbooks).and_return(@loader) + @cookbooks.each do |cookbook| + @loader[cookbook.name] = cookbook + end + @knife.stub!(:cookbook_loader).and_return(@loader) + @loader.each do |key, cookbook| + @knife.should_receive(:test_cookbook).with(cookbook.name) + end + @knife.run + end + end + + end +end diff --git a/spec/unit/knife/cookbook_upload_spec.rb b/spec/unit/knife/cookbook_upload_spec.rb new file mode 100644 index 0000000000..4659e60371 --- /dev/null +++ b/spec/unit/knife/cookbook_upload_spec.rb @@ -0,0 +1,183 @@ +# +# Author:: Matthew Kent (<mkent@magoazul.com>) +# Author:: Steven Danna (<steve@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 File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "spec_helper")) + +require 'chef/cookbook_uploader' +require 'timeout' + +describe Chef::Knife::CookbookUpload do + before(:each) do + @knife = Chef::Knife::CookbookUpload.new + @knife.name_args = ['test_cookbook'] + + @cookbook = Chef::CookbookVersion.new('test_cookbook') + + @cookbook_loader = {} + @cookbook_loader.stub!(:[]).and_return(@cookbook) + @cookbook_loader.stub!(:merged_cookbooks).and_return([]) + @cookbook_loader.stub!(:load_cookbooks).and_return(@cookbook_loader) + Chef::CookbookLoader.stub!(:new).and_return(@cookbook_loader) + + @output = StringIO.new + @knife.ui.stub!(:stdout).and_return(@output) + @knife.ui.stub!(:stderr).and_return(@output) + end + + describe 'run' do + before(:each) do + @knife.stub!(:upload).and_return(true) + Chef::CookbookVersion.stub(:list_all_versions).and_return({}) + end + + it 'should print usage and exit when a cookbook name is not provided' do + @knife.name_args = [] + @knife.should_receive(:show_usage) + @knife.ui.should_receive(:fatal) + lambda { @knife.run }.should raise_error(SystemExit) + end + + describe 'when specifying a cookbook name' do + it 'should upload the cookbook' do + @knife.should_receive(:upload).once + @knife.run + end + + it 'should report on success' do + @knife.should_receive(:upload).once + @knife.ui.should_receive(:info).with(/Uploaded 1 cookbook/) + @knife.run + end + end + + describe 'when specifying the same cookbook name twice' do + it 'should upload the cookbook only once' do + @knife.name_args = ['test_cookbook', 'test_cookbook'] + @knife.should_receive(:upload).once + @knife.run + end + end + + describe 'when specifying a cookbook name among many' do + before(:each) do + @knife.name_args = ['test_cookbook1'] + @cookbooks = { + 'test_cookbook1' => Chef::CookbookVersion.new('test_cookbook1'), + 'test_cookbook2' => Chef::CookbookVersion.new('test_cookbook2'), + 'test_cookbook3' => Chef::CookbookVersion.new('test_cookbook3') + } + @cookbook_loader = {} + @cookbook_loader.stub!(:merged_cookbooks).and_return([]) + @cookbook_loader.stub(:[]) { |ckbk| @cookbooks[ckbk] } + Chef::CookbookLoader.stub!(:new).and_return(@cookbook_loader) + end + + it "should read only one cookbook" do + @cookbook_loader.should_receive(:[]).once.with('test_cookbook1') + @knife.run + end + + it "should not read all cookbooks" do + @cookbook_loader.should_not_receive(:load_cookbooks) + @knife.run + end + + it "should upload only one cookbook" do + @knife.should_receive(:upload).exactly(1).times + @knife.run + end + end + + # This is testing too much. We should break it up. + describe 'when specifying a cookbook name with dependencies' do + it "should upload all dependencies once" do + @knife.name_args = ["test_cookbook2"] + @knife.config[:depends] = true + @test_cookbook1 = Chef::CookbookVersion.new('test_cookbook1') + @test_cookbook2 = Chef::CookbookVersion.new('test_cookbook2') + @test_cookbook3 = Chef::CookbookVersion.new('test_cookbook3') + @test_cookbook2.metadata.depends("test_cookbook3") + @test_cookbook3.metadata.depends("test_cookbook1") + @test_cookbook3.metadata.depends("test_cookbook2") + @cookbook_loader.stub!(:[]) do |ckbk| + { "test_cookbook1" => @test_cookbook1, + "test_cookbook2" => @test_cookbook2, + "test_cookbook3" => @test_cookbook3 }[ckbk] + end + @knife.stub!(:cookbook_names).and_return(["test_cookbook1", "test_cookbook2", "test_cookbook3"]) + @knife.should_receive(:upload).exactly(3).times + Timeout::timeout(5) do + @knife.run + end.should_not raise_error(Timeout::Error) + end + end + + it "should freeze the version of the cookbooks if --freeze is specified" do + @knife.config[:freeze] = true + @cookbook.should_receive(:freeze_version).once + @knife.run + end + + describe 'with -a or --all' do + before(:each) do + @knife.config[:all] = true + @test_cookbook1 = Chef::CookbookVersion.new('test_cookbook1') + @test_cookbook2 = Chef::CookbookVersion.new('test_cookbook2') + @cookbook_loader.stub!(:each).and_yield("test_cookbook1", @test_cookbook1).and_yield("test_cookbook2", @test_cookbook2) + @cookbook_loader.stub!(:cookbook_names).and_return(["test_cookbook1", "test_cookbook2"]) + end + + it 'should upload all cookbooks' do + @knife.should_receive(:upload).once + @knife.run + end + + it 'should report on success' do + @knife.should_receive(:upload).once + @knife.ui.should_receive(:info).with(/Uploaded all cookbooks/) + @knife.run + end + + it 'should update the version constraints for an environment' do + @knife.stub!(:assert_environment_valid!).and_return(true) + @knife.config[:environment] = "production" + @knife.should_receive(:update_version_constraints).once + @knife.run + end + end + + describe 'when a frozen cookbook exists on the server' do + it 'should fail to replace it' do + @knife.stub!(:upload).and_raise(Chef::Exceptions::CookbookFrozen) + @knife.ui.should_receive(:error).with(/Failed to upload 1 cookbook/) + lambda { @knife.run }.should raise_error(SystemExit) + end + + it 'should not update the version constraints for an environment' do + @knife.stub!(:assert_environment_valid!).and_return(true) + @knife.config[:environment] = "production" + @knife.stub!(:upload).and_raise(Chef::Exceptions::CookbookFrozen) + @knife.ui.should_receive(:error).with(/Failed to upload 1 cookbook/) + @knife.ui.should_receive(:warn).with(/Not updating version constraints/) + @knife.should_not_receive(:update_version_constraints) + lambda { @knife.run }.should raise_error(SystemExit) + end + end + end # run +end # Chef::Knife::CookbookUpload diff --git a/spec/unit/knife/core/bootstrap_context_spec.rb b/spec/unit/knife/core/bootstrap_context_spec.rb new file mode 100644 index 0000000000..f8a58484a5 --- /dev/null +++ b/spec/unit/knife/core/bootstrap_context_spec.rb @@ -0,0 +1,128 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.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' +require 'chef/knife/core/bootstrap_context' + +describe Chef::Knife::Core::BootstrapContext do + before do + @config = {:foo => :bar} + @run_list = Chef::RunList.new('recipe[tmux]', 'role[base]') + @chef_config = {:validation_key => File.join(CHEF_SPEC_DATA, 'ssl', 'private_key.pem')} + @chef_config[:chef_server_url] = 'http://chef.example.com:4444' + @chef_config[:validation_client_name] = 'chef-validator-testing' + @context = Chef::Knife::Core::BootstrapContext.new(@config, @run_list, @chef_config) + end + + describe "to support compatability with existing templates" do + it "sets the @config instance variable" do + @context.instance_eval { @config }.should == {:foo => :bar} + end + + it "sets the @run_list instance variable" do + @context.instance_eval { @run_list }.should equal(@run_list) + end + end + + it "installs the same version of chef on the remote host" do + @context.bootstrap_version_string.should == "--version #{Chef::VERSION}" + end + + it "runs chef with the first-boot.json in the _default environment" do + @context.start_chef.should == "chef-client -j /etc/chef/first-boot.json -E _default" + end + + it "it runs chef-client from another path when specified" do + @chef_config[:chef_client_path] = '/usr/local/bin/chef-client' + @context.start_chef.should == "/usr/local/bin/chef-client -j /etc/chef/first-boot.json -E _default" + end + + it "reads the validation key" do + @context.validation_key.should == IO.read(File.join(CHEF_SPEC_DATA, 'ssl', 'private_key.pem')) + end + + it "generates the config file data" do + expected=<<-EXPECTED +log_level :info +log_location STDOUT +chef_server_url "http://chef.example.com:4444" +validation_client_name "chef-validator-testing" +# Using default node name (fqdn) +EXPECTED + @context.config_content.should == expected + end + + describe "when an explicit node name is given" do + before do + @config[:chef_node_name] = 'foobar.example.com' + end + it "sets the node name in the client.rb" do + @context.config_content.should match(/node_name "foobar\.example\.com"/) + end + end + + describe "when bootstrapping into a specific environment" do + before do + @chef_config[:environment] = "prodtastic" + end + + it "starts chef in the configured environment" do + @context.start_chef.should == 'chef-client -j /etc/chef/first-boot.json -E prodtastic' + end + end + + describe "when installing a prerelease version of chef" do + before do + @config[:prerelease] = true + end + it "supplies --prerelease as the version string" do + @context.bootstrap_version_string.should == '--prerelease' + end + end + + describe "when installing an explicit version of chef" do + before do + @context = Chef::Knife::Core::BootstrapContext.new(@config, @run_list, :knife => { :bootstrap_version => '123.45.678' }) + end + + it "gives --version $VERSION as the version string" do + @context.bootstrap_version_string.should == '--version 123.45.678' + end + end + + describe "when JSON attributes are given" do + before do + conf = @config.dup + conf[:first_boot_attributes] = {:baz => :quux} + @context = Chef::Knife::Core::BootstrapContext.new(conf, @run_list, @chef_config) + end + + it "adds the attributes to first_boot" do + @context.first_boot.to_json.should == {:baz => :quux, :run_list => @run_list}.to_json + end + end + + describe "when JSON attributes are NOT given" do + it "sets first_boot equal to run_list" do + @context.first_boot.to_json.should == {:run_list => @run_list}.to_json + end + end + + +end + diff --git a/spec/unit/knife/core/cookbook_scm_repo_spec.rb b/spec/unit/knife/core/cookbook_scm_repo_spec.rb new file mode 100644 index 0000000000..629164ad0a --- /dev/null +++ b/spec/unit/knife/core/cookbook_scm_repo_spec.rb @@ -0,0 +1,187 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.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' +require 'chef/knife/core/cookbook_scm_repo' + +describe Chef::Knife::CookbookSCMRepo do + before do + @repo_path = File.join(CHEF_SPEC_DATA, 'cookbooks') + @stdout, @stderr, @stdin = StringIO.new, StringIO.new, StringIO.new + @ui = Chef::Knife::UI.new(@stdout, @stderr, @stdin, {}) + @cookbook_repo = Chef::Knife::CookbookSCMRepo.new(@repo_path, @ui, :default_branch => 'master') + + @branch_list = Mixlib::ShellOut.new + @branch_list.stdout.replace(<<-BRANCHES) + chef-vendor-apache2 + chef-vendor-build-essential + chef-vendor-dynomite + chef-vendor-ganglia + chef-vendor-graphite + chef-vendor-python + chef-vendor-absent-new +BRANCHES + end + + it "has a path to the cookbook repo" do + @cookbook_repo.repo_path.should == @repo_path + end + + it "has a default branch" do + @cookbook_repo.default_branch.should == 'master' + end + + describe "when sanity checking the repo" do + it "exits when the directory does not exist" do + ::File.should_receive(:directory?).with(@repo_path).and_return(false) + lambda {@cookbook_repo.sanity_check}.should raise_error(SystemExit) + end + + describe "and the repo dir exists" do + before do + ::File.stub!(:directory?).with(@repo_path).and_return(true) + end + + it "exits when there is no git repo" do + ::File.stub!(:directory?).with(/.*\.git/).and_return(false) + lambda {@cookbook_repo.sanity_check}.should raise_error(SystemExit) + end + + describe "and the repo is a git repo" do + before do + ::File.stub!(:directory?).with(File.join(@repo_path, '.git')).and_return(true) + end + + it "exits when the default branch doesn't exist" do + @nobranches = Mixlib::ShellOut.new.tap {|s|s.stdout.replace "\n"} + @cookbook_repo.should_receive(:shell_out!).with('git branch --no-color', :cwd => @repo_path).and_return(@nobranches) + lambda {@cookbook_repo.sanity_check}.should raise_error(SystemExit) + end + + describe "and the default branch exists" do + before do + @master_branch = Mixlib::ShellOut.new + @master_branch.stdout.replace "* master\n" + @cookbook_repo.should_receive(:shell_out!).with("git branch --no-color", :cwd => @repo_path).and_return(@master_branch) + end + + it "exits when the git repo is dirty" do + @dirty_status = Mixlib::ShellOut.new + @dirty_status.stdout.replace(<<-DIRTY) + M chef/lib/chef/knife/cookbook_site_vendor.rb +DIRTY + @cookbook_repo.should_receive(:shell_out!).with('git status --porcelain', :cwd => @repo_path).and_return(@dirty_status) + lambda {@cookbook_repo.sanity_check}.should raise_error(SystemExit) + end + + describe "and the repo is clean" do + before do + @clean_status = Mixlib::ShellOut.new.tap {|s| s.stdout.replace("\n")} + @cookbook_repo.stub!(:shell_out!).with('git status --porcelain', :cwd => @repo_path).and_return(@clean_status) + end + + it "passes the sanity check" do + @cookbook_repo.sanity_check + end + + end + end + end + end + end + + it "resets to default state by checking out the default branch" do + @cookbook_repo.should_receive(:shell_out!).with('git checkout master', :cwd => @repo_path) + @cookbook_repo.reset_to_default_state + end + + it "determines if a the pristine copy branch exists" do + @cookbook_repo.should_receive(:shell_out!).with('git branch --no-color', :cwd => @repo_path).and_return(@branch_list) + @cookbook_repo.branch_exists?("chef-vendor-apache2").should be_true + @cookbook_repo.should_receive(:shell_out!).with('git branch --no-color', :cwd => @repo_path).and_return(@branch_list) + @cookbook_repo.branch_exists?("chef-vendor-nginx").should be_false + end + + it "determines if a the branch not exists correctly without substring search" do + @cookbook_repo.should_receive(:shell_out!).twice.with('git branch --no-color', :cwd => @repo_path).and_return(@branch_list) + @cookbook_repo.should_not be_branch_exists("chef-vendor-absent") + @cookbook_repo.should be_branch_exists("chef-vendor-absent-new") + end + + describe "when the pristine copy branch does not exist" do + it "prepares for import by creating the pristine copy branch" do + @cookbook_repo.should_receive(:shell_out!).with('git branch --no-color', :cwd => @repo_path).and_return(@branch_list) + @cookbook_repo.should_receive(:shell_out!).with('git checkout -b chef-vendor-nginx', :cwd => @repo_path) + @cookbook_repo.prepare_to_import("nginx") + end + end + + describe "when the pristine copy branch does exist" do + it "prepares for import by checking out the pristine copy branch" do + @cookbook_repo.should_receive(:shell_out!).with('git branch --no-color', :cwd => @repo_path).and_return(@branch_list) + @cookbook_repo.should_receive(:shell_out!).with('git checkout chef-vendor-apache2', :cwd => @repo_path) + @cookbook_repo.prepare_to_import("apache2") + end + end + + describe "when the pristine copy branch was not updated by the changes" do + before do + @updates = Mixlib::ShellOut.new + @updates.stdout.replace("\n") + @cookbook_repo.stub!(:shell_out!).with('git status --porcelain -- apache2', :cwd => @repo_path).and_return(@updates) + end + + it "shows no changes in the pristine copy" do + @cookbook_repo.updated?('apache2').should be_false + end + + it "does nothing to finalize the updates" do + @cookbook_repo.finalize_updates_to('apache2', '1.2.3').should be_false + end + end + + describe "when the pristine copy branch was updated by the changes" do + before do + @updates = Mixlib::ShellOut.new + @updates.stdout.replace(" M cookbooks/apache2/recipes/default.rb\n") + @cookbook_repo.stub!(:shell_out!).with('git status --porcelain -- apache2', :cwd => @repo_path).and_return(@updates) + end + + it "shows changes in the pristine copy" do + @cookbook_repo.updated?('apache2').should be_true + end + + it "commits the changes to the repo and tags the commit" do + @cookbook_repo.should_receive(:shell_out!).with("git add apache2", :cwd => @repo_path) + @cookbook_repo.should_receive(:shell_out!).with("git commit -m \"Import apache2 version 1.2.3\" -- apache2", :cwd => @repo_path) + @cookbook_repo.should_receive(:shell_out!).with("git tag -f cookbook-site-imported-apache2-1.2.3", :cwd => @repo_path) + @cookbook_repo.finalize_updates_to("apache2", "1.2.3").should be_true + end + end + + describe "when a custom default branch is specified" do + before do + @cookbook_repo = Chef::Knife::CookbookSCMRepo.new(@repo_path, @ui, :default_branch => 'develop') + end + + it "resets to default state by checking out the default branch" do + @cookbook_repo.should_receive(:shell_out!).with('git checkout develop', :cwd => @repo_path) + @cookbook_repo.reset_to_default_state + end + end +end diff --git a/spec/unit/knife/core/object_loader_spec.rb b/spec/unit/knife/core/object_loader_spec.rb new file mode 100644 index 0000000000..b3456e2b15 --- /dev/null +++ b/spec/unit/knife/core/object_loader_spec.rb @@ -0,0 +1,81 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Author:: Juanje Ojeda (<juanje.ojeda@gmail.com>) +# Copyright:: Copyright (c) 2011-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 'chef/knife/core/object_loader' + +describe Chef::Knife::Core::ObjectLoader do + before(:each) do + @knife = Chef::Knife.new + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + Dir.chdir(File.join(CHEF_SPEC_DATA, 'object_loader')) + end + + shared_examples_for "Chef object" do |chef_class| + it "should create a #{chef_class} object" do + @object.should be_a_kind_of(chef_class) + end + + it "should has a attribute 'name'" do + @object.name.should eql('test') + end + end + + { + 'nodes' => Chef::Node, + 'roles' => Chef::Role, + 'environments' => Chef::Environment + }.each do |repo_location, chef_class| + + describe "when the file is a #{chef_class}" do + before do + @loader = Chef::Knife::Core::ObjectLoader.new(chef_class, @knife.ui) + end + + describe "when the file is a Ruby" do + before do + @object = @loader.load_from(repo_location, 'test.rb') + end + + it_behaves_like "Chef object", chef_class + end + + #NOTE: This is check for the bug described at CHEF-2352 + describe "when the file is a JSON" do + describe "and it has defined 'json_class'" do + before do + @object = @loader.load_from(repo_location, 'test_json_class.json') + end + + it_behaves_like "Chef object", chef_class + end + + describe "and it has not defined 'json_class'" do + before do + @object = @loader.load_from(repo_location, 'test.json') + end + + it_behaves_like "Chef object", chef_class + end + end + end + end + +end diff --git a/spec/unit/knife/core/subcommand_loader_spec.rb b/spec/unit/knife/core/subcommand_loader_spec.rb new file mode 100644 index 0000000000..e39e0be041 --- /dev/null +++ b/spec/unit/knife/core/subcommand_loader_spec.rb @@ -0,0 +1,54 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.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::Knife::SubcommandLoader do + before do + @home = File.join(CHEF_SPEC_DATA, 'knife-home') + @env = {'HOME' => @home} + @loader = Chef::Knife::SubcommandLoader.new(File.join(CHEF_SPEC_DATA, 'knife-site-subcommands'), @env) + end + + it "builds a list of the core subcommand file require paths" do + @loader.subcommand_files.should_not be_empty + @loader.subcommand_files.each do |require_path| + require_path.should match(/chef\/knife\/.*|plugins\/knife\/.*/) + end + end + + it "finds files installed via rubygems" do + @loader.find_subcommands_via_rubygems.should include('chef/knife/node_create') + @loader.find_subcommands_via_rubygems.each {|rel_path, abs_path| abs_path.should match(%r[chef/knife/.+])} + end + + it "finds files using a dirglob when rubygems is not available" do + @loader.find_subcommands_via_dirglob.should include('chef/knife/node_create') + @loader.find_subcommands_via_dirglob.each {|rel_path, abs_path| abs_path.should match(%r[chef/knife/.+])} + end + + it "finds user-specific subcommands in the user's ~/.chef directory" do + expected_command = File.join(@home, '.chef', 'plugins', 'knife', 'example_home_subcommand.rb') + @loader.site_subcommands.should include(expected_command) + end + + it "finds repo specific subcommands by searching for a .chef directory" do + expected_command = File.join(CHEF_SPEC_DATA, 'knife-site-subcommands', 'plugins', 'knife', 'example_subcommand.rb') + @loader.site_subcommands.should include(expected_command) + end +end diff --git a/spec/unit/knife/core/ui_spec.rb b/spec/unit/knife/core/ui_spec.rb new file mode 100644 index 0000000000..784ad1f0d7 --- /dev/null +++ b/spec/unit/knife/core/ui_spec.rb @@ -0,0 +1,309 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Tim Hinderliter (<tim@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2008, 2011, 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::Knife::UI do + before do + @out, @err, @in = StringIO.new, StringIO.new, StringIO.new + @config = {} + @ui = Chef::Knife::UI.new(@out, @err, @in, @config) + end + + describe "format_list_for_display" do + it "should print the full hash if --with-uri is true" do + @ui.config[:with_uri] = true + @ui.format_list_for_display({ :marcy => :playground }).should == { :marcy => :playground } + end + + it "should print only the keys if --with-uri is false" do + @ui.config[:with_uri] = false + @ui.format_list_for_display({ :marcy => :playground }).should == [ :marcy ] + end + end + + describe "output" do + it "formats strings appropriately" do + @ui.output("hi") + @out.string.should == "hi\n" + end + + it "formats hashes appropriately" do + @ui.output({'hi' => 'a', 'lo' => 'b' }) + @out.string.should == <<EOM +hi: a +lo: b +EOM + end + + it "formats empty hashes appropriately" do + @ui.output({}) + @out.string.should == "\n" + end + + it "formats arrays appropriately" do + @ui.output([ 'a', 'b' ]) + @out.string.should == <<EOM +a +b +EOM + end + + it "formats empty arrays appropriately" do + @ui.output([ ]) + @out.string.should == "\n" + end + + it "formats single-member arrays appropriately" do + @ui.output([ 'a' ]) + @out.string.should == "a\n" + end + + it "formats nested single-member arrays appropriately" do + @ui.output([ [ 'a' ] ]) + @out.string.should == "a\n" + end + + it "formats nested arrays appropriately" do + @ui.output([ [ 'a', 'b' ], [ 'c', 'd' ]]) + @out.string.should == <<EOM +a +b + +c +d +EOM + end + + it "formats nested arrays with single- and empty subarrays appropriately" do + @ui.output([ [ 'a', 'b' ], [ 'c' ], [], [ 'd', 'e' ]]) + @out.string.should == <<EOM +a +b + +c + + +d +e +EOM + end + + it "formats arrays of hashes with extra lines in between for readability" do + @ui.output([ { 'a' => 'b', 'c' => 'd' }, { 'x' => 'y' }, { 'm' => 'n', 'o' => 'p' }]) + @out.string.should == <<EOM +a: b +c: d + +x: y + +m: n +o: p +EOM + end + + it "formats hashes with empty array members appropriately" do + @ui.output({ 'a' => [], 'b' => 'c' }) + @out.string.should == <<EOM +a: +b: c +EOM + end + + it "formats hashes with single-member array values appropriately" do + @ui.output({ 'a' => [ 'foo' ], 'b' => 'c' }) + @out.string.should == <<EOM +a: foo +b: c +EOM + end + + it "formats hashes with array members appropriately" do + @ui.output({ 'a' => [ 'foo', 'bar' ], 'b' => 'c' }) + @out.string.should == <<EOM +a: + foo + bar +b: c +EOM + end + + it "formats hashes with single-member nested array values appropriately" do + @ui.output({ 'a' => [ [ 'foo' ] ], 'b' => 'c' }) + @out.string.should == <<EOM +a: + foo +b: c +EOM + end + + it "formats hashes with nested array values appropriately" do + @ui.output({ 'a' => [ [ 'foo', 'bar' ], [ 'baz', 'bjork' ] ], 'b' => 'c' }) + @out.string.should == <<EOM +a: + foo + bar + + baz + bjork +b: c +EOM + end + + it "formats hashes with hash values appropriately" do + @ui.output({ 'a' => { 'aa' => 'bb', 'cc' => 'dd' }, 'b' => 'c' }) + @out.string.should == <<EOM +a: + aa: bb + cc: dd +b: c +EOM + end + + it "formats hashes with empty hash values appropriately" do + @ui.output({ 'a' => { }, 'b' => 'c' }) + @out.string.should == <<EOM +a: +b: c +EOM + end + end + + describe "format_for_display" do + it "should return the raw data" do + input = { :gi => :go } + @ui.format_for_display(input).should == input + end + + describe "with --attribute passed" do + it "should return the deeply nested attribute" do + input = { "gi" => { "go" => "ge" }, "id" => "sample-data-bag-item" } + @ui.config[:attribute] = "gi.go" + @ui.format_for_display(input).should == { "sample-data-bag-item" => { "gi.go" => "ge" } } + end + end + + describe "with --run-list passed" do + it "should return the run list" do + input = Chef::Node.new + input.name("sample-node") + input.run_list("role[monkey]", "role[churchmouse]") + @ui.config[:run_list] = true + response = @ui.format_for_display(input) + response["sample-node"]["run_list"][0].should == "role[monkey]" + response["sample-node"]["run_list"][1].should == "role[churchmouse]" + end + end + end + + describe "format_cookbook_list_for_display" do + before(:each) do + @item = { + "cookbook_name" => { + "url" => "http://url/cookbooks/cookbook", + "versions" => [ + { "version" => "3.0.0", "url" => "http://url/cookbooks/3.0.0" }, + { "version" => "2.0.0", "url" => "http://url/cookbooks/2.0.0" }, + { "version" => "1.0.0", "url" => "http://url/cookbooks/1.0.0" } + ] + } + } + end + + it "should return an array of the cookbooks with versions" do + expected_response = [ "cookbook_name 3.0.0 2.0.0 1.0.0" ] + response = @ui.format_cookbook_list_for_display(@item) + response.should == expected_response + end + + describe "with --with-uri" do + it "should return the URIs" do + response = { + "cookbook_name"=>{ + "1.0.0" => "http://url/cookbooks/1.0.0", + "2.0.0" => "http://url/cookbooks/2.0.0", + "3.0.0" => "http://url/cookbooks/3.0.0"} + } + @ui.config[:with_uri] = true + @ui.format_cookbook_list_for_display(@item).should == response + end + end + end + + describe "confirm" do + before(:each) do + @question = "monkeys rule" + @stdout = StringIO.new + @ui.stub(:stdout).and_return(@stdout) + @ui.stdin.stub!(:readline).and_return("y") + end + + it "should return true if you answer Y" do + @ui.stdin.stub!(:readline).and_return("Y") + @ui.confirm(@question).should == true + end + + it "should return true if you answer y" do + @ui.stdin.stub!(:readline).and_return("y") + @ui.confirm(@question).should == true + end + + it "should exit 3 if you answer N" do + @ui.stdin.stub!(:readline).and_return("N") + lambda { + @ui.confirm(@question) + }.should raise_error(SystemExit) { |e| e.status.should == 3 } + end + + it "should exit 3 if you answer n" do + @ui.stdin.stub!(:readline).and_return("n") + lambda { + @ui.confirm(@question) + }.should raise_error(SystemExit) { |e| e.status.should == 3 } + end + + describe "with --y or --yes passed" do + it "should return true" do + @ui.config[:yes] = true + @ui.confirm(@question).should == true + end + end + + describe "when asking for free-form user input" do + it "asks a question and returns the answer provided by the user" do + out = StringIO.new + @ui.stub!(:stdout).and_return(out) + @ui.stub!(:stdin).and_return(StringIO.new("http://mychefserver.example.com\n")) + @ui.ask_question("your chef server URL?").should == "http://mychefserver.example.com" + out.string.should == "your chef server URL?" + end + + it "suggests a default setting and returns the default when the user's response only contains whitespace" do + out = StringIO.new + @ui.stub!(:stdout).and_return(out) + @ui.stub!(:stdin).and_return(StringIO.new(" \n")) + @ui.ask_question("your chef server URL? ", :default => 'http://localhost:4000').should == "http://localhost:4000" + out.string.should == "your chef server URL? [http://localhost:4000] " + end + end + + end +end diff --git a/spec/unit/knife/data_bag_create_spec.rb b/spec/unit/knife/data_bag_create_spec.rb new file mode 100644 index 0000000000..7d9433984f --- /dev/null +++ b/spec/unit/knife/data_bag_create_spec.rb @@ -0,0 +1,105 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright (c) 2009-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 'tempfile' + +module ChefSpecs + class ChefRest + attr_reader :args_received + def initialize + @args_received = [] + end + + def post_rest(*args) + @args_received << args + end + end +end + + +describe Chef::Knife::DataBagCreate do + before do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::DataBagCreate.new + @rest = ChefSpecs::ChefRest.new + @knife.stub!(:rest).and_return(@rest) + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + + it "creates a data bag when given one argument" do + @knife.name_args = ['sudoing_admins'] + @rest.should_receive(:post_rest).with("data", {"name" => "sudoing_admins"}) + @knife.ui.should_receive(:info).with("Created data_bag[sudoing_admins]") + + @knife.run + end + + it "creates a data bag item when given two arguments" do + @knife.name_args = ['sudoing_admins', 'ME'] + user_supplied_hash = {"login_name" => "alphaomega", "id" => "ME"} + data_bag_item = Chef::DataBagItem.from_hash(user_supplied_hash) + data_bag_item.data_bag("sudoing_admins") + @knife.should_receive(:create_object).and_yield(user_supplied_hash) + @rest.should_receive(:post_rest).with("data", {'name' => 'sudoing_admins'}).ordered + @rest.should_receive(:post_rest).with("data/sudoing_admins", data_bag_item).ordered + + @knife.run + end + + describe "encrypted data bag items" do + before(:each) do + @secret = "abc123SECRET" + @plain_data = {"login_name" => "alphaomega", "id" => "ME"} + @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data, + @secret) + @knife.name_args = ['sudoing_admins', 'ME'] + @knife.should_receive(:create_object).and_yield(@plain_data) + data_bag_item = Chef::DataBagItem.from_hash(@enc_data) + data_bag_item.data_bag("sudoing_admins") + @rest.should_receive(:post_rest).with("data", {'name' => 'sudoing_admins'}).ordered + @rest.should_receive(:post_rest).with("data/sudoing_admins", data_bag_item).ordered + + @secret_file = Tempfile.new("encrypted_data_bag_secret_file_test") + @secret_file.puts(@secret) + @secret_file.flush + end + + after do + @secret_file.close + @secret_file.unlink + end + + it "creates an encrypted data bag item via --secret" do + @knife.stub!(:config).and_return({:secret => @secret}) + @knife.run + end + + it "creates an encrypted data bag item via --secret_file" do + secret_file = Tempfile.new("encrypted_data_bag_secret_file_test") + secret_file.puts(@secret) + secret_file.flush + @knife.stub!(:config).and_return({:secret_file => secret_file.path}) + @knife.run + end + end + +end diff --git a/spec/unit/knife/data_bag_edit_spec.rb b/spec/unit/knife/data_bag_edit_spec.rb new file mode 100644 index 0000000000..572722541a --- /dev/null +++ b/spec/unit/knife/data_bag_edit_spec.rb @@ -0,0 +1,89 @@ +# +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright 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 'tempfile' + +describe Chef::Knife::DataBagEdit do + before do + @plain_data = {"login_name" => "alphaomega", "id" => "item_name"} + @edited_data = { + "login_name" => "rho", "id" => "item_name", + "new_key" => "new_value" } + + Chef::Config[:node_name] = "webmonkey.example.com" + + @knife = Chef::Knife::DataBagEdit.new + @rest = mock('chef-rest-mock') + @knife.stub!(:rest).and_return(@rest) + + @stdout = StringIO.new + @knife.stub!(:stdout).and_return(@stdout) + @log = Chef::Log + @knife.name_args = ['bag_name', 'item_name'] + end + + it "requires data bag and item arguments" do + @knife.name_args = [] + lambda { @knife.run }.should raise_error(SystemExit) + @stdout.string.should match(/^You must supply the data bag and an item to edit/) + end + + it "saves edits on a data bag item" do + Chef::DataBagItem.stub!(:load).with('bag_name', 'item_name').and_return(@plain_data) + @knife.should_receive(:edit_data).with(@plain_data).and_return(@edited_data) + @rest.should_receive(:put_rest).with("data/bag_name/item_name", @edited_data).ordered + @knife.run + end + + describe "encrypted data bag items" do + before(:each) do + @secret = "abc123SECRET" + @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data, + @secret) + @enc_edited_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@edited_data, + @secret) + Chef::DataBagItem.stub!(:load).with('bag_name', 'item_name').and_return(@enc_data) + + @secret_file = Tempfile.new("encrypted_data_bag_secret_file_test") + @secret_file.puts(@secret) + @secret_file.flush + end + + after do + @secret_file.close + @secret_file.unlink + end + + it "decrypts and encrypts via --secret" do + @knife.stub!(:config).and_return({:secret => @secret}) + @knife.should_receive(:edit_data).with(@plain_data).and_return(@edited_data) + @rest.should_receive(:put_rest).with("data/bag_name/item_name", @enc_edited_data).ordered + + @knife.run + end + + it "decrypts and encrypts via --secret_file" do + @knife.stub!(:config).and_return({:secret_file => @secret_file.path}) + @knife.should_receive(:edit_data).with(@plain_data).and_return(@edited_data) + @rest.should_receive(:put_rest).with("data/bag_name/item_name", @enc_edited_data).ordered + + @knife.run + end + end +end diff --git a/spec/unit/knife/data_bag_from_file_spec.rb b/spec/unit/knife/data_bag_from_file_spec.rb new file mode 100644 index 0000000000..f4ed7ca5de --- /dev/null +++ b/spec/unit/knife/data_bag_from_file_spec.rb @@ -0,0 +1,191 @@ +# +# Author:: Seth Falcon (<seth@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 'chef/data_bag_item' +require 'chef/encrypted_data_bag_item' +require 'tempfile' +require 'json' + +Chef::Knife::DataBagFromFile.load_deps + +describe Chef::Knife::DataBagFromFile do + before :each do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::DataBagFromFile.new + @rest = mock("Chef::REST") + @knife.stub!(:rest).and_return(@rest) + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + @tmp_dir = Dir.mktmpdir + @db_folder = File.join(@tmp_dir, 'data_bags', 'bag_name') + FileUtils.mkdir_p(@db_folder) + @db_file = Tempfile.new(["data_bag_from_file_test", ".json"], @db_folder) + @db_file2 = Tempfile.new(["data_bag_from_file_test2", ".json"], @db_folder) + @db_folder2 = File.join(@tmp_dir, 'data_bags', 'bag_name2') + FileUtils.mkdir_p(@db_folder2) + @db_file3 = Tempfile.new(["data_bag_from_file_test3", ".json"], @db_folder2) + @plain_data = { + "id" => "item_name", + "greeting" => "hello", + "nested" => { "a1" => [1, 2, 3], "a2" => { "b1" => true }} + } + @db_file.write(@plain_data.to_json) + @db_file.flush + @knife.instance_variable_set(:@name_args, ['bag_name', @db_file.path]) + end + + # We have to explicitly clean up Tempfile on Windows because it said so. + after :each do + @db_file.close + @db_file2.close + @db_file3.close + FileUtils.rm_rf(@db_folder) + FileUtils.rm_rf(@db_folder2) + FileUtils.remove_entry_secure @tmp_dir + end + + it "loads from a file and saves" do + @knife.loader.should_receive(:load_from).with("data_bags", 'bag_name', @db_file.path).and_return(@plain_data) + dbag = Chef::DataBagItem.new + Chef::DataBagItem.stub!(:new).and_return(dbag) + dbag.should_receive(:save) + @knife.run + + dbag.data_bag.should == 'bag_name' + dbag.raw_data.should == @plain_data + end + + it "loads all from a mutiple files and saves" do + @knife.name_args = [ 'bag_name', @db_file.path, @db_file2.path ] + @knife.loader.should_receive(:load_from).with("data_bags", 'bag_name', @db_file.path).and_return(@plain_data) + @knife.loader.should_receive(:load_from).with("data_bags", 'bag_name', @db_file2.path).and_return(@plain_data) + dbag = Chef::DataBagItem.new + Chef::DataBagItem.stub!(:new).and_return(dbag) + dbag.should_receive(:save).twice + @knife.run + + dbag.data_bag.should == 'bag_name' + dbag.raw_data.should == @plain_data + end + + it "loads all from a folder and saves" do + dir = File.dirname(@db_file.path) + @knife.name_args = [ 'bag_name', @db_folder ] + @knife.loader.should_receive(:load_from).with("data_bags", 'bag_name', @db_file.path).and_return(@plain_data) + @knife.loader.should_receive(:load_from).with("data_bags", 'bag_name', @db_file2.path).and_return(@plain_data) + dbag = Chef::DataBagItem.new + Chef::DataBagItem.stub!(:new).and_return(dbag) + dbag.should_receive(:save).twice + @knife.run + end + + describe "loading all data bags" do + + before do + @pwd = Dir.pwd + Dir.chdir(@tmp_dir) + end + + after do + Dir.chdir(@pwd) + end + + it "loads all data bags when -a or --all options is provided" do + @knife.name_args = [] + @knife.stub!(:config).and_return({:all => true}) + @knife.loader.should_receive(:load_from).with("data_bags", "bag_name", File.basename(@db_file.path)). + and_return(@plain_data) + @knife.loader.should_receive(:load_from).with("data_bags", "bag_name", File.basename(@db_file2.path)). + and_return(@plain_data) + @knife.loader.should_receive(:load_from).with("data_bags", "bag_name2", File.basename(@db_file3.path)). + and_return(@plain_data) + dbag = Chef::DataBagItem.new + Chef::DataBagItem.stub!(:new).and_return(dbag) + dbag.should_receive(:save).exactly(3).times + @knife.run + end + + it "loads all data bags items when -a or --all options is provided" do + @knife.name_args = ["bag_name2"] + @knife.stub!(:config).and_return({:all => true}) + @knife.loader.should_receive(:load_from).with("data_bags", "bag_name2", File.basename(@db_file3.path)). + and_return(@plain_data) + dbag = Chef::DataBagItem.new + Chef::DataBagItem.stub!(:new).and_return(dbag) + dbag.should_receive(:save) + @knife.run + dbag.data_bag.should == 'bag_name2' + dbag.raw_data.should == @plain_data + end + + end + + describe "encrypted data bag items" do + before(:each) do + @secret = "abc123SECRET" + @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data, + @secret) + @secret_file = Tempfile.new("encrypted_data_bag_secret_file_test") + @secret_file.puts(@secret) + @secret_file.flush + end + + after do + @secret_file.close + @secret_file.unlink + end + + it "encrypts values when given --secret" do + @knife.stub!(:config).and_return({:secret => @secret}) + + @knife.loader.should_receive(:load_from).with("data_bags", "bag_name", @db_file.path).and_return(@plain_data) + dbag = Chef::DataBagItem.new + Chef::DataBagItem.stub!(:new).and_return(dbag) + dbag.should_receive(:save) + @knife.run + dbag.data_bag.should == 'bag_name' + dbag.raw_data.should == @enc_data + end + + it "encrypts values when given --secret_file" do + @knife.stub!(:config).and_return({:secret_file => @secret_file.path}) + + @knife.loader.stub!(:load_from).with("data_bags", 'bag_name', @db_file.path).and_return(@plain_data) + dbag = Chef::DataBagItem.new + Chef::DataBagItem.stub!(:new).and_return(dbag) + dbag.should_receive(:save) + @knife.run + dbag.data_bag.should == 'bag_name' + dbag.raw_data.should == @enc_data + end + + end + + describe "command line parsing" do + it "prints help if given no arguments" do + @knife.instance_variable_set(:@name_args, []) + lambda { @knife.run }.should raise_error(SystemExit) + help_text = "knife data bag from file BAG FILE|FOLDER [FILE|FOLDER..] (options)" + help_text_regex = Regexp.new("^#{Regexp.escape(help_text)}") + @stdout.string.should match(help_text_regex) + end + end + +end diff --git a/spec/unit/knife/data_bag_show_spec.rb b/spec/unit/knife/data_bag_show_spec.rb new file mode 100644 index 0000000000..08ecfaa0a7 --- /dev/null +++ b/spec/unit/knife/data_bag_show_spec.rb @@ -0,0 +1,112 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Seth Falcon (<seth@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' + +require 'chef/data_bag_item' +require 'chef/encrypted_data_bag_item' +require 'chef/json_compat' +require 'tempfile' + +describe Chef::Knife::DataBagShow do + before do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::DataBagShow.new + @knife.config[:format] = 'json' + @rest = mock("Chef::REST") + @knife.stub!(:rest).and_return(@rest) + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + + it "prints the ids of the data bag items when given a bag name" do + @knife.instance_variable_set(:@name_args, ['bag_o_data']) + data_bag_contents = { "baz"=>"http://localhost:4000/data/bag_o_data/baz", + "qux"=>"http://localhost:4000/data/bag_o_data/qux"} + Chef::DataBag.should_receive(:load).and_return(data_bag_contents) + expected = %q|[ + "baz", + "qux" +]| + @knife.run + @stdout.string.strip.should == expected + end + + it "prints the contents of the data bag item when given a bag and item name" do + @knife.instance_variable_set(:@name_args, ['bag_o_data', 'an_item']) + data_item = Chef::DataBagItem.new.tap {|item| item.raw_data = {"id" => "an_item", "zsh" => "victory_through_tabbing"}} + + Chef::DataBagItem.should_receive(:load).with('bag_o_data', 'an_item').and_return(data_item) + + @knife.run + Chef::JSONCompat.from_json(@stdout.string).should == data_item.raw_data + + end + + describe "encrypted data bag items" do + before(:each) do + @secret = "abc123SECRET" + @plain_data = { + "id" => "item_name", + "greeting" => "hello", + "nested" => { "a1" => [1, 2, 3], "a2" => { "b1" => true }} + } + @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data, + @secret) + @knife.instance_variable_set(:@name_args, ['bag_name', 'item_name']) + + @secret_file = Tempfile.new("encrypted_data_bag_secret_file_test") + @secret_file.puts(@secret) + @secret_file.flush + end + + after do + @secret_file.close + @secret_file.unlink + end + + it "prints the decrypted contents of an item when given --secret" do + @knife.stub!(:config).and_return({:secret => @secret}) + Chef::EncryptedDataBagItem.should_receive(:load). + with('bag_name', 'item_name', @secret). + and_return(Chef::EncryptedDataBagItem.new(@enc_data, @secret)) + @knife.run + Chef::JSONCompat.from_json(@stdout.string).should == @plain_data + end + + it "prints the decrypted contents of an item when given --secret_file" do + @knife.stub!(:config).and_return({:secret_file => @secret_file.path}) + Chef::EncryptedDataBagItem.should_receive(:load). + with('bag_name', 'item_name', @secret). + and_return(Chef::EncryptedDataBagItem.new(@enc_data, @secret)) + @knife.run + Chef::JSONCompat.from_json(@stdout.string).should == @plain_data + end + end + + describe "command line parsing" do + it "prints help if given no arguments" do + @knife.instance_variable_set(:@name_args, []) + lambda { @knife.run }.should raise_error(SystemExit) + @stdout.string.should match(/^knife data bag show BAG \[ITEM\] \(options\)/) + end + end + +end diff --git a/spec/unit/knife/environment_create_spec.rb b/spec/unit/knife/environment_create_spec.rb new file mode 100644 index 0000000000..36f6556682 --- /dev/null +++ b/spec/unit/knife/environment_create_spec.rb @@ -0,0 +1,91 @@ +# +# Author:: Stephen Delano (<stephen@ospcode.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' + +describe Chef::Knife::EnvironmentCreate do + before(:each) do + @knife = Chef::Knife::EnvironmentCreate.new + @knife.stub!(:msg).and_return true + @knife.stub!(:output).and_return true + @knife.stub!(:show_usage).and_return true + @knife.name_args = [ "production" ] + + @environment = Chef::Environment.new + @environment.stub!(:save) + + Chef::Environment.stub!(:new).and_return @environment + @knife.stub!(:edit_data).and_return @environment + end + + describe "run" do + it "should create a new environment" do + Chef::Environment.should_receive(:new) + @knife.run + end + + it "should set the environment name" do + @environment.should_receive(:name).with("production") + @knife.run + end + + it "should not print the environment" do + @knife.should_not_receive(:output) + @knife.run + end + + it "should prompt you to edit the data" do + @knife.should_receive(:edit_data).with(@environment) + @knife.run + end + + it "should save the environment" do + @environment.should_receive(:save) + @knife.run + end + + it "should show usage and exit when no environment name is provided" do + @knife.name_args = [ ] + @knife.ui.should_receive(:fatal) + @knife.should_receive(:show_usage) + lambda { @knife.run }.should raise_error(SystemExit) + end + + describe "with --description" do + before(:each) do + @knife.config[:description] = "This is production" + end + + it "should set the description" do + @environment.should_receive(:description).with("This is production") + @knife.run + end + end + + describe "with --print-after" do + before(:each) do + @knife.config[:print_after] = true + end + + it "should pretty print the environment, formatted for display" do + @knife.should_receive(:output).with(@environment) + @knife.run + end + end + end +end diff --git a/spec/unit/knife/environment_delete_spec.rb b/spec/unit/knife/environment_delete_spec.rb new file mode 100644 index 0000000000..219ae4a923 --- /dev/null +++ b/spec/unit/knife/environment_delete_spec.rb @@ -0,0 +1,71 @@ +# +# Author:: Stephen Delano (<stephen@ospcode.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' + +describe Chef::Knife::EnvironmentDelete do + before(:each) do + @knife = Chef::Knife::EnvironmentDelete.new + @knife.stub!(:msg).and_return true + @knife.stub!(:output).and_return true + @knife.stub!(:show_usage).and_return true + @knife.stub!(:confirm).and_return true + @knife.name_args = [ "production" ] + + @environment = Chef::Environment.new + @environment.name("production") + @environment.description("Please delete me") + @environment.stub!(:destroy).and_return true + Chef::Environment.stub!(:load).and_return @environment + end + + it "should confirm that you want to delete" do + @knife.should_receive(:confirm) + @knife.run + end + + it "should load the environment" do + Chef::Environment.should_receive(:load).with("production") + @knife.run + end + + it "should delete the environment" do + @environment.should_receive(:destroy) + @knife.run + end + + it "should not print the environment" do + @knife.should_not_receive(:output) + @knife.run + end + + it "should show usage and exit when no environment name is provided" do + @knife.name_args = [] + @knife.ui.should_receive(:fatal) + @knife.should_receive(:show_usage) + lambda { @knife.run }.should raise_error(SystemExit) + end + + describe "with --print-after" do + it "should pretty print the environment, formatted for display" do + @knife.config[:print_after] = true + @knife.should_receive(:output).with(@environment) + @knife.run + end + end +end diff --git a/spec/unit/knife/environment_edit_spec.rb b/spec/unit/knife/environment_edit_spec.rb new file mode 100644 index 0000000000..91f9f5d0f0 --- /dev/null +++ b/spec/unit/knife/environment_edit_spec.rb @@ -0,0 +1,79 @@ +# +# Author:: Stephen Delano (<stephen@ospcode.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' + +describe Chef::Knife::EnvironmentEdit do + before(:each) do + @knife = Chef::Knife::EnvironmentEdit.new + @knife.ui.stub!(:msg).and_return true + @knife.ui.stub!(:output).and_return true + @knife.ui.stub!(:show_usage).and_return true + @knife.name_args = [ "production" ] + + @environment = Chef::Environment.new + @environment.name("production") + @environment.description("Please edit me") + @environment.stub!(:save).and_return true + Chef::Environment.stub!(:load).and_return @environment + @knife.ui.stub(:edit_data).and_return @environment + end + + it "should load the environment" do + Chef::Environment.should_receive(:load).with("production") + @knife.run + end + + it "should let you edit the environment" do + @knife.ui.should_receive(:edit_data).with(@environment) + @knife.run + end + + it "should save the edited environment data" do + pansy = Chef::Environment.new + + @environment.name("new_environment_name") + @knife.ui.should_receive(:edit_data).with(@environment).and_return(pansy) + pansy.should_receive(:save) + @knife.run + end + + it "should not save the unedited environment data" do + @environment.should_not_receive(:save) + @knife.run + end + + it "should not print the environment" do + @knife.should_not_receive(:output) + @knife.run + end + + it "shoud show usage and exit when no environment name is provided" do + @knife.name_args = [] + @knife.should_receive(:show_usage) + lambda { @knife.run }.should raise_error(SystemExit) + end + + describe "with --print-after" do + it "should pretty print the environment, formatted for display" do + @knife.config[:print_after] = true + @knife.ui.should_receive(:output).with(@environment) + @knife.run + end + end +end diff --git a/spec/unit/knife/environment_from_file_spec.rb b/spec/unit/knife/environment_from_file_spec.rb new file mode 100644 index 0000000000..d2234d9be1 --- /dev/null +++ b/spec/unit/knife/environment_from_file_spec.rb @@ -0,0 +1,89 @@ +# +# Author:: Stephen Delano (<stephen@ospcode.com>) +# Author:: Seth Falcon (<seth@ospcode.com>) +# Copyright:: Copyright 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' + +Chef::Knife::EnvironmentFromFile.load_deps + +describe Chef::Knife::EnvironmentFromFile do + before(:each) do + @knife = Chef::Knife::EnvironmentFromFile.new + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + @knife.name_args = [ "spec.rb" ] + + @environment = Chef::Environment.new + @environment.name("spec") + @environment.description("runs the unit tests") + @environment.cookbook_versions({"apt" => "= 1.2.3"}) + @environment.stub!(:save).and_return true + @knife.loader.stub!(:load_from).and_return @environment + end + + describe "run" do + it "loads the environment data from a file and saves it" do + @knife.loader.should_receive(:load_from).with('environments', 'spec.rb').and_return(@environment) + @environment.should_receive(:save) + @knife.run + end + + context "when handling multiple environments" do + before(:each) do + @env_apple = @environment.dup + @env_apple.name("apple") + @knife.loader.stub!(:load_from).with("apple.rb").and_return @env_apple + end + + it "loads multiple environments if given" do + @knife.name_args = [ "spec.rb", "apple.rb" ] + @environment.should_receive(:save).twice + @knife.run + end + + it "loads all environments with -a" do + File.stub!(:expand_path).with("./environments/*.{json,rb}").and_return("/tmp/environments") + Dir.stub!(:glob).with("/tmp/environments").and_return(["spec.rb", "apple.rb"]) + @knife.name_args = [] + @knife.stub!(:config).and_return({:all => true}) + @environment.should_receive(:save).twice + @knife.run + end + end + + it "should not print the environment" do + @knife.should_not_receive(:output) + @knife.run + end + + it "should show usage and exit if not filename is provided" do + @knife.name_args = [] + @knife.ui.should_receive(:fatal) + @knife.should_receive(:show_usage) + lambda { @knife.run }.should raise_error(SystemExit) + end + + describe "with --print-after" do + it "should pretty print the environment, formatted for display" do + @knife.config[:print_after] = true + @knife.should_receive(:output) + @knife.run + end + end + end +end diff --git a/spec/unit/knife/environment_list_spec.rb b/spec/unit/knife/environment_list_spec.rb new file mode 100644 index 0000000000..05a3ae748a --- /dev/null +++ b/spec/unit/knife/environment_list_spec.rb @@ -0,0 +1,54 @@ +# +# Author:: Stephen Delano (<stephen@ospcode.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' + +describe Chef::Knife::EnvironmentList do + before(:each) do + @knife = Chef::Knife::EnvironmentList.new + @knife.stub!(:msg).and_return true + @knife.stub!(:output).and_return true + @knife.stub!(:show_usage).and_return true + + @environments = { + "production" => "http://localhost:4000/environments/production", + "development" => "http://localhost:4000/environments/development", + "testing" => "http://localhost:4000/environments/testing" + } + Chef::Environment.stub!(:list).and_return @environments + end + + it "should make an api call to list the environments" do + Chef::Environment.should_receive(:list) + @knife.run + end + + it "should print the environment names in a sorted list" do + names = @environments.keys.sort { |a,b| a <=> b } + @knife.should_receive(:output).with(names) + @knife.run + end + + describe "with --with-uri" do + it "should print and unsorted list of the environments and their URIs" do + @knife.config[:with_uri] = true + @knife.should_receive(:output).with(@environments) + @knife.run + end + end +end diff --git a/spec/unit/knife/environment_show_spec.rb b/spec/unit/knife/environment_show_spec.rb new file mode 100644 index 0000000000..1e1556f4c3 --- /dev/null +++ b/spec/unit/knife/environment_show_spec.rb @@ -0,0 +1,52 @@ +# +# Author:: Stephen Delano (<stephen@ospcode.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' + +describe Chef::Knife::EnvironmentShow do + before(:each) do + @knife = Chef::Knife::EnvironmentShow.new + @knife.stub!(:msg).and_return true + @knife.stub!(:output).and_return true + @knife.stub!(:show_usage).and_return true + @knife.name_args = [ "production" ] + + @environment = Chef::Environment.new + @environment.name("production") + @environment.description("Look at me!") + Chef::Environment.stub!(:load).and_return @environment + end + + it "should load the environment" do + Chef::Environment.should_receive(:load).with("production") + @knife.run + end + + it "should pretty print the environment, formatted for display" do + @knife.should_receive(:format_for_display).with(@environment) + @knife.should_receive(:output) + @knife.run + end + + it "should show usage and exit when no environment name is provided" do + @knife.name_args = [] + @knife.ui.should_receive(:fatal) + @knife.should_receive(:show_usage) + lambda { @knife.run }.should raise_error(SystemExit) + end +end diff --git a/spec/unit/knife/index_rebuild_spec.rb b/spec/unit/knife/index_rebuild_spec.rb new file mode 100644 index 0000000000..3b22a3b88c --- /dev/null +++ b/spec/unit/knife/index_rebuild_spec.rb @@ -0,0 +1,65 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2009 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' + +describe Chef::Knife::IndexRebuild do + before do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::IndexRebuild.new + @rest_client = mock("Chef::REST (mock)", :post_rest => { :result => :true }) + @knife.ui.stub!(:output) + @knife.stub!(:rest).and_return(@rest_client) + + @out = StringIO.new + @knife.ui.stub!(:stdout).and_return(@out) + end + + it "asks a yes/no confirmation and aborts on 'no'" do + @knife.ui.stub!(:stdin).and_return(StringIO.new("NO\n")) + @knife.should_receive(:puts) + @knife.should_receive(:exit).with(7) + @knife.run + @out.string.should match(/yes\/no/) + end + + it "asks a confirmation and continues on 'yes'" do + @knife.ui.stub!(:stdin).and_return(StringIO.new("yes\n")) + @knife.should_not_receive(:exit) + @knife.run + @out.string.should match(/yes\/no/) + end + + describe "after confirming the operation" do + before do + @knife.ui.stub!(:print) + @knife.ui.stub!(:puts) + @knife.stub!(:nag) + @knife.ui.stub!(:output) + end + + it "POSTs to /search/reindex and displays the result" do + @rest_client = mock("Chef::REST") + @knife.stub!(:rest).and_return(@rest_client) + @rest_client.should_receive(:post_rest).with("/search/reindex", {}).and_return("monkey") + @knife.should_receive(:output).with("monkey") + @knife.run + end + end + +end diff --git a/spec/unit/knife/knife_help.rb b/spec/unit/knife/knife_help.rb new file mode 100644 index 0000000000..f5753e3d62 --- /dev/null +++ b/spec/unit/knife/knife_help.rb @@ -0,0 +1,92 @@ +# +# 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::Knife::Help do + before(:each) do + # Perilously use the build in list even though it is dynamic so we don't get warnings about the constant + # HELP_TOPICS = [ "foo", "bar", "knife-kittens", "ceiling-cat", "shell" ] + @knife = Chef::Knife::Help.new + end + + it "should return a list of help topics" do + @knife.help_topics.should include("knife-status") + end + + it "should run man for you" do + @knife.name_args = [ "shell" ] + @knife.should_receive(:exec).with(/^man \/.*\/shell.1$/) + @knife.run + end + + it "should suggest topics" do + @knife.name_args = [ "list" ] + @knife.ui.stub!(:msg) + @knife.ui.should_receive(:info).with("Available help topics are: ") + @knife.ui.should_receive(:msg).with(/knife/) + @knife.stub!(:exec) + @knife.should_receive(:exit).with(1) + @knife.run + end + + describe "find_manpage_path" do + it "should find the man page in the gem" do + @knife.find_manpage_path("shell").should =~ /distro\/common\/man\/man1\/chef-shell.1$/ + end + + it "should provide the man page name if not in the gem" do + @knife.find_manpage_path("foo").should == "foo" + end + end + + describe "find_manpages_for_query" do + it "should error if it does not find a match" do + @knife.ui.stub!(:error) + @knife.ui.stub!(:info) + @knife.ui.stub!(:msg) + @knife.should_receive(:exit).with(1) + @knife.ui.should_receive(:error).with("No help found for 'chickens'") + @knife.ui.should_receive(:msg).with(/knife/) + @knife.find_manpages_for_query("chickens") + end + end + + describe "print_help_topics" do + it "should print the known help topics" do + @knife.ui.stub!(:msg) + @knife.ui.stub!(:info) + @knife.ui.should_receive(:msg).with(/knife/) + @knife.print_help_topics + end + + it "should shorten topics prefixed by knife-" do + @knife.ui.stub!(:msg) + @knife.ui.stub!(:info) + @knife.ui.should_receive(:msg).with(/node/) + @knife.print_help_topics + end + + it "should not leave topics prefixed by knife-" do + @knife.ui.stub!(:msg) + @knife.ui.stub!(:info) + @knife.ui.should_not_receive(:msg).with(/knife-node/) + @knife.print_help_topics + end + end +end diff --git a/spec/unit/knife/node_bulk_delete_spec.rb b/spec/unit/knife/node_bulk_delete_spec.rb new file mode 100644 index 0000000000..51f707dfcf --- /dev/null +++ b/spec/unit/knife/node_bulk_delete_spec.rb @@ -0,0 +1,97 @@ +# +# 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::Knife::NodeBulkDelete do + before(:each) do + Chef::Log.logger = Logger.new(StringIO.new) + + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::NodeBulkDelete.new + @knife.name_args = ["."] + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + @knife.ui.stub!(:confirm).and_return(true) + @nodes = Hash.new + %w{adam brent jacob}.each do |node_name| + @nodes[node_name] = "http://localhost:4000/nodes/#{node_name}" + end + end + + describe "when creating the list of nodes" do + it "fetches the node list" do + expected = @nodes.inject({}) do |inflatedish, (name, uri)| + inflatedish[name] = Chef::Node.new.tap {|n| n.name(name)} + inflatedish + end + Chef::Node.should_receive(:list).and_return(@nodes) + # I hate not having == defined for anything :( + actual = @knife.all_nodes + actual.keys.should =~ expected.keys + actual.values.map {|n| n.name }.should =~ %w[adam brent jacob] + end + end + + describe "run" do + before do + @inflatedish_list = @nodes.keys.inject({}) do |nodes_by_name, name| + node = Chef::Node.new() + node.name(name) + node.stub!(:destroy).and_return(true) + nodes_by_name[name] = node + nodes_by_name + end + @knife.stub!(:all_nodes).and_return(@inflatedish_list) + end + + it "should print the nodes you are about to delete" do + @knife.run + @stdout.string.should match(/#{@knife.ui.list(@nodes.keys.sort, :columns_down)}/) + end + + it "should confirm you really want to delete them" do + @knife.ui.should_receive(:confirm) + @knife.run + end + + it "should delete each node" do + @inflatedish_list.each_value do |n| + n.should_receive(:destroy) + end + @knife.run + end + + it "should only delete nodes that match the regex" do + @knife.name_args = ['adam'] + @inflatedish_list['adam'].should_receive(:destroy) + @inflatedish_list['brent'].should_not_receive(:destroy) + @inflatedish_list['jacob'].should_not_receive(:destroy) + @knife.run + end + + it "should exit if the regex is not provided" do + @knife.name_args = [] + lambda { @knife.run }.should raise_error(SystemExit) + end + + end +end + + + diff --git a/spec/unit/knife/node_delete_spec.rb b/spec/unit/knife/node_delete_spec.rb new file mode 100644 index 0000000000..b1b3db1aa4 --- /dev/null +++ b/spec/unit/knife/node_delete_spec.rb @@ -0,0 +1,68 @@ +# +# 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::Knife::NodeDelete do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::NodeDelete.new + @knife.config = { + :print_after => nil + } + @knife.name_args = [ "adam" ] + @knife.stub!(:output).and_return(true) + @knife.stub!(:confirm).and_return(true) + @node = Chef::Node.new() + @node.stub!(:destroy).and_return(true) + Chef::Node.stub!(:load).and_return(@node) + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + describe "run" do + it "should confirm that you want to delete" do + @knife.should_receive(:confirm) + @knife.run + end + + it "should load the node" do + Chef::Node.should_receive(:load).with("adam").and_return(@node) + @knife.run + end + + it "should delete the node" do + @node.should_receive(:destroy).and_return(@node) + @knife.run + end + + it "should not print the node" do + @knife.should_not_receive(:output).with("poop") + @knife.run + end + + describe "with -p or --print-after" do + it "should pretty print the node, formatted for display" do + @knife.config[:print_after] = true + @knife.should_receive(:format_for_display).with(@node).and_return("poop") + @knife.should_receive(:output).with("poop") + @knife.run + end + end + end +end diff --git a/spec/unit/knife/node_edit_spec.rb b/spec/unit/knife/node_edit_spec.rb new file mode 100644 index 0000000000..0ba2e90cfe --- /dev/null +++ b/spec/unit/knife/node_edit_spec.rb @@ -0,0 +1,88 @@ +# +# 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' +Chef::Knife::NodeEdit.load_deps + +describe Chef::Knife::NodeEdit do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::NodeEdit.new + @knife.config = { + :editor => 'cat', + :attribute => nil, + :print_after => nil + } + @knife.name_args = [ "adam" ] + @node = Chef::Node.new() + end + + it "should load the node" do + Chef::Node.should_receive(:load).with("adam").and_return(@node) + @knife.node + end + + describe "after loading the node" do + before do + @knife.stub!(:node).and_return(@node) + @node.automatic_attrs = {:go => :away} + @node.default_attrs = {:hide => :me} + @node.override_attrs = {:dont => :show} + @node.normal_attrs = {:do_show => :these} + @node.chef_environment("prod") + @node.run_list("recipe[foo]") + end + + it "creates a view of the node without attributes from roles or ohai" do + actual = Chef::JSONCompat.from_json(@knife.node_editor.view) + actual.should_not have_key("automatic") + actual.should_not have_key("override") + actual.should_not have_key("default") + actual["normal"].should == {"do_show" => "these"} + actual["run_list"].should == ["recipe[foo]"] + actual["chef_environment"].should == "prod" + end + + it "shows the extra attributes when given the --all option" do + @knife.config[:all_attributes] = true + + actual = Chef::JSONCompat.from_json(@knife.node_editor.view) + actual["automatic"].should == {"go" => "away"} + actual["override"].should == {"dont" => "show"} + actual["default"].should == {"hide" => "me"} + actual["normal"].should == {"do_show" => "these"} + actual["run_list"].should == ["recipe[foo]"] + actual["chef_environment"].should == "prod" + end + + it "does not consider unedited data updated" do + view = Chef::JSONCompat.from_json( @knife.node_editor.view ) + @knife.node_editor.apply_updates(view) + @knife.node_editor.should_not be_updated + end + + it "considers edited data updated" do + view = Chef::JSONCompat.from_json( @knife.node_editor.view ) + view["run_list"] << "role[fuuu]" + @knife.node_editor.apply_updates(view) + @knife.node_editor.should be_updated + end + + end +end + diff --git a/spec/unit/knife/node_from_file_spec.rb b/spec/unit/knife/node_from_file_spec.rb new file mode 100644 index 0000000000..c6b9610d9e --- /dev/null +++ b/spec/unit/knife/node_from_file_spec.rb @@ -0,0 +1,59 @@ +# +# 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' + +Chef::Knife::NodeFromFile.load_deps + +describe Chef::Knife::NodeFromFile do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::NodeFromFile.new + @knife.config = { + :print_after => nil + } + @knife.name_args = [ "adam.rb" ] + @knife.stub!(:output).and_return(true) + @knife.stub!(:confirm).and_return(true) + @node = Chef::Node.new() + @node.stub!(:save) + @knife.loader.stub!(:load_from).and_return(@node) + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + describe "run" do + it "should load from a file" do + @knife.loader.should_receive(:load_from).with('nodes', 'adam.rb').and_return(@node) + @knife.run + end + + it "should not print the Node" do + @knife.should_not_receive(:output) + @knife.run + end + + describe "with -p or --print-after" do + it "should print the Node" do + @knife.config[:print_after] = true + @knife.should_receive(:output) + @knife.run + end + end + end +end diff --git a/spec/unit/knife/node_list_spec.rb b/spec/unit/knife/node_list_spec.rb new file mode 100644 index 0000000000..5637d679c8 --- /dev/null +++ b/spec/unit/knife/node_list_spec.rb @@ -0,0 +1,63 @@ +# +# 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::Knife::NodeList do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + Chef::Config[:environment] = nil # reset this value each time, as it is not reloaded + @knife = Chef::Knife::NodeList.new + @knife.stub!(:output).and_return(true) + @list = { + "foo" => "http://example.com/foo", + "bar" => "http://example.com/foo" + } + Chef::Node.stub!(:list).and_return(@list) + Chef::Node.stub!(:list_by_environment).and_return(@list) + end + + describe "run" do + it "should list all of the nodes if -E is not specified" do + Chef::Node.should_receive(:list).and_return(@list) + @knife.run + end + + it "should pretty print the list" do + Chef::Node.should_receive(:list).and_return(@list) + @knife.should_receive(:output).with([ "bar", "foo" ]) + @knife.run + end + + it "should list nodes in the specific environment if -E ENVIRONMENT is specified" do + Chef::Config[:environment] = "prod" + Chef::Node.should_receive(:list_by_environment).with("prod").and_return(@list) + @knife.run + end + + describe "with -w or --with-uri" do + it "should pretty print the hash" do + @knife.config[:with_uri] = true + Chef::Node.should_receive(:list).and_return(@list) + @knife.should_receive(:output).with(@list) + @knife.run + end + end + end +end + diff --git a/spec/unit/knife/node_run_list_add_spec.rb b/spec/unit/knife/node_run_list_add_spec.rb new file mode 100644 index 0000000000..ee0cfc9038 --- /dev/null +++ b/spec/unit/knife/node_run_list_add_spec.rb @@ -0,0 +1,125 @@ +# +# 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::Knife::NodeRunListAdd do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::NodeRunListAdd.new + @knife.config = { + :after => nil + } + @knife.name_args = [ "adam", "role[monkey]" ] + @knife.stub!(:output).and_return(true) + @node = Chef::Node.new() + @node.stub!(:save).and_return(true) + Chef::Node.stub!(:load).and_return(@node) + end + + describe "run" do + it "should load the node" do + Chef::Node.should_receive(:load).with("adam") + @knife.run + end + + it "should add to the run list" do + @knife.run + @node.run_list[0].should == 'role[monkey]' + end + + it "should save the node" do + @node.should_receive(:save) + @knife.run + end + + it "should print the run list" do + @knife.should_receive(:output).and_return(true) + @knife.run + end + + describe "with -a or --after specified" do + it "should add to the run list after the specified entry" do + @node.run_list << "role[acorns]" + @node.run_list << "role[barn]" + @knife.config[:after] = "role[acorns]" + @knife.run + @node.run_list[0].should == "role[acorns]" + @node.run_list[1].should == "role[monkey]" + @node.run_list[2].should == "role[barn]" + end + end + + describe "with more than one role or recipe" do + it "should add to the run list all the entries" do + @knife.name_args = [ "adam", "role[monkey],role[duck]" ] + @node.run_list << "role[acorns]" + @knife.run + @node.run_list[0].should == "role[acorns]" + @node.run_list[1].should == "role[monkey]" + @node.run_list[2].should == "role[duck]" + end + end + + describe "with more than one role or recipe with space between items" do + it "should add to the run list all the entries" do + @knife.name_args = [ "adam", "role[monkey], role[duck]" ] + @node.run_list << "role[acorns]" + @knife.run + @node.run_list[0].should == "role[acorns]" + @node.run_list[1].should == "role[monkey]" + @node.run_list[2].should == "role[duck]" + end + end + + describe "with more than one role or recipe as different arguments" do + it "should add to the run list all the entries" do + @knife.name_args = [ "adam", "role[monkey]", "role[duck]" ] + @node.run_list << "role[acorns]" + @knife.run + @node.run_list[0].should == "role[acorns]" + @node.run_list[1].should == "role[monkey]" + @node.run_list[2].should == "role[duck]" + end + end + + describe "with more than one role or recipe as different arguments and list separated by comas" do + it "should add to the run list all the entries" do + @knife.name_args = [ "adam", "role[monkey]", "role[duck],recipe[bird::fly]" ] + @node.run_list << "role[acorns]" + @knife.run + @node.run_list[0].should == "role[acorns]" + @node.run_list[1].should == "role[monkey]" + @node.run_list[2].should == "role[duck]" + end + end + + describe "with one role or recipe but with an extraneous comma" do + it "should add to the run list one item" do + @knife.name_args = [ "adam", "role[monkey]," ] + @node.run_list << "role[acorns]" + @knife.run + @node.run_list[0].should == "role[acorns]" + @node.run_list[1].should == "role[monkey]" + end + end + end +end + + + diff --git a/spec/unit/knife/node_run_list_remove_spec.rb b/spec/unit/knife/node_run_list_remove_spec.rb new file mode 100644 index 0000000000..90869e8baa --- /dev/null +++ b/spec/unit/knife/node_run_list_remove_spec.rb @@ -0,0 +1,74 @@ +# +# 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::Knife::NodeRunListRemove do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::NodeRunListRemove.new + @knife.config[:print_after] = nil + @knife.name_args = [ "adam", "role[monkey]" ] + @node = Chef::Node.new() + @node.name("knifetest-node") + @node.run_list << "role[monkey]" + @node.stub!(:save).and_return(true) + + @knife.ui.stub!(:output).and_return(true) + @knife.ui.stub!(:confirm).and_return(true) + + Chef::Node.stub!(:load).and_return(@node) + end + + describe "run" do + it "should load the node" do + Chef::Node.should_receive(:load).with("adam").and_return(@node) + @knife.run + end + + it "should remove the item from the run list" do + @knife.run + @node.run_list[0].should_not == 'role[monkey]' + end + + it "should save the node" do + @node.should_receive(:save).and_return(true) + @knife.run + end + + it "should print the run list" do + @knife.config[:print_after] = true + @knife.ui.should_receive(:output).with({ "knifetest-node" => { 'run_list' => [] } }) + @knife.run + end + + describe "run with a list of roles and recipes" do + it "should remove the items from the run list" do + @node.run_list << 'role[monkey]' + @node.run_list << 'recipe[duck::type]' + @knife.name_args = [ 'adam', 'role[monkey],recipe[duck::type]' ] + @knife.run + @node.run_list.should_not include('role[monkey]') + @node.run_list.should_not include('recipe[duck::type]') + end + end + end +end + + + diff --git a/spec/unit/knife/node_show_spec.rb b/spec/unit/knife/node_show_spec.rb new file mode 100644 index 0000000000..6600b2aa96 --- /dev/null +++ b/spec/unit/knife/node_show_spec.rb @@ -0,0 +1,48 @@ +# +# 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::Knife::NodeShow do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::NodeShow.new + @knife.config = { + :attribute => nil, + :run_list => nil, + :environment => nil + } + @knife.name_args = [ "adam" ] + @knife.stub!(:output).and_return(true) + @node = Chef::Node.new() + Chef::Node.stub!(:load).and_return(@node) + end + + describe "run" do + it "should load the node" do + Chef::Node.should_receive(:load).with("adam").and_return(@node) + @knife.run + end + + it "should pretty print the node, formatted for display" do + @knife.should_receive(:format_for_display).with(@node).and_return("poop") + @knife.should_receive(:output).with("poop") + @knife.run + end + end +end diff --git a/spec/unit/knife/role_bulk_delete_spec.rb b/spec/unit/knife/role_bulk_delete_spec.rb new file mode 100644 index 0000000000..0ee84f6455 --- /dev/null +++ b/spec/unit/knife/role_bulk_delete_spec.rb @@ -0,0 +1,80 @@ +# +# Author:: Stephen Delano (<stephen@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' + +describe Chef::Knife::RoleBulkDelete do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::RoleBulkDelete.new + @knife.config = { + :print_after => nil + } + @knife.name_args = ["."] + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + @knife.ui.stub!(:confirm).and_return(true) + @roles = Hash.new + %w{dev staging production}.each do |role_name| + role = Chef::Role.new() + role.name(role_name) + role.stub!(:destroy).and_return(true) + @roles[role_name] = role + end + Chef::Role.stub!(:list).and_return(@roles) + end + + describe "run" do + + it "should get the list of the roles" do + Chef::Role.should_receive(:list).and_return(@roles) + @knife.run + end + + it "should print the roles you are about to delete" do + @knife.run + @stdout.string.should match(/#{@knife.ui.list(@roles.keys.sort, :columns_down)}/) + end + + it "should confirm you really want to delete them" do + @knife.ui.should_receive(:confirm) + @knife.run + end + + it "should delete each role" do + @roles.each_value do |r| + r.should_receive(:destroy) + end + @knife.run + end + + it "should only delete roles that match the regex" do + @knife.name_args = ["dev"] + @roles["dev"].should_receive(:destroy) + @roles["staging"].should_not_receive(:destroy) + @roles["production"].should_not_receive(:destroy) + @knife.run + end + + it "should exit if the regex is not provided" do + @knife.name_args = [] + lambda { @knife.run }.should raise_error(SystemExit) + end + + end +end diff --git a/spec/unit/knife/role_create_spec.rb b/spec/unit/knife/role_create_spec.rb new file mode 100644 index 0000000000..af3a6bf539 --- /dev/null +++ b/spec/unit/knife/role_create_spec.rb @@ -0,0 +1,80 @@ +# +# 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::Knife::RoleCreate do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::RoleCreate.new + @knife.config = { + :description => nil + } + @knife.name_args = [ "adam" ] + @knife.stub!(:output).and_return(true) + @role = Chef::Role.new() + @role.stub!(:save) + Chef::Role.stub!(:new).and_return(@role) + @knife.stub!(:edit_data).and_return(@role) + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + describe "run" do + it "should create a new role" do + Chef::Role.should_receive(:new).and_return(@role) + @knife.run + end + + it "should set the role name" do + @role.should_receive(:name).with("adam") + @knife.run + end + + it "should not print the role" do + @knife.should_not_receive(:output) + @knife.run + end + + it "should allow you to edit the data" do + @knife.should_receive(:edit_data).with(@role) + @knife.run + end + + it "should save the role" do + @role.should_receive(:save) + @knife.run + end + + describe "with -d or --description" do + it "should set the description" do + @knife.config[:description] = "All is bob" + @role.should_receive(:description).with("All is bob") + @knife.run + end + end + + describe "with -p or --print-after" do + it "should pretty print the node, formatted for display" do + @knife.config[:print_after] = true + @knife.should_receive(:output).with(@role) + @knife.run + end + end + end +end diff --git a/spec/unit/knife/role_delete_spec.rb b/spec/unit/knife/role_delete_spec.rb new file mode 100644 index 0000000000..d2d8b889b3 --- /dev/null +++ b/spec/unit/knife/role_delete_spec.rb @@ -0,0 +1,67 @@ +# +# 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::Knife::RoleDelete do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::RoleDelete.new + @knife.config = { + :print_after => nil + } + @knife.name_args = [ "adam" ] + @knife.stub!(:output).and_return(true) + @knife.stub!(:confirm).and_return(true) + @role = Chef::Role.new() + @role.stub!(:destroy).and_return(true) + Chef::Role.stub!(:load).and_return(@role) + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + describe "run" do + it "should confirm that you want to delete" do + @knife.should_receive(:confirm) + @knife.run + end + + it "should load the Role" do + Chef::Role.should_receive(:load).with("adam").and_return(@role) + @knife.run + end + + it "should delete the Role" do + @role.should_receive(:destroy).and_return(@role) + @knife.run + end + + it "should not print the Role" do + @knife.should_not_receive(:output) + @knife.run + end + + describe "with -p or --print-after" do + it "should pretty print the Role, formatted for display" do + @knife.config[:print_after] = true + @knife.should_receive(:output) + @knife.run + end + end + end +end diff --git a/spec/unit/knife/role_edit_spec.rb b/spec/unit/knife/role_edit_spec.rb new file mode 100644 index 0000000000..3a002f348c --- /dev/null +++ b/spec/unit/knife/role_edit_spec.rb @@ -0,0 +1,79 @@ +# +# 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::Knife::RoleEdit do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::RoleEdit.new + @knife.config[:print_after] = nil + @knife.name_args = [ "adam" ] + @knife.ui.stub!(:output).and_return(true) + @role = Chef::Role.new() + @role.stub!(:save) + Chef::Role.stub!(:load).and_return(@role) + @knife.ui.stub!(:edit_data).and_return(@role) + @knife.ui.stub!(:msg) + end + + describe "run" do + it "should load the role" do + Chef::Role.should_receive(:load).with("adam").and_return(@role) + @knife.run + end + + it "should edit the role data" do + @knife.ui.should_receive(:edit_data).with(@role) + @knife.run + end + + it "should save the edited role data" do + pansy = Chef::Role.new + + @role.name("new_role_name") + @knife.ui.should_receive(:edit_data).with(@role).and_return(pansy) + pansy.should_receive(:save) + @knife.run + end + + it "should not save the unedited role data" do + pansy = Chef::Role.new + + @knife.ui.should_receive(:edit_data).with(@role).and_return(pansy) + pansy.should_not_receive(:save) + @knife.run + + end + + it "should not print the role" do + @knife.ui.should_not_receive(:output) + @knife.run + end + + describe "with -p or --print-after" do + it "should pretty print the role, formatted for display" do + @knife.config[:print_after] = true + @knife.ui.should_receive(:output).with(@role) + @knife.run + end + end + end +end + + diff --git a/spec/unit/knife/role_from_file_spec.rb b/spec/unit/knife/role_from_file_spec.rb new file mode 100644 index 0000000000..9b81bb14af --- /dev/null +++ b/spec/unit/knife/role_from_file_spec.rb @@ -0,0 +1,69 @@ +# +# 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' + +Chef::Knife::RoleFromFile.load_deps + +describe Chef::Knife::RoleFromFile do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::RoleFromFile.new + @knife.config = { + :print_after => nil + } + @knife.name_args = [ "adam.rb" ] + @knife.stub!(:output).and_return(true) + @knife.stub!(:confirm).and_return(true) + @role = Chef::Role.new() + @role.stub!(:save) + @knife.loader.stub!(:load_from).and_return(@role) + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + describe "run" do + it "should load from a file" do + @knife.loader.should_receive(:load_from).with('roles', 'adam.rb').and_return(@role) + @knife.run + end + + it "should not print the role" do + @knife.should_not_receive(:output) + @knife.run + end + + describe "with -p or --print-after" do + it "should print the role" do + @knife.config[:print_after] = true + @knife.should_receive(:output) + @knife.run + end + end + end + + describe "run with multiple arguments" do + it "should load each file" do + @knife.name_args = [ "adam.rb", "caleb.rb" ] + @knife.loader.should_receive(:load_from).with('roles', 'adam.rb').and_return(@role) + @knife.loader.should_receive(:load_from).with('roles', 'caleb.rb').and_return(@role) + @knife.run + end + end + +end diff --git a/spec/unit/knife/role_list_spec.rb b/spec/unit/knife/role_list_spec.rb new file mode 100644 index 0000000000..1a5e8e2a72 --- /dev/null +++ b/spec/unit/knife/role_list_spec.rb @@ -0,0 +1,56 @@ +# +# 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::Knife::RoleList do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::RoleList.new + @knife.stub!(:output).and_return(true) + @list = { + "foo" => "http://example.com/foo", + "bar" => "http://example.com/foo" + } + Chef::Role.stub!(:list).and_return(@list) + end + + describe "run" do + it "should list the roles" do + Chef::Role.should_receive(:list).and_return(@list) + @knife.run + end + + it "should pretty print the list" do + Chef::Role.should_receive(:list).and_return(@list) + @knife.should_receive(:output).with([ "bar", "foo" ]) + @knife.run + end + + describe "with -w or --with-uri" do + it "should pretty print the hash" do + @knife.config[:with_uri] = true + Chef::Role.should_receive(:list).and_return(@list) + @knife.should_receive(:output).with(@list) + @knife.run + end + end + end +end + + diff --git a/spec/unit/knife/ssh_spec.rb b/spec/unit/knife/ssh_spec.rb new file mode 100644 index 0000000000..6e90a87f01 --- /dev/null +++ b/spec/unit/knife/ssh_spec.rb @@ -0,0 +1,182 @@ +# +# 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 'net/ssh' +require 'net/ssh/multi' + +describe Chef::Knife::Ssh do + before(:all) do + @original_config = Chef::Config.hash_dup + @original_knife_config = Chef::Config[:knife].dup + Chef::Config[:client_key] = CHEF_SPEC_DATA + "/ssl/private_key.pem" + end + + after(:all) do + Chef::Config.configuration = @original_config + Chef::Config[:knife] = @original_knife_config + end + + before do + @knife = Chef::Knife::Ssh.new + @knife.config.clear + @knife.config[:attribute] = "fqdn" + @node_foo = Chef::Node.new + @node_foo.automatic_attrs[:fqdn] = "foo.example.org" + @node_foo.automatic_attrs[:ipaddress] = "10.0.0.1" + @node_bar = Chef::Node.new + @node_bar.automatic_attrs[:fqdn] = "bar.example.org" + @node_bar.automatic_attrs[:ipaddress] = "10.0.0.2" + end + + describe "#configure_session" do + context "manual is set to false (default)" do + before do + @knife.config[:manual] = false + @query = Chef::Search::Query.new + end + + def configure_query(node_array) + @query.stub!(:search).and_return([node_array]) + Chef::Search::Query.stub!(:new).and_return(@query) + end + + def self.should_return_specified_attributes + it "returns an array of the attributes specified on the command line OR config file, if only one is set" do + @knife.config[:attribute] = "ipaddress" + @knife.config[:override_attribute] = "ipaddress" + configure_query([@node_foo, @node_bar]) + @knife.should_receive(:session_from_list).with(['10.0.0.1', '10.0.0.2']) + @knife.configure_session + end + + it "returns an array of the attributes specified on the command line even when a config value is set" do + @knife.config[:attribute] = "config_file" # this value will be the config file + @knife.config[:override_attribute] = "ipaddress" # this is the value of the command line via #configure_attribute + configure_query([@node_foo, @node_bar]) + @knife.should_receive(:session_from_list).with(['10.0.0.1', '10.0.0.2']) + @knife.configure_session + end + end + + it "searchs for and returns an array of fqdns" do + configure_query([@node_foo, @node_bar]) + @knife.should_receive(:session_from_list).with(['foo.example.org', 'bar.example.org']) + @knife.configure_session + end + + should_return_specified_attributes + + context "when cloud hostnames are available" do + before do + @node_foo.automatic_attrs[:cloud][:public_hostname] = "ec2-10-0-0-1.compute-1.amazonaws.com" + @node_bar.automatic_attrs[:cloud][:public_hostname] = "ec2-10-0-0-2.compute-1.amazonaws.com" + end + + it "returns an array of cloud public hostnames" do + configure_query([@node_foo, @node_bar]) + @knife.should_receive(:session_from_list).with(['ec2-10-0-0-1.compute-1.amazonaws.com', 'ec2-10-0-0-2.compute-1.amazonaws.com']) + @knife.configure_session + end + + should_return_specified_attributes + end + + it "should raise an error if no host are found" do + configure_query([ ]) + @knife.ui.should_receive(:fatal) + @knife.should_receive(:exit).with(10) + @knife.configure_session + end + + context "when there are some hosts found but they do not have an attribute to connect with" do + before do + @query.stub!(:search).and_return([[@node_foo, @node_bar]]) + @node_foo.automatic_attrs[:fqdn] = nil + @node_bar.automatic_attrs[:fqdn] = nil + Chef::Search::Query.stub!(:new).and_return(@query) + end + + it "should raise a specific error (CHEF-3402)" do + @knife.ui.should_receive(:fatal).with(/^2 nodes found/) + @knife.should_receive(:exit).with(10) + @knife.configure_session + end + end + end + + context "manual is set to true" do + before do + @knife.config[:manual] = true + end + + it "returns an array of provided values" do + @knife.instance_variable_set(:@name_args, ["foo.example.org bar.example.org"]) + @knife.should_receive(:session_from_list).with(['foo.example.org', 'bar.example.org']) + @knife.configure_session + end + end + end + + describe "#configure_attribute" do + before do + Chef::Config[:knife][:ssh_attribute] = nil + @knife.config[:attribute] = nil + end + + it "should return fqdn by default" do + @knife.configure_attribute + @knife.config[:attribute].should == "fqdn" + end + + it "should return the value set in the configuration file" do + Chef::Config[:knife][:ssh_attribute] = "config_file" + @knife.configure_attribute + @knife.config[:attribute].should == "config_file" + end + + it "should return the value set on the command line" do + @knife.config[:attribute] = "command_line" + @knife.configure_attribute + @knife.config[:attribute].should == "command_line" + end + + it "should set override_attribute to the value of attribute from the command line" do + @knife.config[:attribute] = "command_line" + @knife.configure_attribute + @knife.config[:attribute].should == "command_line" + @knife.config[:override_attribute].should == "command_line" + end + + it "should set override_attribute to the value of attribute from the config file" do + Chef::Config[:knife][:ssh_attribute] = "config_file" + @knife.configure_attribute + @knife.config[:attribute].should == "config_file" + @knife.config[:override_attribute].should == "config_file" + end + + it "should prefer the command line over the config file for the value of override_attribute" do + Chef::Config[:knife][:ssh_attribute] = "config_file" + @knife.config[:attribute] = "command_line" + @knife.configure_attribute + @knife.config[:override_attribute].should == "command_line" + end + end + +end + diff --git a/spec/unit/knife/status_spec.rb b/spec/unit/knife/status_spec.rb new file mode 100644 index 0000000000..b009997ab1 --- /dev/null +++ b/spec/unit/knife/status_spec.rb @@ -0,0 +1,43 @@ +# +# Author:: Sahil Muthoo (<sahil.muthoo@gmail.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 'highline' + +describe Chef::Knife::Status do + before(:each) do + node = Chef::Node.new.tap do |n| + n.automatic_attrs["fqdn"] = "foobar" + n.automatic_attrs["ohai_time"] = 1343845969 + end + query = mock("Chef::Search::Query") + query.stub!(:search).and_yield(node) + Chef::Search::Query.stub!(:new).and_return(query) + @knife = Chef::Knife::Status.new + @stdout = StringIO.new + @knife.stub!(:highline).and_return(HighLine.new(StringIO.new, @stdout)) + end + + describe "run" do + it "should not colorize output unless it's writing to a tty" do + @knife.run + @stdout.string.match(/foobar/).should_not be_nil + @stdout.string.match(/\e.*ago/).should be_nil + end + end +end diff --git a/spec/unit/knife/tag_create_spec.rb b/spec/unit/knife/tag_create_spec.rb new file mode 100644 index 0000000000..925d060879 --- /dev/null +++ b/spec/unit/knife/tag_create_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Chef::Knife::TagCreate do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::TagCreate.new + @knife.name_args = [ Chef::Config[:node_name], "happytag" ] + + @node = Chef::Node.new + @node.stub! :save + Chef::Node.stub!(:load).and_return @node + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + describe "run" do + it "can create tags on a node" do + @knife.run + @node.tags.should == ["happytag"] + @stdout.string.should match /created tags happytag.+node webmonkey.example.com/i + end + end +end diff --git a/spec/unit/knife/tag_delete_spec.rb b/spec/unit/knife/tag_delete_spec.rb new file mode 100644 index 0000000000..ca279033a4 --- /dev/null +++ b/spec/unit/knife/tag_delete_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Chef::Knife::TagDelete do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::TagDelete.new + @knife.name_args = [ Chef::Config[:node_name], "sadtag" ] + + @node = Chef::Node.new + @node.stub! :save + @node.tags << "sadtag" << "happytag" + Chef::Node.stub!(:load).and_return @node + @stdout = StringIO.new + @knife.ui.stub!(:stdout).and_return(@stdout) + end + + describe "run" do + it "can delete tags on a node" do + @node.tags.should == ["sadtag", "happytag"] + @knife.run + @node.tags.should == ["happytag"] + @stdout.string.should match /deleted.+sadtag/i + end + end +end diff --git a/spec/unit/knife/tag_list_spec.rb b/spec/unit/knife/tag_list_spec.rb new file mode 100644 index 0000000000..0de5d5ebd8 --- /dev/null +++ b/spec/unit/knife/tag_list_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Chef::Knife::TagList do + before(:each) do + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife::TagList.new + @knife.name_args = [ Chef::Config[:node_name], "sadtag" ] + + @node = Chef::Node.new + @node.stub! :save + @node.tags << "sadtag" << "happytag" + Chef::Node.stub!(:load).and_return @node + end + + describe "run" do + it "can list tags on a node" do + expected = %w(sadtag happytag) + @node.tags.should == expected + @knife.should_receive(:output).with(expected) + @knife.run + end + end +end diff --git a/spec/unit/knife_spec.rb b/spec/unit/knife_spec.rb new file mode 100644 index 0000000000..0517014db0 --- /dev/null +++ b/spec/unit/knife_spec.rb @@ -0,0 +1,295 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Tim Hinderliter (<tim@opscode.com>) +# Copyright:: Copyright (c) 2008, 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. +# + +# Fixtures for subcommand loading live in this namespace +module KnifeSpecs +end + +require 'spec_helper' + +describe Chef::Knife do + before(:each) do + Chef::Log.logger = Logger.new(StringIO.new) + + Chef::Config[:node_name] = "webmonkey.example.com" + @knife = Chef::Knife.new + @knife.ui.stub!(:puts) + @knife.ui.stub!(:print) + Chef::Log.stub!(:init) + Chef::Log.stub!(:level) + [:debug, :info, :warn, :error, :crit].each do |level_sym| + Chef::Log.stub!(level_sym) + end + Chef::Knife.stub!(:puts) + @stdout = StringIO.new + end + + describe "after loading a subcommand" do + before do + Chef::Knife.reset_subcommands! + + if KnifeSpecs.const_defined?(:TestNameMapping) + KnifeSpecs.send(:remove_const, :TestNameMapping) + end + + if KnifeSpecs.const_defined?(:TestExplicitCategory) + KnifeSpecs.send(:remove_const, :TestExplicitCategory) + end + + Kernel.load(File.join(CHEF_SPEC_DATA, 'knife_subcommand', 'test_name_mapping.rb')) + Kernel.load(File.join(CHEF_SPEC_DATA, 'knife_subcommand', 'test_explicit_category.rb')) + end + + it "has a category based on its name" do + KnifeSpecs::TestNameMapping.subcommand_category.should == 'test' + end + + it "has an explictly defined category if set" do + KnifeSpecs::TestExplicitCategory.subcommand_category.should == 'cookbook site' + end + + it "can reference the subcommand by its snake cased name" do + Chef::Knife.subcommands['test_name_mapping'].should equal(KnifeSpecs::TestNameMapping) + end + + it "lists subcommands by category" do + Chef::Knife.subcommands_by_category['test'].should include('test_name_mapping') + end + + it "lists subcommands by category when the subcommands have explicit categories" do + Chef::Knife.subcommands_by_category['cookbook site'].should include('test_explicit_category') + end + + end + + describe "after loading all subcommands" do + before do + Chef::Knife.reset_subcommands! + Chef::Knife.load_commands + end + + it "references a subcommand class by its snake cased name" do + class SuperAwesomeCommand < Chef::Knife + end + + Chef::Knife.load_commands + + Chef::Knife.subcommands.should have_key("super_awesome_command") + Chef::Knife.subcommands["super_awesome_command"].should == SuperAwesomeCommand + end + + it "guesses a category from a given ARGV" do + Chef::Knife.subcommands_by_category["cookbook"] << :cookbook + Chef::Knife.subcommands_by_category["cookbook site"] << :cookbook_site + Chef::Knife.guess_category(%w{cookbook foo bar baz}).should == 'cookbook' + Chef::Knife.guess_category(%w{cookbook site foo bar baz}).should == 'cookbook site' + Chef::Knife.guess_category(%w{cookbook site --help}).should == 'cookbook site' + end + + it "finds a subcommand class based on ARGV" do + Chef::Knife.subcommands["cookbook_site_vendor"] = :CookbookSiteVendor + Chef::Knife.subcommands["cookbook"] = :Cookbook + Chef::Knife.subcommand_class_from(%w{cookbook site vendor --help foo bar baz}).should == :CookbookSiteVendor + end + + end + + describe "when running a command" do + before(:each) do + if KnifeSpecs.const_defined?(:TestYourself) + KnifeSpecs.send :remove_const, :TestYourself + end + Kernel.load(File.join(CHEF_SPEC_DATA, 'knife_subcommand', 'test_yourself.rb')) + Chef::Knife.subcommands.each { |name, klass| Chef::Knife.subcommands.delete(name) unless klass.kind_of?(Class) } + end + + it "merges the global knife CLI options" do + extra_opts = {} + extra_opts[:editor] = {:long=>"--editor EDITOR", + :description=>"Set the editor to use for interactive commands", + :short=>"-e EDITOR", + :default=>"/usr/bin/vim"} + + # there is special hackery to return the subcommand instance going on here. + command = Chef::Knife.run(%w{test yourself}, extra_opts) + editor_opts = command.options[:editor] + editor_opts[:long].should == "--editor EDITOR" + editor_opts[:description].should == "Set the editor to use for interactive commands" + editor_opts[:short].should == "-e EDITOR" + editor_opts[:default].should == "/usr/bin/vim" + end + + it "creates an instance of the subcommand and runs it" do + command = Chef::Knife.run(%w{test yourself}) + command.should be_an_instance_of(KnifeSpecs::TestYourself) + command.ran.should be_true + end + + it "passes the command specific args to the subcommand" do + command = Chef::Knife.run(%w{test yourself with some args}) + command.name_args.should == %w{with some args} + end + + it "excludes the command name from the name args when parts are joined with underscores" do + command = Chef::Knife.run(%w{test_yourself with some args}) + command.name_args.should == %w{with some args} + end + + it "exits if no subcommand matches the CLI args" do + Chef::Knife.ui.stub!(:stdout).and_return(@stdout) + Chef::Knife.ui.should_receive(:fatal) + lambda {Chef::Knife.run(%w{fuuu uuuu fuuuu})}.should raise_error(SystemExit) { |e| e.status.should_not == 0 } + end + + end + + describe "when first created" do + before do + unless KnifeSpecs.const_defined?(:TestYourself) + Kernel.load(File.join(CHEF_SPEC_DATA, 'knife_subcommand', 'test_yourself.rb')) + end + @knife = KnifeSpecs::TestYourself.new(%w{with some args -s scrogramming}) + end + + it "it parses the options passed to it" do + @knife.config[:scro].should == 'scrogramming' + end + + it "extracts its command specific args from the full arg list" do + @knife.name_args.should == %w{with some args} + end + + end + + describe "when formatting exceptions" do + before do + @stdout, @stderr, @stdin = StringIO.new, StringIO.new, StringIO.new + @knife.ui = Chef::Knife::UI.new(@stdout, @stderr, @stdin, {}) + @knife.should_receive(:exit).with(100) + end + + it "formats 401s nicely" do + response = Net::HTTPUnauthorized.new("1.1", "401", "Unauthorized") + response.instance_variable_set(:@read, true) # I hate you, net/http. + response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "y u no syncronize your clock?")) + @knife.stub!(:run).and_raise(Net::HTTPServerException.new("401 Unauthorized", response)) + @knife.run_with_pretty_exceptions + @stderr.string.should match(/ERROR: Failed to authenticate to/) + @stdout.string.should match(/Response: y u no syncronize your clock\?/) + end + + it "formats 403s nicely" do + response = Net::HTTPForbidden.new("1.1", "403", "Forbidden") + response.instance_variable_set(:@read, true) # I hate you, net/http. + response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "y u no administrator")) + @knife.stub!(:run).and_raise(Net::HTTPServerException.new("403 Forbidden", response)) + @knife.stub!(:username).and_return("sadpanda") + @knife.run_with_pretty_exceptions + @stderr.string.should match(%r[ERROR: You authenticated successfully to http.+ as sadpanda but you are not authorized for this action]) + @stdout.string.should match(%r[Response: y u no administrator]) + end + + it "formats 400s nicely" do + response = Net::HTTPBadRequest.new("1.1", "400", "Bad Request") + response.instance_variable_set(:@read, true) # I hate you, net/http. + response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "y u search wrong")) + @knife.stub!(:run).and_raise(Net::HTTPServerException.new("400 Bad Request", response)) + @knife.run_with_pretty_exceptions + @stderr.string.should match(%r[ERROR: The data in your request was invalid]) + @stdout.string.should match(%r[Response: y u search wrong]) + end + + it "formats 404s nicely" do + response = Net::HTTPNotFound.new("1.1", "404", "Not Found") + response.instance_variable_set(:@read, true) # I hate you, net/http. + response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "nothing to see here")) + @knife.stub!(:run).and_raise(Net::HTTPServerException.new("404 Not Found", response)) + @knife.run_with_pretty_exceptions + @stderr.string.should match(%r[ERROR: The object you are looking for could not be found]) + @stdout.string.should match(%r[Response: nothing to see here]) + end + + it "formats 500s nicely" do + response = Net::HTTPInternalServerError.new("1.1", "500", "Internal Server Error") + response.instance_variable_set(:@read, true) # I hate you, net/http. + response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "sad trombone")) + @knife.stub!(:run).and_raise(Net::HTTPFatalError.new("500 Internal Server Error", response)) + @knife.run_with_pretty_exceptions + @stderr.string.should match(%r[ERROR: internal server error]) + @stdout.string.should match(%r[Response: sad trombone]) + end + + it "formats 502s nicely" do + response = Net::HTTPBadGateway.new("1.1", "502", "Bad Gateway") + response.instance_variable_set(:@read, true) # I hate you, net/http. + response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "sadder trombone")) + @knife.stub!(:run).and_raise(Net::HTTPFatalError.new("502 Bad Gateway", response)) + @knife.run_with_pretty_exceptions + @stderr.string.should match(%r[ERROR: bad gateway]) + @stdout.string.should match(%r[Response: sadder trombone]) + end + + it "formats 503s nicely" do + response = Net::HTTPServiceUnavailable.new("1.1", "503", "Service Unavailable") + response.instance_variable_set(:@read, true) # I hate you, net/http. + response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "saddest trombone")) + @knife.stub!(:run).and_raise(Net::HTTPFatalError.new("503 Service Unavailable", response)) + @knife.run_with_pretty_exceptions + @stderr.string.should match(%r[ERROR: Service temporarily unavailable]) + @stdout.string.should match(%r[Response: saddest trombone]) + end + + it "formats other HTTP errors nicely" do + response = Net::HTTPPaymentRequired.new("1.1", "402", "Payment Required") + response.instance_variable_set(:@read, true) # I hate you, net/http. + response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "nobugfixtillyoubuy")) + @knife.stub!(:run).and_raise(Net::HTTPServerException.new("402 Payment Required", response)) + @knife.run_with_pretty_exceptions + @stderr.string.should match(%r[ERROR: Payment Required]) + @stdout.string.should match(%r[Response: nobugfixtillyoubuy]) + end + + it "formats NameError and NoMethodError nicely" do + @knife.stub!(:run).and_raise(NameError.new("Undefined constant FUUU")) + @knife.run_with_pretty_exceptions + @stderr.string.should match(%r[ERROR: knife encountered an unexpected error]) + @stdout.string.should match(%r[This may be a bug in the 'knife' knife command or plugin]) + @stdout.string.should match(%r[Exception: NameError: Undefined constant FUUU]) + end + + it "formats missing private key errors nicely" do + @knife.stub!(:run).and_raise(Chef::Exceptions::PrivateKeyMissing.new('key not there')) + @knife.stub!(:api_key).and_return("/home/root/.chef/no-key-here.pem") + @knife.run_with_pretty_exceptions + @stderr.string.should match(%r[ERROR: Your private key could not be loaded from /home/root/.chef/no-key-here.pem]) + @stdout.string.should match(%r[Check your configuration file and ensure that your private key is readable]) + end + + it "formats connection refused errors nicely" do + @knife.stub!(:run).and_raise(Errno::ECONNREFUSED.new('y u no shut up')) + @knife.run_with_pretty_exceptions + # Errno::ECONNREFUSED message differs by platform + # *nix = Errno::ECONNREFUSED: Connection refused + # win32: Errno::ECONNREFUSED: No connection could be made because the target machine actively refused it. + @stderr.string.should match(%r[ERROR: Network Error: .* - y u no shut up]) + @stdout.string.should match(%r[Check your knife configuration and network settings]) + end + end + +end diff --git a/spec/unit/log_spec.rb b/spec/unit/log_spec.rb new file mode 100644 index 0000000000..b91ee020bb --- /dev/null +++ b/spec/unit/log_spec.rb @@ -0,0 +1,24 @@ +# +# 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 'tempfile' +require 'logger' +require 'spec_helper' + +describe Chef::Log do +end diff --git a/spec/unit/lwrp_spec.rb b/spec/unit/lwrp_spec.rb new file mode 100644 index 0000000000..da2278e547 --- /dev/null +++ b/spec/unit/lwrp_spec.rb @@ -0,0 +1,231 @@ +# +# Author:: Christopher Walters (<cw@opscode.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 "override logging" do + before :each do + $stderr.stub!(:write) + end + + it "should log if attempting to load resource of same name" do + Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp", "resources", "*"))].each do |file| + Chef::Resource.build_from_file("lwrp", file, nil) + end + + Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp_override", "resources", "*"))].each do |file| + Chef::Log.should_receive(:info).with(/overriding/) + Chef::Resource.build_from_file("lwrp", file, nil) + end + end + + it "should log if attempting to load provider of same name" do + Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp", "providers", "*"))].each do |file| + Chef::Provider.build_from_file("lwrp", file, nil) + end + + Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp_override", "providers", "*"))].each do |file| + Chef::Log.should_receive(:info).with(/overriding/) + Chef::Provider.build_from_file("lwrp", file, nil) + end + end + +end + +describe "LWRP" do + before do + @original_VERBOSE = $VERBOSE + $VERBOSE = nil + end + + after do + $VERBOSE = @original_VERBOSE + end + + describe "Lightweight Chef::Resource" do + + before do + Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp", "resources", "*"))].each do |file| + Chef::Resource.build_from_file("lwrp", file, nil) + end + + Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp_override", "resources", "*"))].each do |file| + Chef::Resource.build_from_file("lwrp", file, nil) + end + end + + it "should load the resource into a properly-named class" do + Chef::Resource.const_get("LwrpFoo").should be_kind_of(Class) + end + + it "should set resource_name" do + Chef::Resource::LwrpFoo.new("blah").resource_name.should eql(:lwrp_foo) + end + + it "should add the specified actions to the allowed_actions array" do + Chef::Resource::LwrpFoo.new("blah").allowed_actions.should include(:pass_buck, :twiddle_thumbs) + end + + it "should set the specified action as the default action" do + Chef::Resource::LwrpFoo.new("blah").action.should == :pass_buck + end + + it "should create a method for each attribute" do + Chef::Resource::LwrpFoo.new("blah").methods.map{ |m| m.to_sym}.should include(:monkey) + end + + it "should build attribute methods that respect validation rules" do + lambda { Chef::Resource::LwrpFoo.new("blah").monkey(42) }.should raise_error(ArgumentError) + end + + it "should have access to the run context and node during class definition" do + node = Chef::Node.new + node.normal[:penguin_name] = "jackass" + run_context = Chef::RunContext.new(node, Chef::CookbookCollection.new, @events) + + Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp", "resources_with_default_attributes", "*"))].each do |file| + Chef::Resource.build_from_file("lwrp", file, run_context) + end + + cls = Chef::Resource.const_get("LwrpNodeattr") + cls.node.should be_kind_of(Chef::Node) + cls.run_context.should be_kind_of(Chef::RunContext) + cls.node[:penguin_name].should eql("jackass") + end + + end + + describe "Lightweight Chef::Provider" do + before do + @node = Chef::Node.new + @node.automatic[:platform] = :ubuntu + @node.automatic[:platform_version] = '8.10' + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, Chef::CookbookCollection.new({}), @events) + @runner = Chef::Runner.new(@run_context) + end + + before(:each) do + Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp", "resources", "*"))].each do |file| + Chef::Resource.build_from_file("lwrp", file, @run_context) + end + + Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp_override", "resources", "*"))].each do |file| + Chef::Resource.build_from_file("lwrp", file, @run_context) + end + + Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp", "providers", "*"))].each do |file| + Chef::Provider.build_from_file("lwrp", file, @run_context) + end + + Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp_override", "providers", "*"))].each do |file| + Chef::Provider.build_from_file("lwrp", file, @run_context) + end + + end + + it "should properly handle a new_resource reference" do + resource = Chef::Resource::LwrpFoo.new("morpheus") + resource.monkey("bob") + resource.provider(:lwrp_monkey_name_printer) + resource.run_context = @run_context + + provider = Chef::Platform.provider_for_resource(resource, :twiddle_thumbs) + provider.action_twiddle_thumbs + end + + it "should load the provider into a properly-named class" do + Chef::Provider.const_get("LwrpBuckPasser").should be_kind_of(Class) + end + + it "should create a method for each attribute" do + new_resource = mock("new resource", :null_object=>true) + Chef::Provider::LwrpBuckPasser.new(nil, new_resource).methods.map{|m|m.to_sym}.should include(:action_pass_buck) + Chef::Provider::LwrpThumbTwiddler.new(nil, new_resource).methods.map{|m|m.to_sym}.should include(:action_twiddle_thumbs) + end + + it "should insert resources embedded in the provider into the middle of the resource collection" do + injector = Chef::Resource::LwrpFoo.new("morpheus", @run_context) + injector.action(:pass_buck) + injector.provider(:lwrp_buck_passer) + dummy = Chef::Resource::ZenMaster.new("keanu reeves", @run_context) + dummy.provider(Chef::Provider::Easy) + @run_context.resource_collection.insert(injector) + @run_context.resource_collection.insert(dummy) + + Chef::Runner.new(@run_context).converge + + @run_context.resource_collection[0].should eql(injector) + @run_context.resource_collection[1].name.should eql(:prepared_thumbs) + @run_context.resource_collection[2].name.should eql(:twiddled_thumbs) + @run_context.resource_collection[3].should eql(dummy) + end + + it "should insert embedded resources from multiple providers, including from the last position, properly into the resource collection" do + injector = Chef::Resource::LwrpFoo.new("morpheus", @run_context) + injector.action(:pass_buck) + injector.provider(:lwrp_buck_passer) + + injector2 = Chef::Resource::LwrpBar.new("tank", @run_context) + injector2.action(:pass_buck) + injector2.provider(:lwrp_buck_passer_2) + + dummy = Chef::Resource::ZenMaster.new("keanu reeves", @run_context) + dummy.provider(Chef::Provider::Easy) + + @run_context.resource_collection.insert(injector) + @run_context.resource_collection.insert(dummy) + @run_context.resource_collection.insert(injector2) + + Chef::Runner.new(@run_context).converge + + @run_context.resource_collection[0].should eql(injector) + @run_context.resource_collection[1].name.should eql(:prepared_thumbs) + @run_context.resource_collection[2].name.should eql(:twiddled_thumbs) + @run_context.resource_collection[3].should eql(dummy) + @run_context.resource_collection[4].should eql(injector2) + @run_context.resource_collection[5].name.should eql(:prepared_eyes) + @run_context.resource_collection[6].name.should eql(:dried_paint_watched) + end + + it "should properly handle a new_resource reference" do + resource = Chef::Resource::LwrpFoo.new("morpheus", @run_context) + resource.monkey("bob") + resource.provider(:lwrp_monkey_name_printer) + + provider = Chef::Platform.provider_for_resource(resource, :twiddle_thumbs) + provider.action_twiddle_thumbs + + provider.monkey_name.should == "my monkey's name is 'bob'" + end + + it "should properly handle an embedded Resource accessing the enclosing Provider's scope" do + resource = Chef::Resource::LwrpFoo.new("morpheus", @run_context) + resource.monkey("bob") + resource.provider(:lwrp_embedded_resource_accesses_providers_scope) + + provider = Chef::Platform.provider_for_resource(resource, :twiddle_thumbs) + #provider = @runner.build_provider(resource) + provider.action_twiddle_thumbs + + provider.enclosed_resource.monkey.should == 'bob, the monkey' + end + + end + +end diff --git a/spec/unit/mash_spec.rb b/spec/unit/mash_spec.rb new file mode 100644 index 0000000000..b9a7cd0932 --- /dev/null +++ b/spec/unit/mash_spec.rb @@ -0,0 +1,51 @@ +# +# Author:: Matthew Kent (<mkent@magoazul.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' +require 'chef/mash' + +describe Mash do + it "should duplicate a simple key/value mash to a new mash" do + data = {:x=>"one", :y=>"two", :z=>"three"} + @orig = Mash.new(data) + @copy = @orig.dup + @copy.to_hash.should == Mash.new(data).to_hash + @copy[:x] = "four" + @orig[:x].should == "one" + end + + it "should duplicate a mash with an array to a new mash" do + data = {:x=>"one", :y=>"two", :z=>[1,2,3]} + @orig = Mash.new(data) + @copy = @orig.dup + @copy.to_hash.should == Mash.new(data).to_hash + @copy[:z] << 4 + @orig[:z].should == [1,2,3] + end + + it "should duplicate a nested mash to a new mash" do + data = {:x=>"one", :y=>"two", :z=>Mash.new({:a=>[1,2,3]})} + @orig = Mash.new(data) + @copy = @orig.dup + @copy.to_hash.should == Mash.new(data).to_hash + @copy[:z][:a] << 4 + @orig[:z][:a].should == [1,2,3] + end + + # add more! +end diff --git a/spec/unit/mixin/checksum_spec.rb b/spec/unit/mixin/checksum_spec.rb new file mode 100644 index 0000000000..dec270e18f --- /dev/null +++ b/spec/unit/mixin/checksum_spec.rb @@ -0,0 +1,41 @@ +# +# Author:: Adam Jacob (<adam@opscode.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' +require 'chef/mixin/checksum' +require 'stringio' + +class Chef::CMCCheck + include Chef::Mixin::Checksum +end + +describe Chef::Mixin::Checksum do + before(:each) do + @checksum_user = Chef::CMCCheck.new + @cache = Chef::ChecksumCache.instance + @file = CHEF_SPEC_DATA + "/checksum/random.txt" + @stat = mock("File::Stat", { :mtime => Time.at(0) }) + File.stub!(:stat).and_return(@stat) + end + + it "gets the checksum of a file" do + @checksum_user.checksum(@file).should == "09ee9c8cc70501763563bcf9c218d71b2fbf4186bf8e1e0da07f0f42c80a3394" + end + +end + diff --git a/spec/unit/mixin/command_spec.rb b/spec/unit/mixin/command_spec.rb new file mode 100644 index 0000000000..e143b8728b --- /dev/null +++ b/spec/unit/mixin/command_spec.rb @@ -0,0 +1,105 @@ +# +# Author:: Hongli Lai (hongli@phusion.nl) +# Copyright:: Copyright (c) 2009 Phusion +# 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::Mixin::Command do + + if windows? + + pending("TODO MOVE: this is a platform specific integration test.") + + else + + describe "popen4" do + include Chef::Mixin::Command + + it "should be possible to read the child process's stdout and stderr" do + popen4("sh -c 'echo hello && echo world >&2'") do |pid, stdin, stdout, stderr| + stdout.read.should == "hello\n" + stderr.read.should == "world\n" + end + end + + it "should default all commands to be run in the POSIX standard C locale" do + popen4("echo $LC_ALL") do |pid, stdin, stdout, stderr| + stdout.read.strip.should == "C" + end + end + + it "should respect locale when specified explicitly" do + popen4("echo $LC_ALL", :environment => {"LC_ALL" => "es"}) do |pid, stdin, stdout, stderr| + stdout.read.strip.should == "es" + end + end + + it "should end when the child process reads from STDIN and a block is given" do + lambda {Timeout.timeout(10) do + popen4("ruby -e 'while gets; end'", :waitlast => true) do |pid, stdin, stdout, stderr| + (1..5).each { |i| stdin.puts "#{i}" } + end + end + }.should_not raise_error + end + + describe "when a process detaches but doesn't close STDOUT and STDERR [CHEF-584]" do + + it "returns immediately after the first child process exits" do + lambda {Timeout.timeout(10) do + pid, stdin,stdout,stderr = nil,nil,nil,nil + evil_forker="exit if fork; 10.times { sleep 1}" + popen4("ruby -e '#{evil_forker}'") do |pid,stdin,stdout,stderr| + end + end}.should_not raise_error + end + + end + + end + + describe "run_command" do + include Chef::Mixin::Command + + it "logs the command's stderr and stdout output if the command failed" do + Chef::Log.stub!(:level).and_return(:debug) + begin + run_command(:command => "sh -c 'echo hello; echo world >&2; false'") + violated "Exception expected, but nothing raised." + rescue => e + e.message.should =~ /STDOUT: hello/ + e.message.should =~ /STDERR: world/ + end + end + + describe "when a process detaches but doesn't close STDOUT and STDERR [CHEF-584]" do + it "returns successfully" do + # CHEF-2916 might have added a slight delay here, or our CI + # infrastructure is burdened. Bumping timeout from 2 => 4 -- + # btm + # Serdar - During Solaris tests, we've seen that processes + # are taking a long time to exit. Bumping timeout now to 10. + lambda {Timeout.timeout(10) do + evil_forker="exit if fork; 10.times { sleep 1}" + run_command(:command => "ruby -e '#{evil_forker}'") + end}.should_not raise_error + end + + end + end + end +end diff --git a/spec/unit/mixin/convert_to_class_name_spec.rb b/spec/unit/mixin/convert_to_class_name_spec.rb new file mode 100644 index 0000000000..b78d3f9101 --- /dev/null +++ b/spec/unit/mixin/convert_to_class_name_spec.rb @@ -0,0 +1,50 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2009 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' + +class ConvertToClassTestHarness + include Chef::Mixin::ConvertToClassName +end + +describe Chef::Mixin::ConvertToClassName do + + before do + @convert = ConvertToClassTestHarness.new + end + + it "converts a_snake_case_word to a CamelCaseWord" do + @convert.convert_to_class_name("now_camelized").should == "NowCamelized" + end + + it "converts a CamelCaseWord to a snake_case_word" do + @convert.convert_to_snake_case("NowImASnake").should == "now_im_a_snake" + end + + it "removes the base classes before snake casing" do + @convert.convert_to_snake_case("NameSpaced::Class::ThisIsWin", "NameSpaced::Class").should == "this_is_win" + end + + it "removes the base classes without explicitly naming them and returns snake case" do + @convert.snake_case_basename("NameSpaced::Class::ExtraWin").should == "extra_win" + end + + it "interprets non-alphanumeric characters in snake case as word boundaries" do + @convert.convert_to_class_name("now_camelized_without-hyphen").should == "NowCamelizedWithoutHyphen" + end +end diff --git a/spec/unit/mixin/deep_merge_spec.rb b/spec/unit/mixin/deep_merge_spec.rb new file mode 100644 index 0000000000..cbc9b1544f --- /dev/null +++ b/spec/unit/mixin/deep_merge_spec.rb @@ -0,0 +1,314 @@ +# +# Author:: Matthew Kent (<mkent@magoazul.com>) +# Author:: Steve Midgley (http://www.misuse.org/science) +# Copyright:: Copyright (c) 2010 Matthew Kent +# Copyright:: Copyright (c) 2008 Steve Midgley +# 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. + +# Notice: +# This code is imported from deep_merge by Steve Midgley. deep_merge is +# available under the MIT license from +# http://trac.misuse.org/science/wiki/DeepMerge + +require 'spec_helper' + +# Test coverage from the original author converted to rspec +describe Chef::Mixin::DeepMerge, "deep_merge!" do + before do + @dm = Chef::Mixin::DeepMerge + @field_ko_prefix = '!merge' + end + + # deep_merge core tests - moving from basic to more complex + + it "tests merging an hash w/array into blank hash" do + hash_src = {'id' => '2'} + hash_dst = {} + @dm.deep_merge!(hash_src.dup, hash_dst) + hash_dst.should == hash_src + end + + it "tests merging an hash w/array into blank hash" do + hash_src = {'region' => {'id' => ['227', '2']}} + hash_dst = {} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == hash_src + end + + it "tests merge from empty hash" do + hash_src = {} + hash_dst = {"property" => ["2","4"]} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => ["2","4"]} + end + + it "tests merge to empty hash" do + hash_src = {"property" => ["2","4"]} + hash_dst = {} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => ["2","4"]} + end + + it "tests simple string overwrite" do + hash_src = {"name" => "value"} + hash_dst = {"name" => "value1"} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"name" => "value"} + end + + it "tests simple string overwrite of empty hash" do + hash_src = {"name" => "value"} + hash_dst = {} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == hash_src + end + + it "tests hashes holding array" do + hash_src = {"property" => ["1","3"]} + hash_dst = {"property" => ["2","4"]} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => ["2","4","1","3"]} + end + + it "tests hashes holding hashes holding arrays (array with duplicate elements is merged with dest then src" do + hash_src = {"property" => {"bedroom_count" => ["1", "2"], "bathroom_count" => ["1", "4+"]}} + hash_dst = {"property" => {"bedroom_count" => ["3", "2"], "bathroom_count" => ["2"]}} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => {"bedroom_count" => ["3","2","1"], "bathroom_count" => ["2", "1", "4+"]}} + end + + it "tests hash holding hash holding array v string (string is overwritten by array)" do + hash_src = {"property" => {"bedroom_count" => ["1", "2"], "bathroom_count" => ["1", "4+"]}} + hash_dst = {"property" => {"bedroom_count" => "3", "bathroom_count" => ["2"]}} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => {"bedroom_count" => ["1", "2"], "bathroom_count" => ["2","1","4+"]}} + end + + it "tests hash holding hash holding string v array (array is overwritten by string)" do + hash_src = {"property" => {"bedroom_count" => "3", "bathroom_count" => ["1", "4+"]}} + hash_dst = {"property" => {"bedroom_count" => ["1", "2"], "bathroom_count" => ["2"]}} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => {"bedroom_count" => "3", "bathroom_count" => ["2","1","4+"]}} + end + + it "tests hash holding hash holding hash v array (array is overwritten by hash)" do + hash_src = {"property" => {"bedroom_count" => {"king_bed" => 3, "queen_bed" => 1}, "bathroom_count" => ["1", "4+"]}} + hash_dst = {"property" => {"bedroom_count" => ["1", "2"], "bathroom_count" => ["2"]}} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => {"bedroom_count" => {"king_bed" => 3, "queen_bed" => 1}, "bathroom_count" => ["2","1","4+"]}} + end + + it "tests 3 hash layers holding integers (integers are overwritten by source)" do + hash_src = {"property" => {"bedroom_count" => {"king_bed" => 3, "queen_bed" => 1}, "bathroom_count" => ["1", "4+"]}} + hash_dst = {"property" => {"bedroom_count" => {"king_bed" => 2, "queen_bed" => 4}, "bathroom_count" => ["2"]}} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => {"bedroom_count" => {"king_bed" => 3, "queen_bed" => 1}, "bathroom_count" => ["2","1","4+"]}} + end + + it "tests 3 hash layers holding arrays of int (arrays are merged)" do + hash_src = {"property" => {"bedroom_count" => {"king_bed" => [3], "queen_bed" => [1]}, "bathroom_count" => ["1", "4+"]}} + hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => {"bedroom_count" => {"king_bed" => [2,3], "queen_bed" => [4,1]}, "bathroom_count" => ["2","1","4+"]}} + end + + it "tests 1 hash overwriting 3 hash layers holding arrays of int" do + hash_src = {"property" => "1"} + hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => "1"} + end + + it "tests 3 hash layers holding arrays of int (arrays are merged) but second hash's array is overwritten" do + hash_src = {"property" => {"bedroom_count" => {"king_bed" => [3], "queen_bed" => [1]}, "bathroom_count" => "1"}} + hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => {"bedroom_count" => {"king_bed" => [2,3], "queen_bed" => [4,1]}, "bathroom_count" => "1"}} + end + + it "tests 3 hash layers holding arrays of int, but one holds int. This one overwrites, but the rest merge" do + hash_src = {"property" => {"bedroom_count" => {"king_bed" => 3, "queen_bed" => [1]}, "bathroom_count" => ["1"]}} + hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => {"bedroom_count" => {"king_bed" => 3, "queen_bed" => [4,1]}, "bathroom_count" => ["2","1"]}} + end + + it "tests 3 hash layers holding arrays of int, but source is incomplete." do + hash_src = {"property" => {"bedroom_count" => {"king_bed" => [3]}, "bathroom_count" => ["1"]}} + hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => {"bedroom_count" => {"king_bed" => [2,3], "queen_bed" => [4]}, "bathroom_count" => ["2","1"]}} + end + + it "tests 3 hash layers holding arrays of int, but source is shorter and has new 2nd level ints." do + hash_src = {"property" => {"bedroom_count" => {2=>3, "king_bed" => [3]}, "bathroom_count" => ["1"]}} + hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => {"bedroom_count" => {2=>3, "king_bed" => [2,3], "queen_bed" => [4]}, "bathroom_count" => ["2","1"]}} + end + + it "tests 3 hash layers holding arrays of int, but source is empty" do + hash_src = {} + hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} + end + + it "tests 3 hash layers holding arrays of int, but dest is empty" do + hash_src = {"property" => {"bedroom_count" => {2=>3, "king_bed" => [3]}, "bathroom_count" => ["1"]}} + hash_dst = {} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"property" => {"bedroom_count" => {2=>3, "king_bed" => [3]}, "bathroom_count" => ["1"]}} + end + + it "tests hash holding arrays of arrays" do + hash_src = {["1", "2", "3"] => ["1", "2"]} + hash_dst = {["4", "5"] => ["3"]} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {["1","2","3"] => ["1", "2"], ["4", "5"] => ["3"]} + end + + it "tests merging of hash with blank hash, and make sure that source array split does not function when turned off" do + hash_src = {'property' => {'bedroom_count' => ["1","2,3"]}} + hash_dst = {} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {'property' => {'bedroom_count' => ["1","2,3"]}} + end + + it "tests merging into a blank hash" do + hash_src = {"action"=>"browse", "controller"=>"results"} + hash_dst = {} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == hash_src + end + + it "tests are unmerged hashes passed unmodified w/out :unpack_arrays?" do + hash_src = {"amenity"=>{"id"=>["26,27"]}} + hash_dst = {} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"amenity"=>{"id"=>["26,27"]}} + end + + it "tests hash of array of hashes" do + hash_src = {"item" => [{"1" => "3"}, {"2" => "4"}]} + hash_dst = {"item" => [{"3" => "5"}]} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"item" => [{"3" => "5"}, {"1" => "3"}, {"2" => "4"}]} + end + + # Additions since import + it "should overwrite true with false when merging boolean values" do + hash_src = {"valid" => false} + hash_dst = {"valid" => true} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"valid" => false} + end + + it "should overwrite false with true when merging boolean values" do + hash_src = {"valid" => true} + hash_dst = {"valid" => false} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"valid" => true} + end + + it "should overwrite a string with an empty string when merging string values" do + hash_src = {"item" => " "} + hash_dst = {"item" => "orange"} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"item" => " "} + end + + it "should overwrite an empty string with a string when merging string values" do + hash_src = {"item" => "orange"} + hash_dst = {"item" => " "} + @dm.deep_merge!(hash_src, hash_dst) + hash_dst.should == {"item" => "orange"} + end +end # deep_merge! + +# Chef specific +describe Chef::Mixin::DeepMerge do + before do + @dm = Chef::Mixin::DeepMerge + end + + describe "merge" do + it "should merge a hash into an empty hash" do + hash_dst = {} + hash_src = {'id' => '2'} + @dm.merge(hash_dst, hash_src).should == hash_src + end + + it "should merge a nested hash into an empty hash" do + hash_dst = {} + hash_src = {'region' => {'id' => ['227', '2']}} + @dm.merge(hash_dst, hash_src).should == hash_src + end + + it "should overwrite as string value when merging hashes" do + hash_dst = {"name" => "value1"} + hash_src = {"name" => "value"} + @dm.merge(hash_dst, hash_src).should == {"name" => "value"} + end + + it "should merge arrays within hashes" do + hash_dst = {"property" => ["2","4"]} + hash_src = {"property" => ["1","3"]} + @dm.merge(hash_dst, hash_src).should == {"property" => ["2","4","1","3"]} + end + + it "should merge deeply nested hashes" do + hash_dst = {"property" => {"values" => {"are" => "falling", "can" => "change"}}} + hash_src = {"property" => {"values" => {"are" => "stable", "may" => "rise"}}} + @dm.merge(hash_dst, hash_src).should == {"property" => {"values" => {"are" => "stable", "can" => "change", "may" => "rise"}}} + end + + it "should not modify the source or destination during the merge" do + hash_dst = {"property" => ["1","2","3"]} + hash_src = {"property" => ["4","5","6"]} + ret = @dm.merge(hash_dst, hash_src) + hash_dst.should == {"property" => ["1","2","3"]} + hash_src.should == {"property" => ["4","5","6"]} + ret.should == {"property" => ["1","2","3","4","5","6"]} + end + + end + + describe "role_merge" do + it "errors out if knockout merge use is detected in an array" do + hash_dst = {"property" => ["2","4"]} + hash_src = {"property" => ["1","!merge:4"]} + lambda {@dm.role_merge(hash_dst, hash_src)}.should raise_error(Chef::Mixin::DeepMerge::InvalidSubtractiveMerge) + end + + it "errors out if knockout merge use is detected in an array (reversed merge order)" do + hash_dst = {"property" => ["1","!merge:4"]} + hash_src = {"property" => ["2","4"]} + lambda {@dm.role_merge(hash_dst, hash_src)}.should raise_error(Chef::Mixin::DeepMerge::InvalidSubtractiveMerge) + end + + it "errors out if knockout merge use is detected in a string" do + hash_dst = {"property" => ["2","4"]} + hash_src = {"property" => "!merge"} + lambda {@dm.role_merge(hash_dst, hash_src)}.should raise_error(Chef::Mixin::DeepMerge::InvalidSubtractiveMerge) + end + + it "errors out if knockout merge use is detected in a string (reversed merge order)" do + hash_dst = {"property" => "!merge"} + hash_src= {"property" => ["2","4"]} + lambda {@dm.role_merge(hash_dst, hash_src)}.should raise_error(Chef::Mixin::DeepMerge::InvalidSubtractiveMerge) + end + end +end diff --git a/spec/unit/mixin/deprecation_spec.rb b/spec/unit/mixin/deprecation_spec.rb new file mode 100644 index 0000000000..1b62dcd124 --- /dev/null +++ b/spec/unit/mixin/deprecation_spec.rb @@ -0,0 +1,34 @@ +# +# 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 'chef/mixin/deprecation' + +describe Chef::Mixin::Deprecation::DeprecatedInstanceVariable do + before do + Chef::Log.logger = Logger.new(StringIO.new) + + @deprecated_ivar = Chef::Mixin::Deprecation::DeprecatedInstanceVariable.new('value', 'an_ivar') + end + + it "forward method calls to the target object" do + @deprecated_ivar.length.should == 5 + @deprecated_ivar.to_sym.should == :value + end + +end diff --git a/spec/unit/mixin/enforce_ownership_and_permissions_spec.rb b/spec/unit/mixin/enforce_ownership_and_permissions_spec.rb new file mode 100644 index 0000000000..53a8260cdc --- /dev/null +++ b/spec/unit/mixin/enforce_ownership_and_permissions_spec.rb @@ -0,0 +1,93 @@ +# +# Author:: Mark Mzyk (<mmzyk@opscode.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' +require 'etc' +require 'ostruct' + +describe Chef::Mixin::EnforceOwnershipAndPermissions do + + before(:each) do + @node = Chef::Node.new + @node.name "make_believe" + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @tmpdir = Dir.mktmpdir + @resource = Chef::Resource::File.new("#{@tmpdir}/madeup.txt") + FileUtils.touch @resource.path + @resource.owner "adam" + @provider = Chef::Provider::File.new(@resource, @run_context) + @provider.current_resource = @resource + end + + after(:each) do + FileUtils.rm_rf(@tmpdir) + end + + it "should call set_all on the file access control object" do + Chef::FileAccessControl.any_instance.should_receive(:set_all) + @provider.enforce_ownership_and_permissions + end + + context "when nothing was updated" do + before do + Chef::FileAccessControl.any_instance.stub(:uid_from_resource).and_return(0) + Chef::FileAccessControl.any_instance.stub(:requires_changes?).and_return(false) + + 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 + + it "does not set updated_by_last_action on the new resource" do + @provider.new_resource.should_not_receive(:updated_by_last_action) + + Chef::FileAccessControl.any_instance.stub(:set_all) + @provider.run_action(:create) + end + + end + + context "when something was modified" do + before do + Chef::FileAccessControl.any_instance.stub(:requires_changes?).and_return(true) + Chef::FileAccessControl.any_instance.stub(:uid_from_resource).and_return(0) + + 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 + + it "sets updated_by_last_action on the new resource" do + @provider.new_resource.should_receive(:updated_by_last_action) + Chef::FileAccessControl.any_instance.stub(:set_all) + @provider.run_action(:create) + end + end + +end diff --git a/spec/unit/mixin/params_validate_spec.rb b/spec/unit/mixin/params_validate_spec.rb new file mode 100644 index 0000000000..b79156109b --- /dev/null +++ b/spec/unit/mixin/params_validate_spec.rb @@ -0,0 +1,372 @@ +# +# 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' + +class TinyClass + include Chef::Mixin::ParamsValidate + + def music(is_good=true) + is_good + end +end + +describe Chef::Mixin::ParamsValidate do + before(:each) do + @vo = TinyClass.new() + end + + it "should allow a hash and a hash as arguments to validate" do + lambda { @vo.validate({:one => "two"}, {}) }.should_not raise_error(ArgumentError) + end + + it "should raise an argument error if validate is called incorrectly" do + lambda { @vo.validate("one", "two") }.should raise_error(ArgumentError) + end + + it "should require validation map keys to be symbols or strings" do + lambda { @vo.validate({:one => "two"}, { :one => true }) }.should_not raise_error(ArgumentError) + lambda { @vo.validate({:one => "two"}, { "one" => true }) }.should_not raise_error(ArgumentError) + lambda { @vo.validate({:one => "two"}, { Hash.new => true }) }.should raise_error(ArgumentError) + end + + it "should allow options to be required with true" do + lambda { @vo.validate({:one => "two"}, { :one => true }) }.should_not raise_error(ArgumentError) + end + + it "should allow options to be optional with false" do + lambda { @vo.validate({}, {:one => false})}.should_not raise_error(ArgumentError) + end + + it "should allow you to check what kind_of? thing an argument is with kind_of" do + lambda { + @vo.validate( + {:one => "string"}, + { + :one => { + :kind_of => String + } + } + ) + }.should_not raise_error(ArgumentError) + + lambda { + @vo.validate( + {:one => "string"}, + { + :one => { + :kind_of => Array + } + } + ) + }.should raise_error(ArgumentError) + end + + it "should allow you to specify an argument is required with required" do + lambda { + @vo.validate( + {:one => "string"}, + { + :one => { + :required => true + } + } + ) + }.should_not raise_error(ArgumentError) + + lambda { + @vo.validate( + {:two => "string"}, + { + :one => { + :required => true + } + } + ) + }.should raise_error(ArgumentError) + + lambda { + @vo.validate( + {:two => "string"}, + { + :one => { + :required => false + } + } + ) + }.should_not raise_error(ArgumentError) + end + + it "should allow you to specify whether an object has a method with respond_to" do + lambda { + @vo.validate( + {:one => @vo}, + { + :one => { + :respond_to => "validate" + } + } + ) + }.should_not raise_error(ArgumentError) + + lambda { + @vo.validate( + {:one => @vo}, + { + :one => { + :respond_to => "monkey" + } + } + ) + }.should raise_error(ArgumentError) + end + + it "should allow you to specify whether an object has all the given methods with respond_to and an array" do + lambda { + @vo.validate( + {:one => @vo}, + { + :one => { + :respond_to => ["validate", "music"] + } + } + ) + }.should_not raise_error(ArgumentError) + + lambda { + @vo.validate( + {:one => @vo}, + { + :one => { + :respond_to => ["monkey", "validate"] + } + } + ) + }.should raise_error(ArgumentError) + end + + it "should let you set a default value with default => value" do + arguments = Hash.new + @vo.validate(arguments, { + :one => { + :default => "is the loneliest number" + } + }) + arguments[:one].should == "is the loneliest number" + end + + it "should let you check regular expressions" do + lambda { + @vo.validate( + { :one => "is good" }, + { + :one => { + :regex => /^is good$/ + } + } + ) + }.should_not raise_error(ArgumentError) + + lambda { + @vo.validate( + { :one => "is good" }, + { + :one => { + :regex => /^is bad$/ + } + } + ) + }.should raise_error(ArgumentError) + end + + it "should let you specify your own callbacks" do + lambda { + @vo.validate( + { :one => "is good" }, + { + :one => { + :callbacks => { + "should be equal to is good" => lambda { |a| + a == "is good" + }, + } + } + } + ) + }.should_not raise_error(ArgumentError) + + lambda { + @vo.validate( + { :one => "is bad" }, + { + :one => { + :callbacks => { + "should be equal to 'is good'" => lambda { |a| + a == "is good" + }, + } + } + } + ) + }.should raise_error(ArgumentError) + end + + it "should let you combine checks" do + args = { :one => "is good", :two => "is bad" } + lambda { + @vo.validate( + args, + { + :one => { + :kind_of => String, + :respond_to => [ :to_s, :upcase ], + :regex => /^is good/, + :callbacks => { + "should be your friend" => lambda { |a| + a == "is good" + } + }, + :required => true + }, + :two => { + :kind_of => String, + :required => false + }, + :three => { :default => "neato mosquito" } + } + ) + }.should_not raise_error(ArgumentError) + args[:three].should == "neato mosquito" + lambda { + @vo.validate( + args, + { + :one => { + :kind_of => String, + :respond_to => [ :to_s, :upcase ], + :regex => /^is good/, + :callbacks => { + "should be your friend" => lambda { |a| + a == "is good" + } + }, + :required => true + }, + :two => { + :kind_of => Hash, + :required => false + }, + :three => { :default => "neato mosquito" } + } + ) + }.should raise_error(ArgumentError) + end + + it "should raise an ArgumentError if the validation map has an unknown check" do + lambda { @vo.validate( + { :one => "two" }, + { + :one => { + :busted => "check" + } + } + ) + }.should raise_error(ArgumentError) + end + + it "should accept keys that are strings in the options" do + lambda { + @vo.validate({ "one" => "two" }, { :one => { :regex => /^two$/ }}) + }.should_not raise_error(ArgumentError) + end + + it "should allow an array to kind_of" do + lambda { + @vo.validate( + {:one => "string"}, + { + :one => { + :kind_of => [ String, Array ] + } + } + ) + }.should_not raise_error(ArgumentError) + lambda { + @vo.validate( + {:one => ["string"]}, + { + :one => { + :kind_of => [ String, Array ] + } + } + ) + }.should_not raise_error(ArgumentError) + lambda { + @vo.validate( + {:one => Hash.new}, + { + :one => { + :kind_of => [ String, Array ] + } + } + ) + }.should raise_error(ArgumentError) + end + + it "asserts that a value returns false from a predicate method" do + lambda do + @vo.validate({:not_blank => "should pass"}, + {:not_blank => {:cannot_be => :nil, :cannot_be => :empty}}) + end.should_not raise_error + lambda do + @vo.validate({:not_blank => ""}, + {:not_blank => {:cannot_be => :nil, :cannot_be => :empty}}) + end.should raise_error(Chef::Exceptions::ValidationFailed) + end + + it "should set and return a value, then return the same value" do + value = "meow" + @vo.set_or_return(:test, value, {}).object_id.should == value.object_id + @vo.set_or_return(:test, nil, {}).object_id.should == value.object_id + end + + it "should set and return a default value when the argument is nil, then return the same value" do + value = "meow" + @vo.set_or_return(:test, nil, { :default => value }).object_id.should == value.object_id + @vo.set_or_return(:test, nil, {}).object_id.should == value.object_id + end + + it "should raise an ArgumentError when argument is nil and required is true" do + lambda { + @vo.set_or_return(:test, nil, { :required => true }) + }.should raise_error(ArgumentError) + end + + it "should not raise an error when argument is nil and required is false" do + lambda { + @vo.set_or_return(:test, nil, { :required => false }) + }.should_not raise_error(ArgumentError) + end + + it "should set and return @name, then return @name for foo when argument is nil" do + value = "meow" + @vo.set_or_return(:name, value, { }).object_id.should == value.object_id + @vo.set_or_return(:foo, nil, { :name_attribute => true }).object_id.should == value.object_id + end + +end diff --git a/spec/unit/mixin/path_sanity_spec.rb b/spec/unit/mixin/path_sanity_spec.rb new file mode 100644 index 0000000000..e38ee7dc8a --- /dev/null +++ b/spec/unit/mixin/path_sanity_spec.rb @@ -0,0 +1,80 @@ +# +# Author:: Seth Chisamore (<schisamo@opscode.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' + +class PathSanityTestHarness + include Chef::Mixin::PathSanity +end + +describe Chef::Mixin::PathSanity do + + before do + @sanity = PathSanityTestHarness.new + end + + describe "when enforcing path sanity" do + before do + Chef::Config[:enforce_path_sanity] = true + @ruby_bindir = '/some/ruby/bin' + @gem_bindir = '/some/gem/bin' + Gem.stub!(:bindir).and_return(@gem_bindir) + RbConfig::CONFIG.stub!(:[]).with('bindir').and_return(@ruby_bindir) + Chef::Platform.stub!(:windows?).and_return(false) + end + + it "adds all useful PATHs that are not yet in PATH to PATH" do + env = {"PATH" => ""} + @sanity.enforce_path_sanity(env) + env["PATH"].should == "#{@ruby_bindir}:#{@gem_bindir}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + end + + it "does not re-add paths that already exist in PATH" do + env = {"PATH" => "/usr/bin:/sbin:/bin"} + @sanity.enforce_path_sanity(env) + env["PATH"].should == "/usr/bin:/sbin:/bin:#{@ruby_bindir}:#{@gem_bindir}:/usr/local/sbin:/usr/local/bin:/usr/sbin" + end + + it "adds the current executing Ruby's bindir and Gem bindir to the PATH" do + env = {"PATH" => ""} + @sanity.enforce_path_sanity(env) + env["PATH"].should == "#{@ruby_bindir}:#{@gem_bindir}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + end + + it "does not create entries for Ruby/Gem bindirs if they exist in SANE_PATH or PATH" do + ruby_bindir = '/usr/bin' + gem_bindir = '/yo/gabba/gabba' + Gem.stub!(:bindir).and_return(gem_bindir) + RbConfig::CONFIG.stub!(:[]).with('bindir').and_return(ruby_bindir) + env = {"PATH" => gem_bindir} + @sanity.enforce_path_sanity(env) + env["PATH"].should == "/yo/gabba/gabba:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + end + + it "builds a valid windows path" do + ruby_bindir = 'C:\ruby\bin' + gem_bindir = 'C:\gems\bin' + Gem.stub!(:bindir).and_return(gem_bindir) + RbConfig::CONFIG.stub!(:[]).with('bindir').and_return(ruby_bindir) + Chef::Platform.stub!(:windows?).and_return(true) + env = {"PATH" => 'C:\Windows\system32;C:\mr\softie'} + @sanity.enforce_path_sanity(env) + env["PATH"].should == "C:\\Windows\\system32;C:\\mr\\softie;#{ruby_bindir};#{gem_bindir}" + end + end +end diff --git a/spec/unit/mixin/securable_spec.rb b/spec/unit/mixin/securable_spec.rb new file mode 100644 index 0000000000..d2e8770c9d --- /dev/null +++ b/spec/unit/mixin/securable_spec.rb @@ -0,0 +1,254 @@ +# +# Author:: Mark Mzyk (<mmzyk@opscode.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::Mixin::Securable do + + before(:each) do + @securable = Object.new + @securable.send(:extend, Chef::Mixin::Securable) + @securable.send(:extend, Chef::Mixin::ParamsValidate) + end + + it "should accept a group name or id for group" do + lambda { @securable.group "root" }.should_not raise_error(ArgumentError) + lambda { @securable.group 123 }.should_not raise_error(ArgumentError) + lambda { @securable.group "root*goo" }.should raise_error(ArgumentError) + end + + it "should accept a user name or id for owner" do + lambda { @securable.owner "root" }.should_not raise_error(ArgumentError) + lambda { @securable.owner 123 }.should_not raise_error(ArgumentError) + lambda { @securable.owner "root*goo" }.should raise_error(ArgumentError) + end + + it "allows the owner to be specified as #user" do + @securable.should respond_to(:user) + end + + describe "unix-specific behavior" do + before(:each) do + platform_mock :unix do + @original_config = Chef::Config.hash_dup + load File.join(File.dirname(__FILE__), "..", "..", "..", "lib", "chef", "config.rb") + load File.join(File.dirname(__FILE__), "..", "..", "..", "lib", "chef", "mixin", "securable.rb") + @securable = Object.new + @securable.send(:extend, Chef::Mixin::Securable) + @securable.send(:extend, Chef::Mixin::ParamsValidate) + end + end + + after(:each) do + Chef::Config.configuration = @original_config + end + + it "should accept a group name or id for group with spaces and backslashes" do + lambda { @securable.group 'test\ group' }.should_not raise_error(ArgumentError) + end + + it "should accept a unix file mode in string form as an octal number" do + lambda { @securable.mode "0" }.should_not raise_error(ArgumentError) + lambda { @securable.mode "0000" }.should_not raise_error(ArgumentError) + lambda { @securable.mode "0111" }.should_not raise_error(ArgumentError) + lambda { @securable.mode "0444" }.should_not raise_error(ArgumentError) + + lambda { @securable.mode "111" }.should_not raise_error(ArgumentError) + lambda { @securable.mode "444" }.should_not raise_error(ArgumentError) + lambda { @securable.mode "7777" }.should_not raise_error(ArgumentError) + lambda { @securable.mode "07777" }.should_not raise_error(ArgumentError) + + lambda { @securable.mode "-01" }.should raise_error(ArgumentError) + lambda { @securable.mode "010000" }.should raise_error(ArgumentError) + lambda { @securable.mode "-1" }.should raise_error(ArgumentError) + lambda { @securable.mode "10000" }.should raise_error(ArgumentError) + + lambda { @securable.mode "07778" }.should raise_error(ArgumentError) + lambda { @securable.mode "7778" }.should raise_error(ArgumentError) + lambda { @securable.mode "4095" }.should raise_error(ArgumentError) + + lambda { @securable.mode "0foo1234" }.should raise_error(ArgumentError) + lambda { @securable.mode "foo1234" }.should raise_error(ArgumentError) + end + + it "should accept a unix file mode in numeric form as a ruby-interpreted integer" do + lambda { @securable.mode 0 }.should_not raise_error(ArgumentError) + lambda { @securable.mode 0000 }.should_not raise_error(ArgumentError) + lambda { @securable.mode 444 }.should_not raise_error(ArgumentError) + lambda { @securable.mode 0444 }.should_not raise_error(ArgumentError) + lambda { @securable.mode 07777 }.should_not raise_error(ArgumentError) + + lambda { @securable.mode 292 }.should_not raise_error(ArgumentError) + lambda { @securable.mode 4095 }.should_not raise_error(ArgumentError) + + lambda { @securable.mode 0111 }.should_not raise_error(ArgumentError) + lambda { @securable.mode 73 }.should_not raise_error(ArgumentError) + + lambda { @securable.mode -01 }.should raise_error(ArgumentError) + lambda { @securable.mode 010000 }.should raise_error(ArgumentError) + lambda { @securable.mode -1 }.should raise_error(ArgumentError) + lambda { @securable.mode 4096 }.should raise_error(ArgumentError) + end + end + + describe "windows-specific behavior" do + before(:each) do + platform_mock :windows do + @original_config = Chef::Config.hash_dup + load File.join(File.dirname(__FILE__), "..", "..", "..", "lib", "chef", "config.rb") + load File.join(File.dirname(__FILE__), "..", "..", "..", "lib", "chef", "mixin", "securable.rb") + @securable = Object.new + @securable.send(:extend, Chef::Mixin::Securable) + @securable.send(:extend, Chef::Mixin::ParamsValidate) + end + end + + after(:all) do + Chef::Config.configuration = @original_config if @original_config + end + + after(:each) do + Chef::Config.configuration = @original_config if @original_config + end + + it "should not accept a group name or id for group with spaces and multiple backslashes" do + lambda { @securable.group 'test\ \group' }.should raise_error(ArgumentError) + end + + it "should accept a unix file mode in string form as an octal number" do + lambda { @securable.mode "0" }.should_not raise_error(ArgumentError) + lambda { @securable.mode "0000" }.should_not raise_error(ArgumentError) + lambda { @securable.mode "0111" }.should_not raise_error(ArgumentError) + lambda { @securable.mode "0444" }.should_not raise_error(ArgumentError) + + lambda { @securable.mode "111" }.should_not raise_error(ArgumentError) + lambda { @securable.mode "444" }.should_not raise_error(ArgumentError) + lambda { @securable.mode "7777" }.should raise_error(ArgumentError) + lambda { @securable.mode "07777" }.should raise_error(ArgumentError) + + lambda { @securable.mode "-01" }.should raise_error(ArgumentError) + lambda { @securable.mode "010000" }.should raise_error(ArgumentError) + lambda { @securable.mode "-1" }.should raise_error(ArgumentError) + lambda { @securable.mode "10000" }.should raise_error(ArgumentError) + + lambda { @securable.mode "07778" }.should raise_error(ArgumentError) + lambda { @securable.mode "7778" }.should raise_error(ArgumentError) + lambda { @securable.mode "4095" }.should raise_error(ArgumentError) + + lambda { @securable.mode "0foo1234" }.should raise_error(ArgumentError) + lambda { @securable.mode "foo1234" }.should raise_error(ArgumentError) + end + + it "should accept a unix file mode in numeric form as a ruby-interpreted integer" do + lambda { @securable.mode 0 }.should_not raise_error(ArgumentError) + lambda { @securable.mode 0000 }.should_not raise_error(ArgumentError) + lambda { @securable.mode 444 }.should_not raise_error(ArgumentError) + lambda { @securable.mode 0444 }.should_not raise_error(ArgumentError) + lambda { @securable.mode 07777 }.should raise_error(ArgumentError) + + lambda { @securable.mode 292 }.should_not raise_error(ArgumentError) + lambda { @securable.mode 4095 }.should raise_error(ArgumentError) + + lambda { @securable.mode 0111 }.should_not raise_error(ArgumentError) + lambda { @securable.mode 73 }.should_not raise_error(ArgumentError) + + lambda { @securable.mode -01 }.should raise_error(ArgumentError) + lambda { @securable.mode 010000 }.should raise_error(ArgumentError) + lambda { @securable.mode -1 }.should raise_error(ArgumentError) + lambda { @securable.mode 4096 }.should raise_error(ArgumentError) + end + + it "should allow you to specify :full_control, :modify, :read_execute, :read, and :write rights" do + lambda { @securable.rights :full_control, "The Dude" }.should_not raise_error(ArgumentError) + lambda { @securable.rights :modify, "The Dude" }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read_execute, "The Dude" }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude" }.should_not raise_error(ArgumentError) + lambda { @securable.rights :write, "The Dude" }.should_not raise_error(ArgumentError) + lambda { @securable.rights :to_party, "The Dude" }.should raise_error(ArgumentError) + end + + it "should allow you to specify :full_control, :modify, :read_execute, :read, and :write deny_rights" do + lambda { @securable.deny_rights :full_control, "The Dude" }.should_not raise_error(ArgumentError) + lambda { @securable.deny_rights :modify, "The Dude" }.should_not raise_error(ArgumentError) + lambda { @securable.deny_rights :read_execute, "The Dude" }.should_not raise_error(ArgumentError) + lambda { @securable.deny_rights :read, "The Dude" }.should_not raise_error(ArgumentError) + lambda { @securable.deny_rights :write, "The Dude" }.should_not raise_error(ArgumentError) + lambda { @securable.deny_rights :to_party, "The Dude" }.should raise_error(ArgumentError) + end + + it "should accept a principal as a string or an array" do + lambda { @securable.rights :read, "The Dude" }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, ["The Dude","Donny"] }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, 3 }.should raise_error(ArgumentError) + end + + it "should allow you to specify whether the permissions applies_to_children with true/false/:containers_only/:objects_only" do + lambda { @securable.rights :read, "The Dude", :applies_to_children => false }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_children => true }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_children => :containers_only }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_children => :objects_only }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_children => 'poop' }.should raise_error(ArgumentError) + end + + it "should allow you to specify whether the permissions applies_to_self with true/false" do + lambda { @securable.rights :read, "The Dude", :applies_to_children => true, :applies_to_self => false }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_self => true }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_self => 'poop' }.should raise_error(ArgumentError) + end + + it "should allow you to specify whether the permissions applies one_level_deep with true/false" do + lambda { @securable.rights :read, "The Dude", :applies_to_children => true, :one_level_deep => false }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_children => true, :one_level_deep => true }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_children => true, :one_level_deep => 'poop' }.should raise_error(ArgumentError) + end + + it "should allow multiple rights and deny_rights declarations" do + @securable.rights :read, "The Dude" + @securable.deny_rights :full_control, "The Dude" + @securable.rights :full_control, "The Dude" + @securable.rights :write, "The Dude" + @securable.deny_rights :read, "The Dude" + @securable.rights.size.should == 3 + @securable.deny_rights.size.should == 2 + end + + it "should allow you to specify whether the permission applies_to_self only if you specified applies_to_children" do + lambda { @securable.rights :read, "The Dude", :applies_to_children => true, :applies_to_self => true }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_children => true, :applies_to_self => false }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_children => false, :applies_to_self => true }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_children => false, :applies_to_self => false }.should raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_self => true }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_self => false }.should_not raise_error(ArgumentError) + end + + it "should allow you to specify whether the permission applies one_level_deep only if you specified applies_to_children" do + lambda { @securable.rights :read, "The Dude", :applies_to_children => true, :one_level_deep => true }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_children => true, :one_level_deep => false }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_children => false, :one_level_deep => true }.should raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :applies_to_children => false, :one_level_deep => false }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :one_level_deep => true }.should_not raise_error(ArgumentError) + lambda { @securable.rights :read, "The Dude", :one_level_deep => false }.should_not raise_error(ArgumentError) + end + + it "should allow you to specify whether the permissions inherit with true/false" do + lambda { @securable.inherits true }.should_not raise_error(ArgumentError) + lambda { @securable.inherits false }.should_not raise_error(ArgumentError) + lambda { @securable.inherits "monkey" }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/unit/mixin/shell_out_spec.rb b/spec/unit/mixin/shell_out_spec.rb new file mode 100644 index 0000000000..6ca700fcdb --- /dev/null +++ b/spec/unit/mixin/shell_out_spec.rb @@ -0,0 +1,109 @@ +# +# Author:: Ho-Sheng Hsiao (hosh@opscode.com) +# Code derived from spec/unit/mixin/command_spec.rb +# +# Original header: +# Author:: Hongli Lai (hongli@phusion.nl) +# Copyright:: Copyright (c) 2009 Phusion +# 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::Mixin::ShellOut do + include Chef::Mixin::ShellOut + + describe '#run_command_compatible_options' do + subject { run_command_compatible_options(command_args) } + let(:command_args) { [ cmd, options ] } + let(:cmd) { "echo '#{rand(1000)}'" } + + let(:output) { StringIO.new } + let(:capture_log_output) { Chef::Log.logger = Logger.new(output) } + let(:assume_deprecation_log_level) { Chef::Log.stub!(:level).and_return(:warn) } + + context 'without options' do + let(:command_args) { [ cmd ] } + + it 'should not edit command args' do + should eql(command_args) + end + end + + context 'without deprecated options' do + let(:options) { { :environment => environment } } + let(:environment) { { 'LC_ALL' => 'C' } } + + it 'should not edit command args' do + should eql(command_args) + end + end + + def self.should_emit_deprecation_warning_about(old_option, new_option) + it 'should emit a deprecation warning' do + assume_deprecation_log_level and capture_log_output + subject + output.string.should match /DEPRECATION:/ + output.string.should match Regexp.escape(old_option.to_s) + output.string.should match Regexp.escape(new_option.to_s) + end + end + + context 'with :command_log_level option' do + let(:options) { { :command_log_level => command_log_level } } + let(:command_log_level) { :warn } + + it 'should convert :command_log_level to :log_level' do + should eql [ cmd, { :log_level => command_log_level } ] + end + + should_emit_deprecation_warning_about :command_log_level, :log_level + end + + context 'with :command_log_prepend option' do + let(:options) { { :command_log_prepend => command_log_prepend } } + let(:command_log_prepend) { 'PROVIDER:' } + + it 'should convert :command_log_prepend to :log_tag' do + should eql [ cmd, { :log_tag => command_log_prepend } ] + end + + should_emit_deprecation_warning_about :command_log_prepend, :log_tag + end + + context "with 'command_log_level' option" do + let(:options) { { 'command_log_level' => command_log_level } } + let(:command_log_level) { :warn } + + it "should convert 'command_log_level' to :log_level" do + should eql [ cmd, { :log_level => command_log_level } ] + end + + should_emit_deprecation_warning_about :command_log_level, :log_level + end + + context "with 'command_log_prepend' option" do + let(:options) { { 'command_log_prepend' => command_log_prepend } } + let(:command_log_prepend) { 'PROVIDER:' } + + it "should convert 'command_log_prepend' to :log_tag" do + should eql [ cmd, { :log_tag => command_log_prepend } ] + end + + should_emit_deprecation_warning_about :command_log_prepend, :log_tag + end + + end +end diff --git a/spec/unit/mixin/template_spec.rb b/spec/unit/mixin/template_spec.rb new file mode 100644 index 0000000000..3d8a723a75 --- /dev/null +++ b/spec/unit/mixin/template_spec.rb @@ -0,0 +1,104 @@ +# +# 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' + +class TinyTemplateClass; include Chef::Mixin::Template; end +require 'cgi' +describe Chef::Mixin::Template, "render_template" do + + before :each do + @template = TinyTemplateClass.new + end + + it "should render the template evaluated in the given context" do + @template.render_template("<%= @foo %>", { :foo => "bar" }) do |tmp| + tmp.open.read.should == "bar" + end + end + + it "should provide a node method to access @node" do + @template.render_template("<%= node %>",{:node => "tehShizzle"}) do |tmp| + tmp.open.read.should == "tehShizzle" + end + end + + it "should yield the tempfile it renders the template to" do + @template.render_template("abcdef", {}) do |tempfile| + tempfile.should be_kind_of(Tempfile) + end + end + + describe "when an exception is raised in the template" do + def do_raise + @context = {:chef => "cool"} + @template.render_template("foo\nbar\nbaz\n<%= this_is_not_defined %>\nquin\nqunx\ndunno", @context) {|r| r} + end + + it "should catch and re-raise the exception as a TemplateError" do + lambda { do_raise }.should raise_error(Chef::Mixin::Template::TemplateError) + end + + it "should raise an error if an attempt is made to access node but it is nil" do + lambda {@template.render_template("<%= node %>",{}) {|r| r}}.should raise_error(Chef::Mixin::Template::TemplateError) + end + + describe "the raised TemplateError" do + before :each do + begin + do_raise + rescue Chef::Mixin::Template::TemplateError => e + @exception = e + end + end + + it "should have the original exception" do + @exception.original_exception.should be + @exception.original_exception.message.should =~ /undefined local variable or method `this_is_not_defined'/ + end + + it "should determine the line number of the exception" do + @exception.line_number.should == 4 + end + + it "should provide a source listing of the template around the exception" do + @exception.source_listing.should == " 2: bar\n 3: baz\n 4: <%= this_is_not_defined %>\n 5: quin\n 6: qunx" + end + + it "should provide the evaluation context of the template" do + @exception.context.should == @context + end + + it "should defer the message to the original exception" do + @exception.message.should =~ /undefined local variable or method `this_is_not_defined'/ + end + + it "should provide a nice source location" do + @exception.source_location.should == "on line #4" + end + + it "should create a pretty output for the terminal" do + @exception.to_s.should =~ /Chef::Mixin::Template::TemplateError/ + @exception.to_s.should =~ /undefined local variable or method `this_is_not_defined'/ + @exception.to_s.should include(" 2: bar\n 3: baz\n 4: <%= this_is_not_defined %>\n 5: quin\n 6: qunx") + @exception.to_s.should include(@exception.original_exception.backtrace.first) + end + end + end +end + diff --git a/spec/unit/mixin/xml_escape_spec.rb b/spec/unit/mixin/xml_escape_spec.rb new file mode 100644 index 0000000000..d05854ade4 --- /dev/null +++ b/spec/unit/mixin/xml_escape_spec.rb @@ -0,0 +1,54 @@ +# +# 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' + +class XMLEscapingTestHarness + include Chef::Mixin::XMLEscape +end + +describe Chef::Mixin::XMLEscape do + before do + @escaper = XMLEscapingTestHarness.new + end + + it "escapes ampersands to '&'" do + @escaper.xml_escape("&").should == "&" + end + + it "escapes angle brackets to < or >" do + @escaper.xml_escape("<").should == "<" + @escaper.xml_escape(">").should == ">" + end + + it "does not modify ASCII strings" do + @escaper.xml_escape('foobarbaz!@#$%^*()').should == 'foobarbaz!@#$%^*()' + end + + it "converts invalid bytes to asterisks" do + @escaper.xml_escape("\x00").should == "*" + end + + it "converts UTF-8 correctly" do + @escaper.xml_escape("\xC2\xA9").should == '©' + end + + it "converts win 1252 characters correctly" do + @escaper.xml_escape("\x80").should == '€' + end +end diff --git a/spec/unit/monkey_patches/string_spec.rb b/spec/unit/monkey_patches/string_spec.rb new file mode 100644 index 0000000000..8c6710b38e --- /dev/null +++ b/spec/unit/monkey_patches/string_spec.rb @@ -0,0 +1,37 @@ +# +# Author:: Devin Ben-Hur <dbenhur@whitepages.com> +# Copyright:: Copyright (c) 2008, 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' +require 'chef/monkey_patches/string' + +describe String do + + describe "#ord" do + it "converts each ASCII-8BIT character to corresponding positive Fixnum" do + (0..0xff).each do |num| + ch = num.chr + ch.force_encoding('ASCII-8BIT') if ch.respond_to? :force_encoding + + ch.ord.should be_a_kind_of(Fixnum) + ch.ord.should == num + end + end + + end + +end diff --git a/spec/unit/node/attribute_spec.rb b/spec/unit/node/attribute_spec.rb new file mode 100644 index 0000000000..422b34f252 --- /dev/null +++ b/spec/unit/node/attribute_spec.rb @@ -0,0 +1,1194 @@ +# +# Author:: Adam Jacob (<adam@opscode.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' +require 'chef/node/attribute' + +describe Chef::Node::Attribute do + before(:each) do + @attribute_hash = + {"dmi"=>{}, + "command"=>{"ps"=>"ps -ef"}, + "platform_version"=>"10.5.7", + "platform"=>"mac_os_x", + "ipaddress"=>"192.168.0.117", + "network"=> + {"default_interface"=>"en1", + "interfaces"=> + {"vmnet1"=> + {"flags"=> + ["UP", "BROADCAST", "SMART", "RUNNING", "SIMPLEX", "MULTICAST"], + "number"=>"1", + "addresses"=> + {"00:50:56:c0:00:01"=>{"family"=>"lladdr"}, + "192.168.110.1"=> + {"broadcast"=>"192.168.110.255", + "netmask"=>"255.255.255.0", + "family"=>"inet"}}, + "mtu"=>"1500", + "type"=>"vmnet", + "arp"=>{"192.168.110.255"=>"ff:ff:ff:ff:ff:ff"}, + "encapsulation"=>"Ethernet"}, + "stf0"=> + {"flags"=>[], + "number"=>"0", + "addresses"=>{}, + "mtu"=>"1280", + "type"=>"stf", + "encapsulation"=>"6to4"}, + "lo0"=> + {"flags"=>["UP", "LOOPBACK", "RUNNING", "MULTICAST"], + "number"=>"0", + "addresses"=> + {"::1"=>{"scope"=>"Node", "prefixlen"=>"128", "family"=>"inet6"}, + "127.0.0.1"=>{"netmask"=>"255.0.0.0", "family"=>"inet"}, + "fe80::1"=>{"scope"=>"Link", "prefixlen"=>"64", "family"=>"inet6"}}, + "mtu"=>"16384", + "type"=>"lo", + "encapsulation"=>"Loopback"}, + "gif0"=> + {"flags"=>["POINTOPOINT", "MULTICAST"], + "number"=>"0", + "addresses"=>{}, + "mtu"=>"1280", + "type"=>"gif", + "encapsulation"=>"IPIP"}, + "vmnet8"=> + {"flags"=> + ["UP", "BROADCAST", "SMART", "RUNNING", "SIMPLEX", "MULTICAST"], + "number"=>"8", + "addresses"=> + {"192.168.4.1"=> + {"broadcast"=>"192.168.4.255", + "netmask"=>"255.255.255.0", + "family"=>"inet"}, + "00:50:56:c0:00:08"=>{"family"=>"lladdr"}}, + "mtu"=>"1500", + "type"=>"vmnet", + "arp"=>{"192.168.4.255"=>"ff:ff:ff:ff:ff:ff"}, + "encapsulation"=>"Ethernet"}, + "en0"=> + {"status"=>"inactive", + "flags"=> + ["UP", "BROADCAST", "SMART", "RUNNING", "SIMPLEX", "MULTICAST"], + "number"=>"0", + "addresses"=>{"00:23:32:b0:32:f2"=>{"family"=>"lladdr"}}, + "mtu"=>"1500", + "media"=> + {"supported"=> + {"autoselect"=>{"options"=>[]}, + "none"=>{"options"=>[]}, + "1000baseT"=> + {"options"=>["full-duplex", "flow-control", "hw-loopback"]}, + "10baseT/UTP"=> + {"options"=> + ["half-duplex", "full-duplex", "flow-control", "hw-loopback"]}, + "100baseTX"=> + {"options"=> + ["half-duplex", "full-duplex", "flow-control", "hw-loopback"]}}, + "selected"=>{"autoselect"=>{"options"=>[]}}}, + "type"=>"en", + "encapsulation"=>"Ethernet"}, + "en1"=> + {"status"=>"active", + "flags"=> + ["UP", "BROADCAST", "SMART", "RUNNING", "SIMPLEX", "MULTICAST"], + "number"=>"1", + "addresses"=> + {"fe80::223:6cff:fe7f:676c"=> + {"scope"=>"Link", "prefixlen"=>"64", "family"=>"inet6"}, + "00:23:6c:7f:67:6c"=>{"family"=>"lladdr"}, + "192.168.0.117"=> + {"broadcast"=>"192.168.0.255", + "netmask"=>"255.255.255.0", + "family"=>"inet"}}, + "mtu"=>"1500", + "media"=> + {"supported"=>{"autoselect"=>{"options"=>[]}}, + "selected"=>{"autoselect"=>{"options"=>[]}}}, + "type"=>"en", + "arp"=> + {"192.168.0.72"=>"0:f:ea:39:fa:d5", + "192.168.0.1"=>"0:1c:fb:fc:6f:20", + "192.168.0.255"=>"ff:ff:ff:ff:ff:ff", + "192.168.0.3"=>"0:1f:33:ea:26:9b", + "192.168.0.77"=>"0:23:12:70:f8:cf", + "192.168.0.152"=>"0:26:8:7d:2:4c"}, + "encapsulation"=>"Ethernet"}, + "en2"=> + {"status"=>"active", + "flags"=> + ["UP", "BROADCAST", "SMART", "RUNNING", "SIMPLEX", "MULTICAST"], + "number"=>"2", + "addresses"=> + {"169.254.206.152"=> + {"broadcast"=>"169.254.255.255", + "netmask"=>"255.255.0.0", + "family"=>"inet"}, + "00:1c:42:00:00:01"=>{"family"=>"lladdr"}, + "fe80::21c:42ff:fe00:1"=> + {"scope"=>"Link", "prefixlen"=>"64", "family"=>"inet6"}}, + "mtu"=>"1500", + "media"=> + {"supported"=>{"autoselect"=>{"options"=>[]}}, + "selected"=>{"autoselect"=>{"options"=>[]}}}, + "type"=>"en", + "encapsulation"=>"Ethernet"}, + "fw0"=> + {"status"=>"inactive", + "flags"=>["BROADCAST", "SIMPLEX", "MULTICAST"], + "number"=>"0", + "addresses"=>{"00:23:32:ff:fe:b0:32:f2"=>{"family"=>"lladdr"}}, + "mtu"=>"4078", + "media"=> + {"supported"=>{"autoselect"=>{"options"=>["full-duplex"]}}, + "selected"=>{"autoselect"=>{"options"=>["full-duplex"]}}}, + "type"=>"fw", + "encapsulation"=>"1394"}, + "en3"=> + {"status"=>"active", + "flags"=> + ["UP", "BROADCAST", "SMART", "RUNNING", "SIMPLEX", "MULTICAST"], + "number"=>"3", + "addresses"=> + {"169.254.206.152"=> + {"broadcast"=>"169.254.255.255", + "netmask"=>"255.255.0.0", + "family"=>"inet"}, + "00:1c:42:00:00:00"=>{"family"=>"lladdr"}, + "fe80::21c:42ff:fe00:0"=> + {"scope"=>"Link", "prefixlen"=>"64", "family"=>"inet6"}}, + "mtu"=>"1500", + "media"=> + {"supported"=>{"autoselect"=>{"options"=>[]}}, + "selected"=>{"autoselect"=>{"options"=>[]}}}, + "type"=>"en", + "encapsulation"=>"Ethernet"}}}, + "fqdn"=>"latte.local", + "ohai_time"=>1249065590.90391, + "domain"=>"local", + "os"=>"darwin", + "platform_build"=>"9J61", + "os_version"=>"9.7.0", + "hostname"=>"latte", + "macaddress"=>"00:23:6c:7f:67:6c", + "music" => { "jimmy_eat_world" => "nice", "apophis" => false } + } + @default_hash = { + "domain" => "opscode.com", + "hot" => { "day" => "saturday" }, + "music" => { + "jimmy_eat_world" => "is fun!", + "mastodon" => "rocks", + "mars_volta" => "is loud and nutty", + "deeper" => { "gates_of_ishtar" => nil }, + "this" => {"apparatus" => {"must" => "be unearthed"}} + } + } + @override_hash = { + "macaddress" => "00:00:00:00:00:00", + "hot" => { "day" => "sunday" }, + "fire" => "still burn", + "music" => { + "mars_volta" => "cicatriz" + } + } + @automatic_hash = {"week" => "friday"} + @attributes = Chef::Node::Attribute.new(@attribute_hash, @default_hash, @override_hash, @automatic_hash) + end + + describe "initialize" do + it "should return a Chef::Node::Attribute" do + @attributes.should be_a_kind_of(Chef::Node::Attribute) + end + + it "should take an Automatioc, Normal, Default and Override hash" do + lambda { Chef::Node::Attribute.new({}, {}, {}, {}) }.should_not raise_error + end + + [ :normal, :default, :override, :automatic ].each do |accessor| + it "should set #{accessor}" do + na = Chef::Node::Attribute.new({ :normal => true }, { :default => true }, { :override => true }, { :automatic => true }) + na.send(accessor).should == { accessor.to_s => true } + end + end + + it "should be enumerable" do + @attributes.should be_is_a(Enumerable) + end + end + + describe "[]" do + it "should return override data if it exists" do + @attributes["macaddress"].should == "00:00:00:00:00:00" + end + + it "should return attribute data if it is not overridden" do + @attributes["platform"].should == "mac_os_x" + end + + it "should return data that doesn't have corresponding keys in every hash" do + @attributes["command"]["ps"].should == "ps -ef" + end + + it "should return default data if it is not overriden or in attribute data" do + @attributes["music"]["mastodon"].should == "rocks" + end + + it "should prefer the override data over an available default" do + @attributes["music"]["mars_volta"].should == "cicatriz" + end + + it "should prefer the attribute data over an available default" do + @attributes["music"]["jimmy_eat_world"].should == "nice" + end + + it "should prefer override data over default data if there is no attribute data" do + @attributes["hot"]["day"].should == "sunday" + end + + it "should return the merged hash if all three have values" do + result = @attributes["music"] + result["mars_volta"].should == "cicatriz" + result["jimmy_eat_world"].should == "nice" + result["mastodon"].should == "rocks" + end + end + + describe "[]=" do + it "should error out when the type of attribute to set has not been specified" do + @attributes.normal["the_ghost"] = { } + lambda { @attributes["the_ghost"]["exterminate"] = false }.should raise_error(Chef::Exceptions::ImmutableAttributeModification) + end + + it "should let you set an attribute value when another hash has an intermediate value" do + @attributes.normal["the_ghost"] = { "exterminate" => "the future" } + lambda { @attributes.normal["the_ghost"]["exterminate"]["tomorrow"] = false }.should_not raise_error(NoMethodError) + end + + it "should set the attribute value" do + @attributes.normal["longboard"] = "surfing" + @attributes.normal["longboard"].should == "surfing" + @attributes.normal["longboard"].should == "surfing" + end + + it "should set deeply nested attribute values when a precedence level is specified" do + @attributes.normal["deftones"]["hunters"]["nap"] = "surfing" + @attributes.normal["deftones"]["hunters"]["nap"].should == "surfing" + end + + it "should die if you try and do nested attributes that do not exist without read vivification" do + lambda { @attributes["foo"]["bar"] = :baz }.should raise_error + end + + it "should let you set attributes manually without vivification" do + @attributes.normal["foo"] = Mash.new + @attributes.normal["foo"]["bar"] = :baz + @attributes.normal["foo"]["bar"].should == :baz + end + + it "should optionally skip setting the value if one already exists" do + @attributes.set_unless_value_present = true + @attributes.normal["hostname"] = "bar" + @attributes["hostname"].should == "latte" + end + + it "does not support ||= when setting" do + # This is a limitation of auto-vivification. + # Users who need this behavior can use set_unless and friends + @attributes.normal["foo"] = Mash.new + @attributes.normal["foo"]["bar"] ||= "stop the world" + @attributes.normal["foo"]["bar"].should == {} + end + end + + describe "to_hash" do + it "should convert to a hash" do + @attributes.to_hash.class.should == Hash + end + + it "should convert to a hash based on current state" do + hash = @attributes["hot"].to_hash + hash.class.should == Hash + hash["day"].should == "sunday" + end + end + + describe "has_key?" do + it "should return true if an attribute exists" do + @attributes.has_key?("music").should == true + end + + it "should return false if an attribute does not exist" do + @attributes.has_key?("ninja").should == false + end + + it "should return false if an attribute does not exist using dot notation" do + @attributes.has_key?("does_not_exist_at_all").should == false + end + + it "should return true if an attribute exists but is set to nil using dot notation" do + @attributes.music.deeper.has_key?("gates_of_ishtar").should == true + end + + it "should return true if an attribute exists but is set to false" do + @attributes.has_key?("music") + @attributes["music"].has_key?("apophis").should == true + end + + it "does not find keys above the current nesting level" do + @attributes["music"]["this"]["apparatus"].should_not have_key("this") + end + + it "does not find keys below the current nesting level" do + @attributes["music"]["this"].should_not have_key("must") + end + + [:include?, :key?, :member?].each do |method| + it "should alias the method #{method} to itself" do + @attributes.should respond_to(method) + end + + it "#{method} should behave like has_key?" do + @attributes.send(method, "music").should == true + end + end + end + + describe "attribute?" do + it "should return true if an attribute exists" do + @attributes.attribute?("music").should == true + end + + it "should return false if an attribute does not exist" do + @attributes.attribute?("ninja").should == false + end + + end + + describe "method_missing" do + it "should behave like a [] lookup" do + @attributes.music.mastodon.should == "rocks" + end + + it "should allow the last method to set a value if it has an = sign on the end" do + @attributes.normal.music.mastodon = [ "dream", "still", "shining" ] + @attributes.reset + @attributes.normal.music.mastodon.should == [ "dream", "still", "shining" ] + end + end + + describe "keys" do + before(:each) do + @attributes = Chef::Node::Attribute.new( + { + "one" => { "two" => "three" }, + "hut" => { "two" => "three" }, + "place" => { } + }, + { + "one" => { "four" => "five" }, + "snakes" => "on a plane" + }, + { + "one" => { "six" => "seven" }, + "snack" => "cookies" + }, + {} + ) + end + + it "should yield each top level key" do + collect = Array.new + @attributes.keys.each do |k| + collect << k + end + collect.include?("one").should == true + collect.include?("hut").should == true + collect.include?("snakes").should == true + collect.include?("snack").should == true + collect.include?("place").should == true + collect.length.should == 5 + end + + it "should yield lower if we go deeper" do + collect = Array.new + @attributes.one.keys.each do |k| + collect << k + end + collect.include?("two").should == true + collect.include?("four").should == true + collect.include?("six").should == true + collect.length.should == 3 + end + + it "should not raise an exception if one of the hashes has a nil value on a deep lookup" do + lambda { @attributes.place.keys { |k| } }.should_not raise_error(NoMethodError) + end + end + + describe "each" do + before(:each) do + @attributes = Chef::Node::Attribute.new( + { + "one" => "two", + "hut" => "three", + }, + { + "one" => "four", + "snakes" => "on a plane" + }, + { + "one" => "six", + "snack" => "cookies" + }, + {} + ) + end + + it "should yield each top level key and value, post merge rules" do + collect = Hash.new + @attributes.each do |k, v| + collect[k] = v + end + + collect["one"].should == "six" + collect["hut"].should == "three" + collect["snakes"].should == "on a plane" + collect["snack"].should == "cookies" + end + + it "should yield as a two-element array" do + @attributes.each do |a| + a.should be_an_instance_of(Array) + end + end + end + + describe "each_key" do + before do + @attributes = Chef::Node::Attribute.new( + { + "one" => "two", + "hut" => "three", + }, + { + "one" => "four", + "snakes" => "on a plane" + }, + { + "one" => "six", + "snack" => "cookies" + }, + {} + ) + end + + it "should respond to each_key" do + @attributes.should respond_to(:each_key) + end + + it "should yield each top level key, post merge rules" do + collect = Array.new + @attributes.each_key do |k| + collect << k + end + + collect.should include("one") + collect.should include("snack") + collect.should include("hut") + collect.should include("snakes") + end + end + + describe "each_pair" do + before do + @attributes = Chef::Node::Attribute.new( + { + "one" => "two", + "hut" => "three", + }, + { + "one" => "four", + "snakes" => "on a plane" + }, + { + "one" => "six", + "snack" => "cookies" + }, + {} + ) + end + + it "should respond to each_pair" do + @attributes.should respond_to(:each_pair) + end + + it "should yield each top level key and value pair, post merge rules" do + collect = Hash.new + @attributes.each_pair do |k, v| + collect[k] = v + end + + collect["one"].should == "six" + collect["hut"].should == "three" + collect["snakes"].should == "on a plane" + collect["snack"].should == "cookies" + end + end + + describe "each_value" do + before do + @attributes = Chef::Node::Attribute.new( + { + "one" => "two", + "hut" => "three", + }, + { + "one" => "four", + "snakes" => "on a plane" + }, + { + "one" => "six", + "snack" => "cookies" + }, + {} + ) + end + + it "should respond to each_value" do + @attributes.should respond_to(:each_value) + end + + it "should yield each value, post merge rules" do + collect = Array.new + @attributes.each_value do |v| + collect << v + end + + collect.should include("cookies") + collect.should include("three") + collect.should include("on a plane") + end + + it "should yield four elements" do + collect = Array.new + @attributes.each_value do |v| + collect << v + end + + collect.length.should == 4 + end + end + + describe "empty?" do + before do + @attributes = Chef::Node::Attribute.new( + { + "one" => "two", + "hut" => "three", + }, + { + "one" => "four", + "snakes" => "on a plane" + }, + { + "one" => "six", + "snack" => "cookies" + }, + {} + ) + @empty = Chef::Node::Attribute.new({}, {}, {}, {}) + end + + it "should respond to empty?" do + @attributes.should respond_to(:empty?) + end + + it "should return true when there are no keys" do + @empty.empty?.should == true + end + + it "should return false when there are keys" do + @attributes.empty?.should == false + end + + end + + describe "fetch" do + before do + @attributes = Chef::Node::Attribute.new( + { + "one" => "two", + "hut" => "three", + }, + { + "one" => "four", + "snakes" => "on a plane" + }, + { + "one" => "six", + "snack" => "cookies" + }, + {} + ) + end + + it "should respond to fetch" do + @attributes.should respond_to(:fetch) + end + + describe "when the key exists" do + it "should return the value of the key, post merge (same result as each)" do + { + "one" => "six", + "hut" => "three", + "snakes" => "on a plane", + "snack" => "cookies" + }.each do |k,v| + @attributes.fetch(k).should == v + end + end + end + + describe "when the key does not exist" do + describe "and no args are passed" do + it "should raise an indexerror" do + lambda { @attributes.fetch("lololol") }.should raise_error(IndexError) + end + end + + describe "and a default arg is passed" do + it "should return the value of the default arg" do + @attributes.fetch("lol", "blah").should == "blah" + end + end + + describe "and a block is passed" do + it "should run the block and return its value" do + @attributes.fetch("lol") { |x| "#{x}, blah" }.should == "lol, blah" + end + end + end + end + + describe "has_value?" do + before do + @attributes = Chef::Node::Attribute.new( + { + "one" => "two", + "hut" => "three", + }, + { + "one" => "four", + "snakes" => "on a plane" + }, + { + "one" => "six", + "snack" => "cookies" + }, + {} + ) + end + + it "should respond to has_value?" do + @attributes.should respond_to(:has_value?) + end + + it "should return true if any key has the value supplied" do + @attributes.has_value?("cookies").should == true + end + + it "should return false no key has the value supplied" do + @attributes.has_value?("lololol").should == false + end + + it "should alias value?" do + @attributes.should respond_to(:value?) + end + end + + describe "index" do + before do + @attributes = Chef::Node::Attribute.new( + { + "one" => "two", + "hut" => "three", + }, + { + "one" => "four", + "snakes" => "on a plane" + }, + { + "one" => "six", + "snack" => "cookies" + }, + {} + ) + end + + it "should respond to index" do + @attributes.should respond_to(:index) + end + + describe "when the value is indexed" do + it "should return the index" do + @attributes.index("six").should == "one" + end + end + + describe "when the value is not indexed" do + it "should return nil" do + @attributes.index("lolol").should == nil + end + end + + end + + + describe "values" do + before do + @attributes = Chef::Node::Attribute.new( + { + "one" => "two", + "hut" => "three", + }, + { + "one" => "four", + "snakes" => "on a plane" + }, + { + "one" => "six", + "snack" => "cookies" + }, + {} + ) + end + + it "should respond to values" do + @attributes.should respond_to(:values) + end + + it "should return an array of values" do + @attributes.values.length.should == 4 + end + + it "should match the values output from each" do + @attributes.values.should include("six") + @attributes.values.should include("cookies") + @attributes.values.should include("three") + @attributes.values.should include("on a plane") + end + + end + + describe "select" do + before do + @attributes = Chef::Node::Attribute.new( + { + "one" => "two", + "hut" => "three", + }, + { + "one" => "four", + "snakes" => "on a plane" + }, + { + "one" => "six", + "snack" => "cookies" + }, + {} + ) + end + + it "should respond to select" do + @attributes.should respond_to(:select) + end + + if RUBY_VERSION >= "1.8.7" + it "should not raise a LocalJumpError if no block is given" do + lambda { @attributes.select }.should_not raise_error(LocalJumpError) + end + else + it "should raise a LocalJumpError if no block is given" do + lambda{ @attributes.select }.should raise_error(LocalJumpError) + end + end + + it "should return an empty hash/array (ruby-version-dependent) for a block containing nil" do + @attributes.select { nil }.should == {}.select { nil } + end + + # sorted for spec clarity + it "should return a new array of k,v pairs for which the block returns true" do + @attributes.select { true }.sort.should == ( + [ + ["hut", "three"], + ["one", "six"], + ["snack", "cookies"], + ["snakes", "on a plane"] + ] + ) + end + end + + describe "size" do + before do + @attributes = Chef::Node::Attribute.new( + { + "one" => "two", + "hut" => "three", + }, + { + "one" => "four", + "snakes" => "on a plane" + }, + { + "one" => "six", + "snack" => "cookies" + }, + {} + ) + + @empty = Chef::Node::Attribute.new({},{},{},{}) + end + + it "should respond to size" do + @attributes.should respond_to(:size) + end + + it "should alias length to size" do + @attributes.should respond_to(:length) + end + + it "should return 0 for an empty attribute" do + @empty.size.should == 0 + end + + it "should return the number of pairs" do + @attributes.size.should == 4 + end + end + + describe "kind_of?" do + it "should falsely inform you that it is a Hash" do + @attributes.should be_a_kind_of(Hash) + end + + it "should falsely inform you that it is a Mash" do + @attributes.should be_a_kind_of(Mash) + end + + it "should inform you that it is a Chef::Node::Attribute" do + @attributes.should be_a_kind_of(Chef::Node::Attribute) + end + + it "should inform you that it is anything else" do + @attributes.should_not be_a_kind_of(Chef::Node) + end + end + + describe "inspect" do + it "should be readable" do + # NOTE: previous implementation hid the values, showing @automatic={...} + # That is nice and compact, but hides a lot of info, which seems counter + # to the point of calling #inspect... + @attributes.inspect.should =~ /@automatic=\{.*\}/ + @attributes.inspect.should =~ /@normal=\{.*\}/ + end + end + + # For expedience, this test is implementation-heavy. + describe "when a component attribute is mutated" do + [ + :clear, + :shift + ].each do |mutator| + it "resets the cache when the mutator #{mutator} is called" do + @attributes.should_receive(:reset_cache) + @attributes.default.send(mutator) + end + end + + it "resets the cache when the mutator delete is called" do + @attributes.should_receive(:reset_cache) + @attributes.default.delete(:music) + end + + [ + :merge, + :update, + :replace + ].each do |mutator| + it "resets the cache when the mutator #{mutator} is called" do + # Implementation of Mash means that this could get called many times. That's okay. + @attributes.should_receive(:reset_cache).at_least(1).times + @attributes.default.send(mutator, {:foo => :bar}) + end + end + + [ + :delete_if, + :keep_if, + :reject!, + :select!, + ].each do |mutator| + it "resets the cache when the mutator #{mutator} is called" do + # Implementation of Mash means that this could get called many times. That's okay. + @attributes.should_receive(:reset_cache).at_least(1).times + block = lambda {|k,v| true } + @attributes.default.send(mutator, &block) + end + end + end + + describe "when setting a component attribute to a new value" do + it "converts the input in to a VividMash tree (default)" do + @attributes.default = {} + @attributes.default.foo = "bar" + @attributes.merged_attributes[:foo].should == "bar" + end + + it "converts the input in to a VividMash tree (normal)" do + @attributes.normal = {} + @attributes.normal.foo = "bar" + @attributes.merged_attributes[:foo].should == "bar" + end + + it "converts the input in to a VividMash tree (override)" do + @attributes.override = {} + @attributes.override.foo = "bar" + @attributes.merged_attributes[:foo].should == "bar" + end + + it "converts the input in to a VividMash tree (automatic)" do + @attributes.automatic = {} + @attributes.automatic.foo = "bar" + @attributes.merged_attributes[:foo].should == "bar" + end + end + + describe "when attemping to write without specifying precedence" do + it "raises an error when using []=" do + lambda { @attributes[:new_key] = "new value" }.should raise_error(Chef::Exceptions::ImmutableAttributeModification) + end + + it "raises an error when using `attr=value`" do + lambda { @attributes.new_key = "new value" }.should raise_error(Chef::Exceptions::ImmutableAttributeModification) + end + + end + + + describe "when reading from a stale sub tree" do + before do + @attributes.default[:sub_tree] = {:key => "old value", :ary => %w[foo bar]} + @sub_tree = @attributes[:sub_tree] + @sub_array = @attributes[:sub_tree][:ary] + @attributes.default[:sub_tree] = {:key => "new value"} + end + + it "detects reads from a no-longer-valid merged attributes sub-tree" do + lambda { @sub_tree[:key] }.should raise_error(Chef::Exceptions::StaleAttributeRead) + end + + it "detects reads from a no-longer-valid array value" do + lambda {@sub_array.first}.should raise_error(Chef::Exceptions::StaleAttributeRead) + end + [ + :[], + :all?, + :any?, + :assoc, + :chunk, + :collect, + :collect_concat, + :count, + :cycle, + :detect, + :drop, + :drop_while, + :each, + :each_cons, + :each_entry, + :each_key, + :each_pair, + :each_slice, + :each_value, + :each_with_index, + :each_with_object, + :empty?, + :entries, + :except, + :fetch, + :find, + :find_all, + :find_index, + :first, + :flat_map, + :flatten, + :grep, + :group_by, + :has_key?, + :has_value?, + :include?, + :index, + :inject, + :invert, + :key, + :key?, + :keys, + :length, + :map, + :max, + :max_by, + :member?, + :merge, + :min, + :min_by, + :minmax, + :minmax_by, + :none?, + :one?, + :partition, + :rassoc, + :reduce, + :reject, + :reverse_each, + :select, + :size, + :slice_before, + :sort, + :sort_by, + :store, + :symbolize_keys, + :take, + :take_while, + :to_a, + :to_hash, + :to_set, + :value?, + :values, + :values_at, + :zip + ].each do |reader| + it "detects dirty reads from a no-longer-valid Mash via Mash##{reader}" do + lambda { @sub_tree.send(:reader) }.should raise_error(Chef::Exceptions::StaleAttributeRead) + end + end + + + [ + :&, + :*, + :+, + :-, + :[], + :all?, + :any?, + :assoc, + :at, + :chunk, + :collect, + :collect_concat, + :combination, + :compact, + :concat, + :count, + :cycle, + :detect, + :drop, + :drop_while, + :each, + :each_cons, + :each_entry, + :each_index, + :each_slice, + :each_with_index, + :each_with_object, + :empty?, + :entries, + :fetch, + :find, + :find_all, + :find_index, + :first, + :flat_map, + :flatten, + :grep, + :group_by, + :include?, + :index, + :inject, + :join, + :last, + :length, + :map, + :max, + :max_by, + :member?, + :min, + :min_by, + :minmax, + :minmax_by, + :none?, + :one?, + :pack, + :partition, + :permutation, + :product, + :rassoc, + :reduce, + :reject, + :repeated_combination, + :repeated_permutation, + :reverse, + :reverse_each, + :rindex, + :rotate, + :sample, + :select, + :shelljoin, + :shuffle, + :size, + :slice, + :slice_before, + :sort, + :sort_by, + :take, + :take_while, + :to_a, + :to_ary, + :to_set, + :transpose, + :uniq, + :values_at, + :zip, + :| + ].each do |reader| + + it "detects dirty reads via Array##{reader}" do + lambda {@sub_array.send(reader)}.should raise_error(Chef::Exceptions::StaleAttributeRead) + end + end + + end + +end + diff --git a/spec/unit/node/immutable_collections_spec.rb b/spec/unit/node/immutable_collections_spec.rb new file mode 100644 index 0000000000..4ad5313415 --- /dev/null +++ b/spec/unit/node/immutable_collections_spec.rb @@ -0,0 +1,141 @@ +# +# Author:: Daniel DeLeo (<dan@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 "chef/node/immutable_collections" + +describe Chef::Node::ImmutableMash do + before do + @root = Chef::Node::Attribute.new({}, {}, {}, {}) + @data_in = {:top => {:second_level => "some value"}, + "top_level_2" => %w[array of values], + :top_level_3 => [{:hash_array => 1, :hash_array_b => 2}], + :top_level_4 => {:level2 => {:key => "value"}} + } + @immutable_mash = Chef::Node::ImmutableMash.new(@root, @data_in) + end + + it "element references like regular hash" do + @immutable_mash[:top][:second_level].should == "some value" + end + + it "elelment references like a regular Mash" do + @immutable_mash[:top_level_2].should == %w[array of values] + end + + it "converts Hash-like inputs into ImmutableMash's" do + @immutable_mash[:top].should be_a(Chef::Node::ImmutableMash) + end + + it "converts array inputs into ImmutableArray's" do + @immutable_mash[:top_level_2].should be_a(Chef::Node::ImmutableArray) + end + + it "converts arrays of hashes to ImmutableArray's of ImmutableMashes" do + @immutable_mash[:top_level_3].first.should be_a(Chef::Node::ImmutableMash) + end + + it "converts nested hashes to ImmutableMashes" do + @immutable_mash[:top_level_4].should be_a(Chef::Node::ImmutableMash) + @immutable_mash[:top_level_4][:level2].should be_a(Chef::Node::ImmutableMash) + end + + + [ + :[]=, + :clear, + :default=, + :default_proc=, + :delete, + :delete_if, + :keep_if, + :merge!, + :update, + :reject!, + :replace, + :select!, + :shift + ].each do |mutator| + it "doesn't allow mutation via `#{mutator}'" do + lambda { @immutable_mash.send(mutator) }.should raise_error(Chef::Exceptions::ImmutableAttributeModification) + end + end + + it "returns a mutable version of itself when duped" do + mutable = @immutable_mash.dup + mutable[:new_key] = :value + mutable[:new_key].should == :value + end + +end + +describe Chef::Node::ImmutableArray do + + before do + @root = Chef::Node::Attribute.new({}, {}, {}, {}) + @immutable_array = Chef::Node::ImmutableArray.new(@root, %w[foo bar baz]) + end + + ## + # Note: other behaviors, such as immutibilizing input data, are tested along + # with ImmutableMash, above + ### + + [ + :<<, + :[]=, + :clear, + :collect!, + :compact!, + :default=, + :default_proc=, + :delete, + :delete_at, + :delete_if, + :fill, + :flatten!, + :insert, + :keep_if, + :map!, + :merge!, + :pop, + :push, + :update, + :reject!, + :reverse!, + :replace, + :select!, + :shift, + :slice!, + :sort!, + :sort_by!, + :uniq!, + :unshift + ].each do |mutator| + it "does not allow mutation via `#{mutator}" do + lambda { @immutable_array.send(mutator)}.should raise_error(Chef::Exceptions::ImmutableAttributeModification) + end + end + + it "returns a mutable version of itself when duped" do + mutable = @immutable_array.dup + mutable[0] = :value + mutable[0].should == :value + end +end + diff --git a/spec/unit/node_spec.rb b/spec/unit/node_spec.rb new file mode 100644 index 0000000000..b6f63c9651 --- /dev/null +++ b/spec/unit/node_spec.rb @@ -0,0 +1,684 @@ +# +# 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::Node do + before(:each) do + @node = Chef::Node.new() + end + + it "creates a node and assigns it a name" do + node = Chef::Node.build('solo-node') + node.name.should == 'solo-node' + end + + it "should validate the name of the node" do + lambda{Chef::Node.build('solo node')}.should raise_error(Chef::Exceptions::ValidationFailed) + end + + describe "when the node does not exist on the server" do + before do + response = OpenStruct.new(:code => '404') + exception = Net::HTTPServerException.new("404 not found", response) + Chef::Node.stub!(:load).and_raise(exception) + @node.name("created-node") + end + + it "creates a new node for find_or_create" do + Chef::Node.stub!(:new).and_return(@node) + @node.should_receive(:create).and_return(@node) + node = Chef::Node.find_or_create("created-node") + node.name.should == 'created-node' + node.should equal(@node) + end + end + + describe "when the node exists on the server" do + before do + @node.name('existing-node') + Chef::Node.stub!(:load).and_return(@node) + end + + it "loads the node via the REST API for find_or_create" do + Chef::Node.find_or_create('existing-node').should equal(@node) + end + end + + describe "run_state" do + it "is an empty hash" do + @node.run_state.should respond_to(:keys) + @node.run_state.should be_empty + end + end + + describe "initialize" do + it "should default to the '_default' chef_environment" do + n = Chef::Node.new + n.chef_environment.should == '_default' + end + end + + describe "name" do + it "should allow you to set a name with name(something)" do + lambda { @node.name("latte") }.should_not raise_error + end + + it "should return the name with name()" do + @node.name("latte") + @node.name.should eql("latte") + end + + it "should always have a string for name" do + lambda { @node.name(Hash.new) }.should raise_error(ArgumentError) + end + + it "cannot be blank" do + lambda { @node.name("")}.should raise_error(Chef::Exceptions::ValidationFailed) + end + + it "should not accept name doesn't match /^[\-[:alnum:]_:.]+$/" do + lambda { @node.name("space in it")}.should raise_error(Chef::Exceptions::ValidationFailed) + end + end + + describe "chef_environment" do + it "should set an environment with chef_environment(something)" do + lambda { @node.chef_environment("latte") }.should_not raise_error + end + + it "should return the chef_environment with chef_environment()" do + @node.chef_environment("latte") + @node.chef_environment.should == "latte" + end + + it "should disallow non-strings" do + lambda { @node.chef_environment(Hash.new) }.should raise_error(ArgumentError) + lambda { @node.chef_environment(42) }.should raise_error(ArgumentError) + end + + it "cannot be blank" do + lambda { @node.chef_environment("")}.should raise_error(Chef::Exceptions::ValidationFailed) + end + end + + describe "attributes" do + it "should have attributes" do + @node.attribute.should be_a_kind_of(Hash) + end + + it "should allow attributes to be accessed by name or symbol directly on node[]" do + @node.default["locust"] = "something" + @node[:locust].should eql("something") + @node["locust"].should eql("something") + end + + it "should return nil if it cannot find an attribute with node[]" do + @node["secret"].should eql(nil) + end + + it "does not allow you to set an attribute via node[]=" do + lambda { @node["secret"] = "shush" }.should raise_error(Chef::Exceptions::ImmutableAttributeModification) + end + + it "should allow you to query whether an attribute exists with attribute?" do + @node.default["locust"] = "something" + @node.attribute?("locust").should eql(true) + @node.attribute?("no dice").should eql(false) + end + + it "should let you go deep with attribute?" do + @node.set["battles"]["people"]["wonkey"] = true + @node["battles"]["people"].attribute?("wonkey").should == true + @node["battles"]["people"].attribute?("snozzberry").should == false + end + + it "does not allow you to set an attribute via method_missing" do + lambda { @node.sunshine = "is bright"}.should raise_error(Chef::Exceptions::ImmutableAttributeModification) + end + + it "should allow you get get an attribute via method_missing" do + @node.default.sunshine = "is bright" + @node.sunshine.should eql("is bright") + end + + describe "normal attributes" do + it "should allow you to set an attribute with set, without pre-declaring a hash" do + @node.set[:snoopy][:is_a_puppy] = true + @node[:snoopy][:is_a_puppy].should == true + end + + it "should allow you to set an attribute with set_unless" do + @node.set_unless[:snoopy][:is_a_puppy] = false + @node[:snoopy][:is_a_puppy].should == false + end + + it "should not allow you to set an attribute with set_unless if it already exists" do + @node.set[:snoopy][:is_a_puppy] = true + @node.set_unless[:snoopy][:is_a_puppy] = false + @node[:snoopy][:is_a_puppy].should == true + end + + it "auto-vivifies attributes created via method syntax" do + @node.set.fuu.bahrr.baz = "qux" + @node.fuu.bahrr.baz.should == "qux" + end + + end + + describe "default attributes" do + it "should be set with default, without pre-declaring a hash" do + @node.default[:snoopy][:is_a_puppy] = true + @node[:snoopy][:is_a_puppy].should == true + end + + it "should allow you to set with default_unless without pre-declaring a hash" do + @node.default_unless[:snoopy][:is_a_puppy] = false + @node[:snoopy][:is_a_puppy].should == false + end + + it "should not allow you to set an attribute with default_unless if it already exists" do + @node.default[:snoopy][:is_a_puppy] = true + @node.default_unless[:snoopy][:is_a_puppy] = false + @node[:snoopy][:is_a_puppy].should == true + end + + it "auto-vivifies attributes created via method syntax" do + @node.default.fuu.bahrr.baz = "qux" + @node.fuu.bahrr.baz.should == "qux" + end + + end + + describe "override attributes" do + it "should be set with override, without pre-declaring a hash" do + @node.override[:snoopy][:is_a_puppy] = true + @node[:snoopy][:is_a_puppy].should == true + end + + it "should allow you to set with override_unless without pre-declaring a hash" do + @node.override_unless[:snoopy][:is_a_puppy] = false + @node[:snoopy][:is_a_puppy].should == false + end + + it "should not allow you to set an attribute with override_unless if it already exists" do + @node.override[:snoopy][:is_a_puppy] = true + @node.override_unless[:snoopy][:is_a_puppy] = false + @node[:snoopy][:is_a_puppy].should == true + end + + it "auto-vivifies attributes created via method syntax" do + @node.override.fuu.bahrr.baz = "qux" + @node.fuu.bahrr.baz.should == "qux" + end + + end + + it "should raise an ArgumentError if you ask for an attribute that doesn't exist via method_missing" do + lambda { @node.sunshine }.should raise_error(NoMethodError) + end + + it "should allow you to iterate over attributes with each_attribute" do + @node.default.sunshine = "is bright" + @node.default.canada = "is a nice place" + seen_attributes = Hash.new + @node.each_attribute do |a,v| + seen_attributes[a] = v + end + seen_attributes.should have_key("sunshine") + seen_attributes.should have_key("canada") + seen_attributes["sunshine"].should == "is bright" + seen_attributes["canada"].should == "is a nice place" + end + end + + describe "consuming json" do + + before do + @ohai_data = {:platform => 'foo', :platform_version => 'bar'} + end + + it "consumes the run list portion of a collection of attributes and returns the remainder" do + attrs = {"run_list" => [ "role[base]", "recipe[chef::server]" ], "foo" => "bar"} + @node.consume_run_list(attrs).should == {"foo" => "bar"} + @node.run_list.should == [ "role[base]", "recipe[chef::server]" ] + end + + it "should overwrites the run list with the run list it consumes" do + @node.consume_run_list "recipes" => [ "one", "two" ] + @node.consume_run_list "recipes" => [ "three" ] + @node.run_list.should == [ "three" ] + end + + it "should not add duplicate recipes from the json attributes" do + @node.run_list << "one" + @node.consume_run_list "recipes" => [ "one", "two", "three" ] + @node.run_list.should == [ "one", "two", "three" ] + end + + it "doesn't change the run list if no run_list is specified in the json" do + @node.run_list << "role[database]" + @node.consume_run_list "foo" => "bar" + @node.run_list.should == ["role[database]"] + end + + it "raises an exception if you provide both recipe and run_list attributes, since this is ambiguous" do + lambda { @node.consume_run_list "recipes" => "stuff", "run_list" => "other_stuff" }.should raise_error(Chef::Exceptions::AmbiguousRunlistSpecification) + end + + it "should add json attributes to the node" do + @node.consume_external_attrs(@ohai_data, {"one" => "two", "three" => "four"}) + @node.one.should eql("two") + @node.three.should eql("four") + end + + it "should set the tags attribute to an empty array if it is not already defined" do + @node.consume_external_attrs(@ohai_data, {}) + @node.tags.should eql([]) + end + + it "should not set the tags attribute to an empty array if it is already defined" do + @node.normal[:tags] = [ "radiohead" ] + @node.consume_external_attrs(@ohai_data, {}) + @node.tags.should eql([ "radiohead" ]) + end + + it "deep merges attributes instead of overwriting them" do + @node.consume_external_attrs(@ohai_data, "one" => {"two" => {"three" => "four"}}) + @node.one.to_hash.should == {"two" => {"three" => "four"}} + @node.consume_external_attrs(@ohai_data, "one" => {"abc" => "123"}) + @node.consume_external_attrs(@ohai_data, "one" => {"two" => {"foo" => "bar"}}) + @node.one.to_hash.should == {"two" => {"three" => "four", "foo" => "bar"}, "abc" => "123"} + end + + it "gives attributes from JSON priority when deep merging" do + @node.consume_external_attrs(@ohai_data, "one" => {"two" => {"three" => "four"}}) + @node.one.to_hash.should == {"two" => {"three" => "four"}} + @node.consume_external_attrs(@ohai_data, "one" => {"two" => {"three" => "forty-two"}}) + @node.one.to_hash.should == {"two" => {"three" => "forty-two"}} + end + + end + + describe "preparing for a chef client run" do + before do + @ohai_data = {:platform => 'foobuntu', :platform_version => '23.42'} + end + + it "sets its platform according to platform detection" do + @node.consume_external_attrs(@ohai_data, {}) + @node.automatic_attrs[:platform].should == 'foobuntu' + @node.automatic_attrs[:platform_version].should == '23.42' + end + + it "consumes the run list from provided json attributes" do + @node.consume_external_attrs(@ohai_data, {"run_list" => ['recipe[unicorn]']}) + @node.run_list.should == ['recipe[unicorn]'] + end + + it "saves non-runlist json attrs for later" do + expansion = Chef::RunList::RunListExpansion.new('_default', []) + @node.run_list.stub!(:expand).and_return(expansion) + @node.consume_external_attrs(@ohai_data, {"foo" => "bar"}) + @node.expand! + @node.normal_attrs.should == {"foo" => "bar", "tags" => []} + end + + end + + describe "when expanding its run list and merging attributes" do + before do + @expansion = Chef::RunList::RunListExpansion.new("_default", []) + @node.run_list.stub!(:expand).and_return(@expansion) + end + + it "sets the 'recipes' automatic attribute to the recipes in the expanded run_list" do + @expansion.recipes << 'recipe[chef::client]' << 'recipe[nginx::default]' + @node.expand! + @node.automatic_attrs[:recipes].should == ['recipe[chef::client]', 'recipe[nginx::default]'] + end + + it "sets the 'roles' automatic attribute to the expanded role list" do + @expansion.instance_variable_set(:@applied_roles, {'arf' => nil, 'countersnark' => nil}) + @node.expand! + @node.automatic_attrs[:roles].sort.should == ['arf', 'countersnark'] + end + + end + + describe "when clearing computed state at the beginning of a run" do + before do + @node.default[:foo] = "default" + @node.normal[:foo] = "normal" + @node.override[:foo] = "override" + @node.reset_defaults_and_overrides + end + + it "removes default attributes" do + @node.default.should be_empty + end + + it "removes override attributes" do + @node.override.should be_empty + end + + it "leaves normal level attributes untouched" do + @node[:foo].should == "normal" + end + + end + + describe "when merging environment attributes" do + before do + @node.chef_environment = "rspec" + @expansion = Chef::RunList::RunListExpansion.new("rspec", []) + @expansion.default_attrs.replace({:default => "from role", :d_role => "role only"}) + @expansion.override_attrs.replace({:override => "from role", :o_role => "role only"}) + + + @environment = Chef::Environment.new + @environment.default_attributes = {:default => "from env", :d_env => "env only" } + @environment.override_attributes = {:override => "from env", :o_env => "env only"} + Chef::Environment.stub!(:load).and_return(@environment) + @node.apply_expansion_attributes(@expansion) + end + + it "does not nuke role-only default attrs" do + @node[:d_role].should == "role only" + end + + it "does not nuke role-only override attrs" do + @node[:o_role].should == "role only" + end + + it "does not nuke env-only default attrs" do + @node[:o_env].should == "env only" + end + + it "does not nuke role-only override attrs" do + @node[:o_env].should == "env only" + end + + it "gives role defaults precedence over env defaults" do + @node[:default].should == "from role" + end + + it "gives env overrides precedence over role overrides" do + @node[:override].should == "from env" + end + end + + describe "when evaluating attributes files" do + before do + @node = Chef::Node.new + + @cookbook_repo = File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks")) + @cookbook_loader = Chef::CookbookLoader.new(@cookbook_repo) + @cookbook_loader.load_cookbooks + + @cookbook_collection = Chef::CookbookCollection.new(@cookbook_loader.cookbooks_by_name) + + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, @cookbook_collection, @events) + + @node.include_attribute("openldap::default") + @node.include_attribute("openldap::smokey") + end + + it "sets attributes from the files" do + @node.ldap_server.should eql("ops1prod") + @node.ldap_basedn.should eql("dc=hjksolutions,dc=com") + @node.ldap_replication_password.should eql("forsure") + @node.smokey.should eql("robinson") + end + + it "gives a sensible error when attempting to load a missing attributes file" do + lambda { @node.include_attribute("nope-this::doesnt-exist") }.should raise_error(Chef::Exceptions::CookbookNotFound) + end + end + + describe "roles" do + it "should allow you to query whether or not it has a recipe applied with role?" do + @node.run_list << "role[sunrise]" + @node.role?("sunrise").should eql(true) + @node.role?("not at home").should eql(false) + end + + it "should allow you to set roles with arguments" do + @node.run_list << "role[one]" + @node.run_list << "role[two]" + @node.role?("one").should eql(true) + @node.role?("two").should eql(true) + end + end + + describe "run_list" do + it "should have a Chef::RunList of recipes and roles that should be applied" do + @node.run_list.should be_a_kind_of(Chef::RunList) + end + + it "should allow you to query the run list with arguments" do + @node.run_list "recipe[baz]" + @node.run_list?("recipe[baz]").should eql(true) + end + + it "should allow you to set the run list with arguments" do + @node.run_list "recipe[baz]", "role[foo]" + @node.run_list?("recipe[baz]").should eql(true) + @node.run_list?("role[foo]").should eql(true) + end + end + + describe "from file" do + it "should load a node from a ruby file" do + @node.from_file(File.expand_path(File.join(CHEF_SPEC_DATA, "nodes", "test.rb"))) + @node.name.should eql("test.example.com-short") + @node.sunshine.should eql("in") + @node.something.should eql("else") + @node.run_list.should == ["operations-master", "operations-monitoring"] + end + + it "should raise an exception if the file cannot be found or read" do + lambda { @node.from_file("/tmp/monkeydiving") }.should raise_error(IOError) + end + end + + describe "update_from!" do + before(:each) do + @node.name("orig") + @node.chef_environment("dev") + @node.default_attrs = { "one" => { "two" => "three", "four" => "five", "eight" => "nine" } } + @node.override_attrs = { "one" => { "two" => "three", "four" => "six" } } + @node.normal_attrs = { "one" => { "two" => "seven" } } + @node.run_list << "role[marxist]" + @node.run_list << "role[leninist]" + @node.run_list << "recipe[stalinist]" + + @example = Chef::Node.new() + @example.name("newname") + @example.chef_environment("prod") + @example.default_attrs = { "alpha" => { "bravo" => "charlie", "delta" => "echo" } } + @example.override_attrs = { "alpha" => { "bravo" => "foxtrot", "delta" => "golf" } } + @example.normal_attrs = { "alpha" => { "bravo" => "hotel" } } + @example.run_list << "role[comedy]" + @example.run_list << "role[drama]" + @example.run_list << "recipe[mystery]" + end + + it "allows update of everything except name" do + @node.update_from!(@example) + @node.name.should == "orig" + @node.chef_environment.should == @example.chef_environment + @node.default_attrs.should == @example.default_attrs + @node.override_attrs.should == @example.override_attrs + @node.normal_attrs.should == @example.normal_attrs + @node.run_list.should == @example.run_list + end + + it "should not update the name of the node" do + @node.should_not_receive(:name).with(@example.name) + @node.update_from!(@example) + end + end + + describe "to_hash" do + it "should serialize itself as a hash" do + @node.chef_environment("dev") + @node.default_attrs = { "one" => { "two" => "three", "four" => "five", "eight" => "nine" } } + @node.override_attrs = { "one" => { "two" => "three", "four" => "six" } } + @node.normal_attrs = { "one" => { "two" => "seven" } } + @node.run_list << "role[marxist]" + @node.run_list << "role[leninist]" + @node.run_list << "recipe[stalinist]" + h = @node.to_hash + h["one"]["two"].should == "three" + h["one"]["four"].should == "six" + h["one"]["eight"].should == "nine" + h["role"].should be_include("marxist") + h["role"].should be_include("leninist") + h["run_list"].should be_include("role[marxist]") + h["run_list"].should be_include("role[leninist]") + h["run_list"].should be_include("recipe[stalinist]") + h["chef_environment"].should == "dev" + end + end + + describe "json" do + it "should serialize itself as json", :json => true do + @node.from_file(File.expand_path("nodes/test.example.com.rb", CHEF_SPEC_DATA)) + json = Chef::JSONCompat.to_json(@node) + json.should =~ /json_class/ + json.should =~ /name/ + json.should =~ /chef_environment/ + json.should =~ /normal/ + json.should =~ /default/ + json.should =~ /override/ + json.should =~ /run_list/ + end + + it 'should serialze valid json with a run list', :json => true do + #This test came about because activesupport mucks with Chef json serialization + #Test should pass with and without Activesupport + @node.run_list << {"type" => "role", "name" => 'Cthulu'} + @node.run_list << {"type" => "role", "name" => 'Hastur'} + json = Chef::JSONCompat.to_json(@node) + json.should =~ /\"run_list\":\[\"role\[Cthulu\]\",\"role\[Hastur\]\"\]/ + end + + it "should deserialize itself from json", :json => true do + @node.from_file(File.expand_path("nodes/test.example.com.rb", CHEF_SPEC_DATA)) + json = Chef::JSONCompat.to_json(@node) + serialized_node = Chef::JSONCompat.from_json(json) + serialized_node.should be_a_kind_of(Chef::Node) + serialized_node.name.should eql(@node.name) + serialized_node.chef_environment.should eql(@node.chef_environment) + @node.each_attribute do |k,v| + serialized_node[k].should eql(v) + end + serialized_node.run_list.should == @node.run_list + end + end + + describe "to_s" do + it "should turn into a string like node[name]" do + @node.name("airplane") + @node.to_s.should eql("node[airplane]") + end + end + + describe "api model" do + before(:each) do + @rest = mock("Chef::REST") + Chef::REST.stub!(:new).and_return(@rest) + @query = mock("Chef::Search::Query") + Chef::Search::Query.stub!(:new).and_return(@query) + end + + describe "list" do + describe "inflated" do + it "should return a hash of node names and objects" do + n1 = mock("Chef::Node", :name => "one") + @query.should_receive(:search).with(:node).and_yield(n1) + r = Chef::Node.list(true) + r["one"].should == n1 + end + end + + it "should return a hash of node names and urls" do + @rest.should_receive(:get_rest).and_return({ "one" => "http://foo" }) + r = Chef::Node.list + r["one"].should == "http://foo" + end + end + + describe "load" do + it "should load a node by name" do + @rest.should_receive(:get_rest).with("nodes/monkey").and_return("foo") + Chef::Node.load("monkey").should == "foo" + end + end + + describe "destroy" do + it "should destroy a node" do + @rest.should_receive(:delete_rest).with("nodes/monkey").and_return("foo") + @node.name("monkey") + @node.destroy + end + end + + describe "save" do + it "should update a node if it already exists" do + @node.name("monkey") + @rest.should_receive(:put_rest).with("nodes/monkey", @node).and_return("foo") + @node.save + end + + it "should not try and create if it can update" do + @node.name("monkey") + @rest.should_receive(:put_rest).with("nodes/monkey", @node).and_return("foo") + @rest.should_not_receive(:post_rest) + @node.save + end + + it "should create if it cannot update" do + @node.name("monkey") + exception = mock("404 error", :code => "404") + @rest.should_receive(:put_rest).and_raise(Net::HTTPServerException.new("foo", exception)) + @rest.should_receive(:post_rest).with("nodes", @node) + @node.save + end + + describe "when whyrun mode is enabled" do + before do + Chef::Config[:why_run] = true + end + after do + Chef::Config[:why_run] = false + end + it "should not save" do + @node.name("monkey") + @rest.should_not_receive(:put_rest) + @rest.should_not_receive(:post_rest) + @node.save + end + end + end + end + +end diff --git a/spec/unit/platform_spec.rb b/spec/unit/platform_spec.rb new file mode 100644 index 0000000000..b09a68b528 --- /dev/null +++ b/spec/unit/platform_spec.rb @@ -0,0 +1,240 @@ +# +# 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::Platform supports" do + [ + :mac_os_x, + :mac_os_x_server, + :freebsd, + :ubuntu, + :debian, + :centos, + :fedora, + :suse, + :redhat, + :oracle, + :gentoo, + :arch, + :solaris, + :mswin, + :mingw32, + :windows + ].each do |platform| + it "#{platform}" do + Chef::Platform.platforms.should have_key(platform) + end + end +end + +describe Chef::Platform do + + before :all do + @original_platform_map = Chef::Platform.platforms + end + + after :all do || + Chef::Platform.platforms = @original_platform_map + end + + before(:each) do + Chef::Platform.platforms = { + :darwin => { + "9.2.2" => { + :file => "darwinian", + :else => "thing" + }, + :default => { + :file => "old school", + :snicker => "snack" + } + }, + :mars_volta => { + }, + :default => { + :file => Chef::Provider::File, + :pax => "brittania", + :cat => "nice" + } + } + @events = Chef::EventDispatch::Dispatcher.new + end + + it "should allow you to look up a platform by name and version, returning the provider map for it" do + pmap = Chef::Platform.find("Darwin", "9.2.2") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql("darwinian") + end + + it "should use the default providers for an os if the specific version does not exist" do + pmap = Chef::Platform.find("Darwin", "1") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql("old school") + end + + it "should use the default providers if the os doesn't give me a default, but does exist" do + pmap = Chef::Platform.find("mars_volta", "1") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql(Chef::Provider::File) + end + + it "should use the default provider if the os does not exist" do + pmap = Chef::Platform.find("AIX", "1") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql(Chef::Provider::File) + end + + it "should merge the defaults for an os with the specific version" do + pmap = Chef::Platform.find("Darwin", "9.2.2") + pmap[:file].should eql("darwinian") + pmap[:snicker].should eql("snack") + end + + it "should merge the defaults for an os with the universal defaults" do + pmap = Chef::Platform.find("Darwin", "9.2.2") + pmap[:file].should eql("darwinian") + pmap[:pax].should eql("brittania") + end + + it "should allow you to look up a provider for a platform directly by symbol" do + Chef::Platform.find_provider("Darwin", "9.2.2", :file).should eql("darwinian") + end + + it "should raise an exception if a provider cannot be found for a resource type" do + lambda { Chef::Platform.find_provider("Darwin", "9.2.2", :coffee) }.should raise_error(ArgumentError) + end + + it "should look up a provider for a resource with a Chef::Resource object" do + kitty = Chef::Resource::Cat.new("loulou") + Chef::Platform.find_provider("Darwin", "9.2.2", kitty).should eql("nice") + end + + it "should look up a provider with a node and a Chef::Resource object" do + kitty = Chef::Resource::Cat.new("loulou") + node = Chef::Node.new + node.name("Intel") + node.automatic_attrs[:platform] = "mac_os_x" + node.automatic_attrs[:platform_version] = "9.2.2" + Chef::Platform.find_provider_for_node(node, kitty).should eql("nice") + end + + it "should prefer an explicit provider" do + kitty = Chef::Resource::Cat.new("loulou") + kitty.stub!(:provider).and_return(Chef::Provider::File) + node = Chef::Node.new + node.name("Intel") + node.automatic_attrs[:platform] = "mac_os_x" + node.automatic_attrs[:platform_version] = "9.2.2" + Chef::Platform.find_provider_for_node(node, kitty).should eql(Chef::Provider::File) + end + + it "should look up a provider based on the resource name if nothing else matches" do + kitty = Chef::Resource::Cat.new("loulou") + class Chef::Provider::Cat < Chef::Provider; end + Chef::Platform.platforms[:default].delete(:cat) + node = Chef::Node.new + node.name("Intel") + node.automatic_attrs[:platform] = "mac_os_x" + node.automatic_attrs[:platform_version] = "8.5" + Chef::Platform.find_provider_for_node(node, kitty).should eql(Chef::Provider::Cat) + end + + def setup_file_resource + node = Chef::Node.new + node.automatic_attrs[:platform] = "mac_os_x" + node.automatic_attrs[:platform_version] = "9.2.2" + run_context = Chef::RunContext.new(node, {}, @events) + [ Chef::Resource::File.new("whateva", run_context), run_context ] + end + + it "returns a provider object given a Chef::Resource object which has a valid run context and an action" do + file, run_context = setup_file_resource + provider = Chef::Platform.provider_for_resource(file, :foo) + provider.should be_an_instance_of(Chef::Provider::File) + provider.new_resource.should equal(file) + provider.run_context.should equal(run_context) + end + + it "returns a provider object given a Chef::Resource object which has a valid run context without an action" do + file, run_context = setup_file_resource + provider = Chef::Platform.provider_for_resource(file) + provider.should be_an_instance_of(Chef::Provider::File) + provider.new_resource.should equal(file) + provider.run_context.should equal(run_context) + end + + it "raises an error when trying to find the provider for a resource with no run context" do + file = Chef::Resource::File.new("whateva") + lambda {Chef::Platform.provider_for_resource(file)}.should raise_error(ArgumentError) + end + + it "does not support finding a provider by resource and node -- a run context is required" do + lambda {Chef::Platform.provider_for_node('node', 'resource')}.should raise_error(NotImplementedError) + end + + it "should update the provider map with map" do + Chef::Platform.set( + :platform => :darwin, + :version => "9.2.2", + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:darwin]["9.2.2"][:file].should eql("masterful") + Chef::Platform.set( + :platform => :darwin, + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:darwin][:default][:file].should eql("masterful") + Chef::Platform.set( + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:default][:file].should eql("masterful") + + Chef::Platform.set( + :platform => :hero, + :version => "9.2.2", + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:hero]["9.2.2"][:file].should eql("masterful") + + Chef::Platform.set( + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:default][:file].should eql("masterful") + + Chef::Platform.platforms = {} + + Chef::Platform.set( + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:default][:file].should eql("masterful") + + Chef::Platform.platforms = { :neurosis => {} } + Chef::Platform.set(:platform => :neurosis, :resource => :package, :provider => "masterful") + Chef::Platform.platforms[:neurosis][:default][:package].should eql("masterful") + + end + + +end 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 diff --git a/spec/unit/provider_spec.rb b/spec/unit/provider_spec.rb new file mode 100644 index 0000000000..b0edf492ab --- /dev/null +++ b/spec/unit/provider_spec.rb @@ -0,0 +1,168 @@ +# +# 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' + + +class NoWhyrunDemonstrator < Chef::Provider + attr_reader :system_state_altered + def whyrun_supported? + false + end + def load_current_resource + + end + def action_foo + @system_state_altered = true + end +end + +class ConvergeActionDemonstrator < Chef::Provider + attr_reader :system_state_altered + + def whyrun_supported? + true + end + + def load_current_resource + end + + def action_foo + converge_by("running a state changing action") do + @system_state_altered = true + end + end +end + +describe Chef::Provider do + before(:each) do + @cookbook_collection = Chef::CookbookCollection.new([]) + @node = Chef::Node.new + @node.name "latte" + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, @cookbook_collection, @events) + @resource = Chef::Resource.new("funk", @run_context) + @resource.cookbook_name = "a_delicious_pie" + @provider = Chef::Provider.new(@resource, @run_context) + 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 have nil for current_resource by default" do + @provider.current_resource.should eql(nil) + end + + it "should return true for action_nothing" do + @provider.action_nothing.should eql(true) + end + + it "evals embedded recipes with a pristine resource collection" do + @provider.run_context.instance_variable_set(:@resource_collection, "doesn't matter what this is") + temporary_collection = nil + snitch = Proc.new {temporary_collection = @run_context.resource_collection} + @provider.send(:recipe_eval, &snitch) + @provider.converge + temporary_collection.should be_an_instance_of(Chef::ResourceCollection) + @provider.run_context.instance_variable_get(:@resource_collection).should == "doesn't matter what this is" + end + + it "does not re-load recipes when creating the temporary run context" do + # we actually want to test that RunContext#load is never called, but we + # can't stub all instances of an object with rspec's mocks. :/ + Chef::RunContext.stub!(:new).and_raise("not supposed to happen") + snitch = Proc.new {temporary_collection = @run_context.resource_collection} + @provider.send(:recipe_eval, &snitch) + @provider.converge + end + + context "when no converge actions are queued" do + before do + @provider.stub!(:whyrun_supported?).and_return(true) + @provider.stub!(:load_current_resource) + end + + it "does not mark the new resource as updated" do + @provider.converge + @resource.should_not be_updated + @resource.should_not be_updated_by_last_action + end + end + + context "when converge actions have been added to the queue" do + describe "and provider supports whyrun mode" do + before do + @provider = ConvergeActionDemonstrator.new(@resource, @run_context) + end + + it "should tell us that it does support whyrun" do + @provider.should be_whyrun_supported + end + + it "queues up converge actions" do + @provider.action_foo + @provider.send(:converge_actions).should have(1).actions + end + + it "executes pending converge actions to converge the system" do + @provider.run_action(:foo) + @provider.instance_variable_get(:@system_state_altered).should be_true + end + + it "marks the resource as updated" do + @provider.run_action(:foo) + @resource.should be_updated + @resource.should be_updated_by_last_action + end + end + + describe "and provider does not support whyrun mode" do + before do + Chef::Config[:why_run] = true + @provider = NoWhyrunDemonstrator.new(@resource, @run_context) + end + + after do + Chef::Config[:why_run] = false + end + + it "should tell us that it doesn't support whyrun" do + @provider.should_not be_whyrun_supported + end + + it "should automatically generate a converge_by block on the provider's behalf" do + @provider.run_action(:foo) + @provider.send(:converge_actions).should have(0).actions + @provider.system_state_altered.should be_false + end + + it "should automatically execute the generated converge_by block" do + @provider.run_action(:foo) + @provider.system_state_altered.should be_false + @resource.should_not be_updated + @resource.should_not be_updated_by_last_action + end + end + end + +end diff --git a/spec/unit/recipe_spec.rb b/spec/unit/recipe_spec.rb new file mode 100644 index 0000000000..4615bcb4d4 --- /dev/null +++ b/spec/unit/recipe_spec.rb @@ -0,0 +1,270 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Author:: Tim Hinderliter (<tim@opscode.com>) +# Author:: Seth Chisamore (<schisamo@opscode.com>) +# Copyright:: Copyright (c) 2008-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::Recipe do + before(:each) do + @cookbook_repo = File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "cookbooks")) + cl = Chef::CookbookLoader.new(@cookbook_repo) + cl.load_cookbooks + @cookbook_collection = Chef::CookbookCollection.new(cl) + @node = Chef::Node.new + @node.normal[:tags] = Array.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, @cookbook_collection, @events) + @recipe = Chef::Recipe.new("hjk", "test", @run_context) + + # Shell/ext.rb is on the run path, and it defines + # Chef::Recipe#resources to call pp, which we don't want when + # we're running tests. + @recipe.stub!(:pp) + end + + describe "method_missing" do + describe "resources" do + it "should load a two word (zen_master) resource" do + lambda do + @recipe.zen_master "monkey" do + peace true + end + end.should_not raise_error(ArgumentError) + end + + it "should load a one word (cat) resource" do + lambda do + @recipe.cat "loulou" do + pretty_kitty true + end + end.should_not raise_error(ArgumentError) + end + + it "should load a four word (one_two_three_four) resource" do + lambda do + @recipe.one_two_three_four "numbers" do + i_can_count true + end + end.should_not raise_error(ArgumentError) + end + + it "should throw an error if you access a resource that we can't find" do + lambda { @recipe.not_home("not_home_resource") }.should raise_error(NameError) + end + + it "should require a name argument" do + lambda { + @recipe.cat + }.should raise_error(ArgumentError, "You must supply a name when declaring a cat resource") + end + + it "should allow regular errors (not NameErrors) to pass unchanged" do + lambda { + @recipe.cat("felix") { raise ArgumentError, "You Suck" } + }.should raise_error(ArgumentError) + end + + it "should add our zen_master to the collection" do + @recipe.zen_master "monkey" do + peace true + end + @run_context.resource_collection.lookup("zen_master[monkey]").name.should eql("monkey") + end + + it "should add our zen masters to the collection in the order they appear" do + %w{monkey dog cat}.each do |name| + @recipe.zen_master name do + peace true + end + end + + @run_context.resource_collection.map{|r| r.name}.should eql(["monkey", "dog", "cat"]) + end + + it "should return the new resource after creating it" do + res = @recipe.zen_master "makoto" do + peace true + end + res.resource_name.should eql(:zen_master) + res.name.should eql("makoto") + end + + describe "should locate platform mapped resources" do + + it "locate resource for particular platform" do + Object.const_set('ShaunTheSheep', Class.new(Chef::Resource){ provides :laughter, :on_platforms => ["television"] }) + @node.automatic[:platform] = "television" + @node.automatic[:platform_version] = "123" + res = @recipe.laughter "timmy" + res.name.should eql("timmy") + res.kind_of?(ShaunTheSheep) + end + + it "locate a resource for all platforms" do + Object.const_set("YourMom", Class.new(Chef::Resource){ provides :love_and_caring }) + res = @recipe.love_and_caring "mommy" + res.name.should eql("mommy") + res.kind_of?(YourMom) + end + + end + end + + describe "resource definitions" do + it "should execute defined resources" do + crow_define = Chef::ResourceDefinition.new + crow_define.define :crow, :peace => false, :something => true do + zen_master "lao tzu" do + peace params[:peace] + something params[:something] + end + end + @run_context.definitions[:crow] = crow_define + @recipe.crow "mine" do + peace true + end + @run_context.resource_collection.resources(:zen_master => "lao tzu").name.should eql("lao tzu") + @run_context.resource_collection.resources(:zen_master => "lao tzu").something.should eql(true) + end + + it "should set the node on defined resources" do + crow_define = Chef::ResourceDefinition.new + crow_define.define :crow, :peace => false, :something => true do + zen_master "lao tzu" do + peace params[:peace] + something params[:something] + end + end + @run_context.definitions[:crow] = crow_define + @node.normal[:foo] = false + @recipe.crow "mine" do + something node[:foo] + end + @recipe.resources(:zen_master => "lao tzu").something.should eql(false) + end + end + + end + + describe "instance_eval" do + it "should handle an instance_eval properly" do + code = <<-CODE + zen_master "gnome" do + peace = true + end + CODE + lambda { @recipe.instance_eval(code) }.should_not raise_error + @recipe.resources(:zen_master => "gnome").name.should eql("gnome") + end + end + + describe "from_file" do + it "should load a resource from a ruby file" do + @recipe.from_file(File.join(CHEF_SPEC_DATA, "recipes", "test.rb")) + res = @recipe.resources(:file => "/etc/nsswitch.conf") + res.name.should eql("/etc/nsswitch.conf") + res.action.should eql([:create]) + res.owner.should eql("root") + res.group.should eql("root") + res.mode.should eql(0644) + end + + it "should raise an exception if the file cannot be found or read" do + lambda { @recipe.from_file("/tmp/monkeydiving") }.should raise_error(IOError) + end + end + + describe "include_recipe" do + it "should evaluate another recipe with include_recipe" do + @run_context.include_recipe "openldap::gigantor" + res = @run_context.resource_collection.resources(:cat => "blanket") + res.name.should eql("blanket") + res.pretty_kitty.should eql(false) + end + + it "should load the default recipe for a cookbook if include_recipe is called without a ::" do + @run_context.include_recipe "openldap" + res = @run_context.resource_collection.resources(:cat => "blanket") + res.name.should eql("blanket") + res.pretty_kitty.should eql(true) + end + + it "should store that it has seen a recipe in the run_context" do + @run_context.include_recipe "openldap" + @run_context.loaded_recipe?("openldap").should be_true + end + + it "should not include the same recipe twice" do + @cookbook_collection[:openldap].should_receive(:load_recipe).with("default", @run_context) + @recipe.include_recipe "openldap" + @cookbook_collection[:openldap].should_not_receive(:load_recipe).with("default", @run_context) + @recipe.include_recipe "openldap" + end + end + + describe "tags" do + it "should set tags via tag" do + @recipe.tag "foo" + @node[:tags].should include("foo") + end + + it "should set multiple tags via tag" do + @recipe.tag "foo", "bar" + @node[:tags].should include("foo") + @node[:tags].should include("bar") + end + + it "should not set the same tag twice via tag" do + @recipe.tag "foo" + @recipe.tag "foo" + @node[:tags].should eql([ "foo" ]) + end + + it "should return the current list of tags from tag with no arguments" do + @recipe.tag "foo" + @recipe.tag.should eql([ "foo" ]) + end + + it "should return true from tagged? if node is tagged" do + @recipe.tag "foo" + @recipe.tagged?("foo").should be(true) + end + + it "should return false from tagged? if node is not tagged" do + @recipe.tagged?("foo").should be(false) + end + + it "should return false from tagged? if node is not tagged" do + @recipe.tagged?("foo").should be(false) + end + + it "should remove a tag from the tag list via untag" do + @recipe.tag "foo" + @recipe.untag "foo" + @node[:tags].should eql([]) + end + + it "should remove multiple tags from the tag list via untag" do + @recipe.tag "foo", "bar" + @recipe.untag "bar", "foo" + @node[:tags].should eql([]) + end + end +end diff --git a/spec/unit/resource/apt_package_spec.rb b/spec/unit/resource/apt_package_spec.rb new file mode 100644 index 0000000000..795ffc6fc4 --- /dev/null +++ b/spec/unit/resource/apt_package_spec.rb @@ -0,0 +1,43 @@ +# +# 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::Resource::AptPackage, "initialize" do + + before(:each) do + @resource = Chef::Resource::AptPackage.new("foo") + end + + it "should return a Chef::Resource::AptPackage" do + @resource.should be_a_kind_of(Chef::Resource::AptPackage) + end + + it "should set the resource_name to :apt_package" do + @resource.resource_name.should eql(:apt_package) + end + + it "should set the provider to Chef::Provider::Package::Apt" do + @resource.provider.should eql(Chef::Provider::Package::Apt) + end + + it "should support default_release" do + @resource.default_release("lenny-backports") + @resource.default_release.should eql("lenny-backports") + end +end diff --git a/spec/unit/resource/bash_spec.rb b/spec/unit/resource/bash_spec.rb new file mode 100644 index 0000000000..c7f31e1de6 --- /dev/null +++ b/spec/unit/resource/bash_spec.rb @@ -0,0 +1,40 @@ +# +# 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::Resource::Bash do + + before(:each) do + @resource = Chef::Resource::Bash.new("fakey_fakerton") + end + + it "should create a new Chef::Resource::Bash" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Bash) + end + + it "should have a resource name of :bash" do + @resource.resource_name.should eql(:bash) + end + + it "should have an interpreter of bash" do + @resource.interpreter.should eql("bash") + end + +end diff --git a/spec/unit/resource/breakpoint_spec.rb b/spec/unit/resource/breakpoint_spec.rb new file mode 100644 index 0000000000..8eaabb546d --- /dev/null +++ b/spec/unit/resource/breakpoint_spec.rb @@ -0,0 +1,43 @@ +# +# 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::Resource::Breakpoint do + + before do + @breakpoint = Chef::Resource::Breakpoint.new + end + + it "allows the action :break" do + @breakpoint.allowed_actions.should include(:break) + end + + it "defaults to the break action" do + @breakpoint.action.should == "break" + end + + it "names itself after the line number of the file where it's created" do + @breakpoint.name.should match(/breakpoint_spec\.rb\:[\d]{2}\:in \`new\'$/) + end + + it "uses the breakpoint provider" do + @breakpoint.provider.should == Chef::Provider::Breakpoint + end + +end diff --git a/spec/unit/resource/chef_gem_spec.rb b/spec/unit/resource/chef_gem_spec.rb new file mode 100644 index 0000000000..54def9a49d --- /dev/null +++ b/spec/unit/resource/chef_gem_spec.rb @@ -0,0 +1,49 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2008, 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::Resource::ChefGem, "initialize" do + + before(:each) do + @resource = Chef::Resource::ChefGem.new("foo") + end + + it "should return a Chef::Resource::ChefGem" do + @resource.should be_a_kind_of(Chef::Resource::ChefGem) + end + + it "should set the resource_name to :chef_gem" do + @resource.resource_name.should eql(:chef_gem) + end + + it "should set the provider to Chef::Provider::Package::Rubygems" do + @resource.provider.should eql(Chef::Provider::Package::Rubygems) + end +end + +describe Chef::Resource::ChefGem, "gem_binary" do + before(:each) do + @resource = Chef::Resource::ChefGem.new("foo") + end + + it "should raise an exception when gem_binary is set" do + lambda { @resource.gem_binary("/lol/cats/gem") }.should raise_error(ArgumentError) + end +end diff --git a/spec/unit/resource/conditional_spec.rb b/spec/unit/resource/conditional_spec.rb new file mode 100644 index 0000000000..1be7bcea71 --- /dev/null +++ b/spec/unit/resource/conditional_spec.rb @@ -0,0 +1,147 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.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' +require 'ostruct' + +describe Chef::Resource::Conditional do + before do + Mixlib::ShellOut.any_instance.stub(:run_command).and_return(nil) + @status = OpenStruct.new(:success? => true) + Mixlib::ShellOut.any_instance.stub(:status).and_return(@status) + end + + describe "when created as an `only_if`" do + describe "after running a successful command" do + before do + @conditional = Chef::Resource::Conditional.only_if("true") + end + + it "indicates that resource convergence should continue" do + @conditional.continue?.should be_true + end + end + + describe "after running a negative/false command" do + before do + @status.send("success?=", false) + @conditional = Chef::Resource::Conditional.only_if("false") + end + + it "indicates that resource convergence should not continue" do + @conditional.continue?.should be_false + end + end + + describe 'after running a command which timed out' do + before do + @conditional = Chef::Resource::Conditional.only_if("false") + @conditional.stub(:shell_out).and_raise(Chef::Exceptions::CommandTimeout) + end + + it 'indicates that resource convergence should not continue' do + @conditional.continue?.should be_false + end + + it 'should log a warning' do + Chef::Log.should_receive(:warn).with("Command 'false' timed out") + @conditional.continue? + end + end + + describe "after running a block that returns a truthy value" do + before do + @conditional = Chef::Resource::Conditional.only_if { Object.new } + end + + it "indicates that resource convergence should continue" do + @conditional.continue?.should be_true + end + end + + describe "after running a block that returns a falsey value" do + before do + @conditional = Chef::Resource::Conditional.only_if { nil } + end + + it "indicates that resource convergence should not continue" do + @conditional.continue?.should be_false + end + end + end + + describe "when created as a `not_if`" do + describe "after running a successful/true command" do + before do + @conditional = Chef::Resource::Conditional.not_if("true") + end + + it "indicates that resource convergence should not continue" do + @conditional.continue?.should be_false + end + end + + describe "after running a failed/false command" do + before do + @status.send("success?=", false) + @conditional = Chef::Resource::Conditional.not_if("false") + end + + it "indicates that resource convergence should continue" do + @conditional.continue?.should be_true + end + end + + describe 'after running a command which timed out' do + before do + @conditional = Chef::Resource::Conditional.not_if("false") + @conditional.stub(:shell_out).and_raise(Chef::Exceptions::CommandTimeout) + end + + it 'indicates that resource convergence should continue' do + @conditional.continue?.should be_true + end + + it 'should log a warning' do + Chef::Log.should_receive(:warn).with("Command 'false' timed out") + @conditional.continue? + end + end + + describe "after running a block that returns a truthy value" do + before do + @conditional = Chef::Resource::Conditional.not_if { Object.new } + end + + it "indicates that resource convergence should not continue" do + @conditional.continue?.should be_false + end + end + + describe "after running a block that returns a falsey value" do + before do + @conditional = Chef::Resource::Conditional.not_if { nil } + end + + it "indicates that resource convergence should continue" do + @conditional.continue?.should be_true + end + end + end + +end diff --git a/spec/unit/resource/cookbook_file_spec.rb b/spec/unit/resource/cookbook_file_spec.rb new file mode 100644 index 0000000000..d0408c251a --- /dev/null +++ b/spec/unit/resource/cookbook_file_spec.rb @@ -0,0 +1,89 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Author:: Tyler Cloke (<tyler@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +#p License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +describe Chef::Resource::CookbookFile do + before do + @cookbook_file = Chef::Resource::CookbookFile.new('sourcecode_tarball.tgz') + end + + it "uses the name parameter for the source parameter" do + @cookbook_file.name.should == 'sourcecode_tarball.tgz' + end + + it "has a source parameter" do + @cookbook_file.name('config_file.conf') + @cookbook_file.name.should == 'config_file.conf' + end + + it "defaults to a nil cookbook parameter (current cookbook will be used)" do + @cookbook_file.cookbook.should be_nil + end + + it "has a cookbook parameter" do + @cookbook_file.cookbook("munin") + @cookbook_file.cookbook.should == 'munin' + end + + it "sets the provider to Chef::Provider::CookbookFile" do + @cookbook_file.provider.should == Chef::Provider::CookbookFile + end + + describe "when it has a backup number, group, mode, owner, source, checksum, and cookbook on nix or path, rights, deny_rights, checksum on windows" do + before do + if Chef::Platform.windows? + @cookbook_file.path("C:/temp/origin/file.txt") + @cookbook_file.rights(:read, "Everyone") + @cookbook_file.deny_rights(:full_control, "Clumsy_Sam") + else + @cookbook_file.path("/tmp/origin/file.txt") + @cookbook_file.group("wheel") + @cookbook_file.mode("0664") + @cookbook_file.owner("root") + @cookbook_file.source("/tmp/foo.txt") + @cookbook_file.cookbook("/tmp/cookbooks/cooked.rb") + end + @cookbook_file.checksum("1" * 64) + end + + + it "describes the state" do + state = @cookbook_file.state + if Chef::Platform.windows? + puts state + state[:rights].should == [{:permissions => :read, :principals => "Everyone"}] + state[:deny_rights].should == [{:permissions => :full_control, :principals => "Clumsy_Sam"}] + else + state[:group].should == "wheel" + state[:mode].should == "0664" + state[:owner].should == "root" + end + state[:checksum].should == "1" * 64 + end + + it "returns the path as its identity" do + if Chef::Platform.windows? + @cookbook_file.identity.should == "C:/temp/origin/file.txt" + else + @cookbook_file.identity.should == "/tmp/origin/file.txt" + end + end + end +end diff --git a/spec/unit/resource/cron_spec.rb b/spec/unit/resource/cron_spec.rb new file mode 100644 index 0000000000..403ffb009b --- /dev/null +++ b/spec/unit/resource/cron_spec.rb @@ -0,0 +1,181 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org) +# Author:: Tyler Cloke (<tyler@opscode.com>) +# 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::Resource::Cron do + + before(:each) do + @resource = Chef::Resource::Cron.new("cronify") + end + + it "should create a new Chef::Resource::Cron" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Cron) + end + + it "should have a name" do + @resource.name.should eql("cronify") + end + + it "should have a default action of 'create'" do + @resource.action.should eql(:create) + end + + it "should accept create or delete for action" do + lambda { @resource.action :create }.should_not raise_error(ArgumentError) + lambda { @resource.action :delete }.should_not raise_error(ArgumentError) + lambda { @resource.action :lolcat }.should raise_error(ArgumentError) + end + + it "should allow you to set a command" do + @resource.command "/bin/true" + @resource.command.should eql("/bin/true") + end + + it "should allow you to set a user" do + @resource.user "daemon" + @resource.user.should eql("daemon") + end + + it "should allow you to specify the minute" do + @resource.minute "30" + @resource.minute.should eql("30") + end + + it "should allow you to specify the hour" do + @resource.hour "6" + @resource.hour.should eql("6") + end + + it "should allow you to specify the day" do + @resource.day "10" + @resource.day.should eql("10") + end + + it "should allow you to specify the month" do + @resource.month "10" + @resource.month.should eql("10") + end + + it "should allow you to specify the weekday" do + @resource.weekday "2" + @resource.weekday.should eql("2") + end + + it "should allow you to specify the mailto variable" do + @resource.mailto "test@example.com" + @resource.mailto.should eql("test@example.com") + end + + it "should allow you to specify the path" do + @resource.path "/usr/bin:/usr/sbin" + @resource.path.should eql("/usr/bin:/usr/sbin") + end + + it "should allow you to specify the home directory" do + @resource.home "/root" + @resource.home.should eql("/root") + end + + it "should allow you to specify the shell to run the command with" do + @resource.shell "/bin/zsh" + @resource.shell.should eql("/bin/zsh") + end + + it "should allow you to specify environment variables hash" do + env = {"TEST" => "LOL"} + @resource.environment env + @resource.environment.should eql(env) + end + + it "should allow * for all time and date values" do + [ "minute", "hour", "day", "month", "weekday" ].each do |x| + @resource.send(x, "*").should eql("*") + end + end + + it "should allow ranges for all time and date values" do + [ "minute", "hour", "day", "month", "weekday" ].each do |x| + @resource.send(x, "1-2,5").should eql("1-2,5") + end + end + + it "should have a default value of * for all time and date values" do + [ "minute", "hour", "day", "month", "weekday" ].each do |x| + @resource.send(x).should eql("*") + end + end + + it "should have a default value of root for the user" do + @resource.user.should eql("root") + end + + it "should reject any minute over 59" do + lambda { @resource.minute "60" }.should raise_error(RangeError) + end + + it "should reject any hour over 23" do + lambda { @resource.hour "24" }.should raise_error(RangeError) + end + + it "should reject any day over 31" do + lambda { @resource.day "32" }.should raise_error(RangeError) + end + + it "should reject any month over 12" do + lambda { @resource.month "13" }.should raise_error(RangeError) + end + + it "should reject any weekday over 7" do + lambda { @resource.weekday "8" }.should raise_error(RangeError) + end + + it "should convert integer schedule values to a string" do + [ "minute", "hour", "day", "month", "weekday" ].each do |x| + @resource.send(x, 5).should eql("5") + end + end + + describe "when it has a time (minute, hour, day, month, weeekend) and user" do + before do + @resource.command("tackle") + @resource.minute("1") + @resource.hour("2") + @resource.day("3") + @resource.month("4") + @resource.weekday("5") + @resource.user("root") + end + + it "describes the state" do + state = @resource.state + state[:minute].should == "1" + state[:hour].should == "2" + state[:day].should == "3" + state[:month].should == "4" + state[:weekday].should == "5" + state[:user].should == "root" + end + + it "returns the command as its identity" do + @resource.identity.should == "tackle" + end + end +end diff --git a/spec/unit/resource/csh_spec.rb b/spec/unit/resource/csh_spec.rb new file mode 100644 index 0000000000..291c6ea745 --- /dev/null +++ b/spec/unit/resource/csh_spec.rb @@ -0,0 +1,40 @@ +# +# 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::Resource::Csh do + + before(:each) do + @resource = Chef::Resource::Csh.new("fakey_fakerton") + end + + it "should create a new Chef::Resource::Csh" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Csh) + end + + it "should have a resource name of :csh" do + @resource.resource_name.should eql(:csh) + end + + it "should have an interpreter of csh" do + @resource.interpreter.should eql("csh") + end + +end diff --git a/spec/unit/resource/deploy_revision_spec.rb b/spec/unit/resource/deploy_revision_spec.rb new file mode 100644 index 0000000000..671ba9a1f6 --- /dev/null +++ b/spec/unit/resource/deploy_revision_spec.rb @@ -0,0 +1,47 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2009 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' + +describe Chef::Resource::DeployRevision do + + it "defaults to the revision deploy provider" do + @resource = Chef::Resource::DeployRevision.new("deploy _this_!") + @resource.provider.should == Chef::Provider::Deploy::Revision + end + + it "has a name of deploy_revision" do + @resource = Chef::Resource::DeployRevision.new("deploy _this_!") + @resource.resource_name.should == :deploy_revision + end + +end + +describe Chef::Resource::DeployBranch do + + it "defaults to the revision deploy provider" do + @resource = Chef::Resource::DeployBranch.new("deploy _this_!") + @resource.provider.should == Chef::Provider::Deploy::Revision + end + + it "has a name of deploy_branch" do + @resource = Chef::Resource::DeployBranch.new("deploy _this_!") + @resource.resource_name.should == :deploy_branch + end + +end diff --git a/spec/unit/resource/deploy_spec.rb b/spec/unit/resource/deploy_spec.rb new file mode 100644 index 0000000000..98c9fa1581 --- /dev/null +++ b/spec/unit/resource/deploy_spec.rb @@ -0,0 +1,259 @@ +# +# 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::Resource::Deploy do + + class << self + def resource_has_a_string_attribute(attr_name) + it "has a String attribute for #{attr_name.to_s}" do + @resource.send(attr_name, "this is a string") + @resource.send(attr_name).should eql("this is a string") + lambda {@resource.send(attr_name, 8675309)}.should raise_error(ArgumentError) + end + end + + def resource_has_a_boolean_attribute(attr_name, opts={:defaults_to=>false}) + it "has a Boolean attribute for #{attr_name.to_s}" do + @resource.send(attr_name).should eql(opts[:defaults_to]) + @resource.send(attr_name, !opts[:defaults_to]) + @resource.send(attr_name).should eql( !opts[:defaults_to] ) + end + end + + def resource_has_a_callback_attribute(attr_name) + it "has a Callback attribute #{attr_name}" do + callback_block = lambda { :noop } + lambda {@resource.send(attr_name, &callback_block)}.should_not raise_error + @resource.send(attr_name).should == callback_block + callback_file = "path/to/callback.rb" + lambda {@resource.send(attr_name, callback_file)}.should_not raise_error + @resource.send(attr_name).should == callback_file + lambda {@resource.send(attr_name, :this_is_fail)}.should raise_error(ArgumentError) + end + end + end + + before do + @resource = Chef::Resource::Deploy.new("/my/deploy/dir") + end + + resource_has_a_string_attribute(:repo) + resource_has_a_string_attribute(:deploy_to) + resource_has_a_string_attribute(:role) + resource_has_a_string_attribute(:restart_command) + resource_has_a_string_attribute(:migration_command) + resource_has_a_string_attribute(:user) + resource_has_a_string_attribute(:group) + resource_has_a_string_attribute(:repository_cache) + resource_has_a_string_attribute(:copy_exclude) + resource_has_a_string_attribute(:revision) + resource_has_a_string_attribute(:remote) + resource_has_a_string_attribute(:git_ssh_wrapper) + resource_has_a_string_attribute(:svn_username) + resource_has_a_string_attribute(:svn_password) + resource_has_a_string_attribute(:svn_arguments) + resource_has_a_string_attribute(:svn_info_args) + + resource_has_a_boolean_attribute(:migrate, :defaults_to=>false) + resource_has_a_boolean_attribute(:enable_submodules, :defaults_to=>false) + resource_has_a_boolean_attribute(:shallow_clone, :defaults_to=>false) + + it "uses the first argument as the deploy directory" do + @resource.deploy_to.should eql("/my/deploy/dir") + end + + # For git, any revision, branch, tag, whatever is resolved to a SHA1 ref. + # For svn, the branch is included in the repo URL. + # Therefore, revision and branch ARE NOT SEPARATE THINGS + it "aliases #revision as #branch" do + @resource.branch "stable" + @resource.revision.should eql("stable") + end + + it "takes the SCM resource to use as a constant, and defaults to git" do + @resource.scm_provider.should eql(Chef::Provider::Git) + @resource.scm_provider Chef::Provider::Subversion + @resource.scm_provider.should eql(Chef::Provider::Subversion) + end + + it "allows scm providers to be set via symbol" do + @resource.scm_provider.should == Chef::Provider::Git + @resource.scm_provider :subversion + @resource.scm_provider.should == Chef::Provider::Subversion + end + + it "allows scm providers to be set via string" do + @resource.scm_provider.should == Chef::Provider::Git + @resource.scm_provider "subversion" + @resource.scm_provider.should == Chef::Provider::Subversion + end + + it "has a boolean attribute for svn_force_export defaulting to false" do + @resource.svn_force_export.should be_false + @resource.svn_force_export true + @resource.svn_force_export.should be_true + lambda {@resource.svn_force_export(10053)}.should raise_error(ArgumentError) + end + + it "takes arbitrary environment variables in a hash" do + @resource.environment "RAILS_ENV" => "production" + @resource.environment.should == {"RAILS_ENV" => "production"} + end + + it "takes string arguments to environment for backwards compat, setting RAILS_ENV, RACK_ENV, and MERB_ENV" do + @resource.environment "production" + @resource.environment.should == {"RAILS_ENV"=>"production", "RACK_ENV"=>"production","MERB_ENV"=>"production"} + end + + it "sets destination to $deploy_to/shared/$repository_cache" do + @resource.destination.should eql("/my/deploy/dir/shared/cached-copy") + end + + it "sets shared_path to $deploy_to/shared" do + @resource.shared_path.should eql("/my/deploy/dir/shared") + end + + it "sets current_path to $deploy_to/current" do + @resource.current_path.should eql("/my/deploy/dir/current") + end + + it "gets the current_path correct even if the shared_path is set (regression test)" do + @resource.shared_path + @resource.current_path.should eql("/my/deploy/dir/current") + end + + it "gives #depth as 5 if shallow clone is true, nil otherwise" do + @resource.depth.should be_nil + @resource.shallow_clone true + @resource.depth.should eql("5") + end + + it "aliases repo as repository" do + @resource.repository "git@github.com/opcode/cookbooks.git" + @resource.repo.should eql("git@github.com/opcode/cookbooks.git") + end + + it "aliases git_ssh_wrapper as ssh_wrapper" do + @resource.ssh_wrapper "git_my_repo.sh" + @resource.git_ssh_wrapper.should eql("git_my_repo.sh") + end + + it "has an Array attribute purge_before_symlink, default: log, tmp/pids, public/system" do + @resource.purge_before_symlink.should == %w{ log tmp/pids public/system } + @resource.purge_before_symlink %w{foo bar baz} + @resource.purge_before_symlink.should == %w{foo bar baz} + end + + it "has an Array attribute create_dirs_before_symlink, default: tmp, public, config" do + @resource.create_dirs_before_symlink.should == %w{tmp public config} + @resource.create_dirs_before_symlink %w{foo bar baz} + @resource.create_dirs_before_symlink.should == %w{foo bar baz} + end + + it 'has a Hash attribute symlinks, default: {"system" => "public/system", "pids" => "tmp/pids", "log" => "log"}' do + default = { "system" => "public/system", "pids" => "tmp/pids", "log" => "log"} + @resource.symlinks.should == default + @resource.symlinks "foo" => "bar/baz" + @resource.symlinks.should == {"foo" => "bar/baz"} + end + + it 'has a Hash attribute symlink_before_migrate, default "config/database.yml" => "config/database.yml"' do + @resource.symlink_before_migrate.should == {"config/database.yml" => "config/database.yml"} + @resource.symlink_before_migrate "wtf?" => "wtf is going on" + @resource.symlink_before_migrate.should == {"wtf?" => "wtf is going on"} + end + + resource_has_a_callback_attribute :before_migrate + resource_has_a_callback_attribute :before_symlink + resource_has_a_callback_attribute :before_restart + resource_has_a_callback_attribute :after_restart + + it "aliases restart_command as restart" do + @resource.restart "foobaz" + @resource.restart_command.should == "foobaz" + end + + it "takes a block for the restart parameter" do + restart_like_this = lambda {p :noop} + @resource.restart(&restart_like_this) + @resource.restart.should == restart_like_this + end + + it "defaults to using the Deploy::Timestamped provider" do + @resource.provider.should == Chef::Provider::Deploy::Timestamped + end + + it "allows providers to be set with a full class name" do + @resource.provider Chef::Provider::Deploy::Timestamped + @resource.provider.should == Chef::Provider::Deploy::Timestamped + end + + it "allows deploy providers to be set via symbol" do + @resource.provider :revision + @resource.provider.should == Chef::Provider::Deploy::Revision + end + + it "allows deploy providers to be set via string" do + @resource.provider "revision" + @resource.provider.should == Chef::Provider::Deploy::Revision + end + + it "defaults keep_releases to 5" do + @resource.keep_releases.should == 5 + end + + it "allows keep_releases to be set via integer" do + @resource.keep_releases 10 + @resource.keep_releases.should == 10 + end + + it "enforces a minimum keep_releases of 1" do + @resource.keep_releases 0 + @resource.keep_releases.should == 1 + end + + describe "when it has meta application root, revision, user, group, + scm provider, repository cache, environment, simlinks and migrate" do + before do + @resource.repository("http://uri.org") + @resource.deploy_to("/") + @resource.revision("1.2.3") + @resource.user("root") + @resource.group("pokemon") + @resource.scm_provider(Chef::Provider::Git) + @resource.repository_cache("cached-copy") + @resource.environment({"SUDO" => "TRUE"}) + @resource.symlinks({"system" => "public/system"}) + @resource.migrate(false) + + end + + it "describes its state" do + state = @resource.state + state[:deploy_to].should == "/" + state[:revision].should == "1.2.3" + end + + it "returns the repository URI as its identity" do + @resource.identity.should == "http://uri.org" + end + end + +end diff --git a/spec/unit/resource/directory_spec.rb b/spec/unit/resource/directory_spec.rb new file mode 100644 index 0000000000..9b0c8242e6 --- /dev/null +++ b/spec/unit/resource/directory_spec.rb @@ -0,0 +1,82 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Tyler Cloke (<tyler@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::Resource::Directory do + + before(:each) do + @resource = Chef::Resource::Directory.new("fakey_fakerton") + end + + it "should create a new Chef::Resource::Directory" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Directory) + end + + it "should have a name" do + @resource.name.should eql("fakey_fakerton") + end + + it "should have a default action of 'create'" do + @resource.action.should eql(:create) + end + + it "should accept create or delete for action" do + lambda { @resource.action :create }.should_not raise_error(ArgumentError) + lambda { @resource.action :delete }.should_not raise_error(ArgumentError) + lambda { @resource.action :blues }.should raise_error(ArgumentError) + end + + it "should use the object name as the path by default" do + @resource.path.should eql("fakey_fakerton") + end + + it "should accept a string as the path" do + lambda { @resource.path "/tmp" }.should_not raise_error(ArgumentError) + @resource.path.should eql("/tmp") + lambda { @resource.path Hash.new }.should raise_error(ArgumentError) + end + + it "should allow you to have specify whether the action is recursive with true/false" do + lambda { @resource.recursive true }.should_not raise_error(ArgumentError) + lambda { @resource.recursive false }.should_not raise_error(ArgumentError) + lambda { @resource.recursive "monkey" }.should raise_error(ArgumentError) + end + + describe "when it has group, mode, and owner" do + before do + @resource.path("/tmp/foo/bar/") + @resource.group("wheel") + @resource.mode("0664") + @resource.owner("root") + end + + it "describes its state" do + state = @resource.state + state[:group].should == "wheel" + state[:mode].should == "0664" + state[:owner].should == "root" + end + + it "returns the directory path as its identity" do + @resource.identity.should == "/tmp/foo/bar/" + end + end +end diff --git a/spec/unit/resource/dpkg_package_spec.rb b/spec/unit/resource/dpkg_package_spec.rb new file mode 100644 index 0000000000..000908712f --- /dev/null +++ b/spec/unit/resource/dpkg_package_spec.rb @@ -0,0 +1,38 @@ +# +# 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::Resource::DpkgPackage, "initialize" do + + before(:each) do + @resource = Chef::Resource::DpkgPackage.new("foo") + end + + it "should return a Chef::Resource::DpkgPackage" do + @resource.should be_a_kind_of(Chef::Resource::DpkgPackage) + end + + it "should set the resource_name to :dpkg_package" do + @resource.resource_name.should eql(:dpkg_package) + end + + it "should set the provider to Chef::Provider::Package::Dpkg" do + @resource.provider.should eql(Chef::Provider::Package::Dpkg) + end +end diff --git a/spec/unit/resource/easy_install_package_spec.rb b/spec/unit/resource/easy_install_package_spec.rb new file mode 100644 index 0000000000..9682c8177b --- /dev/null +++ b/spec/unit/resource/easy_install_package_spec.rb @@ -0,0 +1,48 @@ +# +# 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::Resource::EasyInstallPackage, "initialize" do + + before(:each) do + @resource = Chef::Resource::EasyInstallPackage.new("foo") + end + + it "should create a new Chef::Resource::EasyInstallPackage" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::EasyInstallPackage) + end + + it "should return a Chef::Resource::EasyInstallPackage" do + @resource.should be_a_kind_of(Chef::Resource::EasyInstallPackage) + end + + it "should set the resource_name to :easy_install_package" do + @resource.resource_name.should eql(:easy_install_package) + end + + it "should set the provider to Chef::Provider::Package::EasyInstall" do + @resource.provider.should eql(Chef::Provider::Package::EasyInstall) + end + + it "should allow you to set the easy_install_binary attribute" do + @resource.easy_install_binary "/opt/local/bin/easy_install" + @resource.easy_install_binary.should eql("/opt/local/bin/easy_install") + end +end diff --git a/spec/unit/resource/env_spec.rb b/spec/unit/resource/env_spec.rb new file mode 100644 index 0000000000..6862c669b2 --- /dev/null +++ b/spec/unit/resource/env_spec.rb @@ -0,0 +1,85 @@ +# +# Author:: Doug MacEachern (<dougm@vmware.com>) +# Author:: Tyler Cloke (<tyler@opscode.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::Resource::Env do + + before(:each) do + @resource = Chef::Resource::Env.new("FOO") + end + + it "should create a new Chef::Resource::Env" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Env) + end + + it "should have a name" do + @resource.name.should eql("FOO") + end + + it "should have a default action of 'create'" do + @resource.action.should eql(:create) + end + + { :create => false, :delete => false, :modify => false, :flibber => true }.each do |action,bad_value| + it "should #{bad_value ? 'not' : ''} accept #{action.to_s}" do + if bad_value + lambda { @resource.action action }.should raise_error(ArgumentError) + else + lambda { @resource.action action }.should_not raise_error(ArgumentError) + end + end + end + + it "should use the object name as the key_name by default" do + @resource.key_name.should eql("FOO") + end + + it "should accept a string as the env value via 'value'" do + lambda { @resource.value "bar" }.should_not raise_error(ArgumentError) + end + + it "should not accept a Hash for the env value via 'to'" do + lambda { @resource.value Hash.new }.should raise_error(ArgumentError) + end + + it "should allow you to set an env value via 'to'" do + @resource.value "bar" + @resource.value.should eql("bar") + end + + describe "when it has key name and value" do + before do + @resource.key_name("charmander") + @resource.value("level7") + @resource.delim("hi") + end + + it "describes its state" do + state = @resource.state + state[:value].should == "level7" + end + + it "returns the key name as its identity" do + @resource.identity.should == "charmander" + end + end + +end diff --git a/spec/unit/resource/erl_call_spec.rb b/spec/unit/resource/erl_call_spec.rb new file mode 100644 index 0000000000..ccad371723 --- /dev/null +++ b/spec/unit/resource/erl_call_spec.rb @@ -0,0 +1,81 @@ +# +# Author:: Joe Williams (<joe@joetify.com>) +# Author:: Tyler Cloke (<tyler@opscode.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::Resource::ErlCall do + + before(:each) do + @resource = Chef::Resource::ErlCall.new("fakey_fakerton") + end + + it "should create a new Chef::Resource::ErlCall" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::ErlCall) + end + + it "should have a resource name of :erl_call" do + @resource.resource_name.should eql(:erl_call) + end + + it "should have a default action of run" do + @resource.action.should eql("run") + end + + it "should accept run as an action" do + lambda { @resource.action :run }.should_not raise_error(ArgumentError) + end + + it "should allow you to set the code attribute" do + @resource.code "q()." + @resource.code.should eql("q().") + end + + it "should allow you to set the cookie attribute" do + @resource.cookie "nomnomnom" + @resource.cookie.should eql("nomnomnom") + end + + it "should allow you to set the distributed attribute" do + @resource.distributed true + @resource.distributed.should eql(true) + end + + it "should allow you to set the name_type attribute" do + @resource.name_type "sname" + @resource.name_type.should eql("sname") + end + + it "should allow you to set the node_name attribute" do + @resource.node_name "chef@erlang" + @resource.node_name.should eql("chef@erlang") + end + + describe "when it has cookie and node_name" do + before do + @resource.code("erl-call:function()") + @resource.cookie("cookie") + @resource.node_name("raster") + end + + it "returns the code as its identity" do + @resource.identity.should == "erl-call:function()" + end + end +end diff --git a/spec/unit/resource/execute_spec.rb b/spec/unit/resource/execute_spec.rb new file mode 100644 index 0000000000..0dcdab7409 --- /dev/null +++ b/spec/unit/resource/execute_spec.rb @@ -0,0 +1,124 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Tyler Cloke (<tyler@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::Resource::Execute do + + before(:each) do + @resource = Chef::Resource::Execute.new("some command") + end + + it "should create a new Chef::Resource::Execute" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Execute) + end + + it "should set the command to the first argument to new" do + @resource.command.should eql("some command") + end + + it "should accept an array on instantiation, too" do + resource = Chef::Resource::Execute.new(%w{something else}) + resource.should be_a_kind_of(Chef::Resource) + resource.should be_a_kind_of(Chef::Resource::Execute) + resource.command.should eql(%w{something else}) + end + + it "should accept a string for the command to run" do + @resource.command "something" + @resource.command.should eql("something") + end + + it "should accept an array for the command to run" do + @resource.command %w{something else} + @resource.command.should eql(%w{something else}) + end + + it "should accept a string for the cwd" do + @resource.cwd "something" + @resource.cwd.should eql("something") + end + + it "should accept a hash for the environment" do + test_hash = { :one => :two } + @resource.environment(test_hash) + @resource.environment.should eql(test_hash) + end + + it "allows the environment to be specified with #env" do + @resource.should respond_to(:env) + end + + it "should accept a string for the group" do + @resource.group "something" + @resource.group.should eql("something") + end + + it "should accept an integer for the group" do + @resource.group 1 + @resource.group.should eql(1) + end + + it "should accept an array for the execution path" do + @resource.path ["woot"] + @resource.path.should eql(["woot"]) + end + + it "should accept an integer for the return code" do + @resource.returns 1 + @resource.returns.should eql(1) + end + + it "should accept an integer for the timeout" do + @resource.timeout 1 + @resource.timeout.should eql(1) + end + + it "should accept a string for the user" do + @resource.user "something" + @resource.user.should eql("something") + end + + it "should accept an integer for the user" do + @resource.user 1 + @resource.user.should eql(1) + end + + it "should accept a string for creates" do + @resource.creates "something" + @resource.creates.should eql("something") + end + + describe "when it has cwd, environment, group, path, return value, and a user" do + before do + @resource.command("grep") + @resource.cwd("/tmp/") + @resource.environment({ :one => :two }) + @resource.group("legos") + @resource.path(["/var/local/"]) + @resource.returns(1) + @resource.user("root") + end + + it "returns the command as its identity" do + @resource.identity.should == "grep" + end + end +end diff --git a/spec/unit/resource/file_spec.rb b/spec/unit/resource/file_spec.rb new file mode 100644 index 0000000000..58a7bd68b3 --- /dev/null +++ b/spec/unit/resource/file_spec.rb @@ -0,0 +1,121 @@ +# +# 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::Resource::File do + + before(:each) do + @resource = Chef::Resource::File.new("fakey_fakerton") + end + + it "should have a name" do + @resource.name.should eql("fakey_fakerton") + end + + it "should have a default action of 'create'" do + @resource.action.should eql("create") + end + + it "should have a default content of nil" do + @resource.content.should be_nil + end + + it "should be set to back up 5 files by default" do + @resource.backup.should eql(5) + end + + it "should only accept strings for content" do + lambda { @resource.content 5 }.should raise_error(ArgumentError) + lambda { @resource.content :foo }.should raise_error(ArgumentError) + lambda { @resource.content "hello" => "there" }.should raise_error(ArgumentError) + lambda { @resource.content "hi" }.should_not raise_error(ArgumentError) + end + + it "should only accept false or a number for backup" do + lambda { @resource.backup true }.should raise_error(ArgumentError) + lambda { @resource.backup false }.should_not raise_error(ArgumentError) + lambda { @resource.backup 10 }.should_not raise_error(ArgumentError) + lambda { @resource.backup "blues" }.should raise_error(ArgumentError) + end + + it "should accept a sha256 for checksum" do + lambda { @resource.checksum "0fd012fdc96e96f8f7cf2046522a54aed0ce470224513e45da6bc1a17a4924aa" }.should_not raise_error(ArgumentError) + lambda { @resource.checksum "monkey!" }.should raise_error(ArgumentError) + end + + it "should accept create, delete or touch for action" do + lambda { @resource.action :create }.should_not raise_error(ArgumentError) + lambda { @resource.action :delete }.should_not raise_error(ArgumentError) + lambda { @resource.action :touch }.should_not raise_error(ArgumentError) + lambda { @resource.action :blues }.should raise_error(ArgumentError) + end + + it "should use the object name as the path by default" do + @resource.path.should eql("fakey_fakerton") + end + + it "should accept a string as the path" do + lambda { @resource.path "/tmp" }.should_not raise_error(ArgumentError) + @resource.path.should eql("/tmp") + lambda { @resource.path Hash.new }.should raise_error(ArgumentError) + end + + describe "when it has a path, owner, group, mode, and checksum" do + before do + @resource.path("/tmp/foo.txt") + @resource.owner("root") + @resource.group("wheel") + @resource.mode("0644") + @resource.checksum("1" * 64) + end + + context "on unix", :unix_only do + it "describes its state" do + state = @resource.state + state[:owner].should == "root" + state[:group].should == "wheel" + state[:mode].should == "0644" + state[:checksum].should == "1" * 64 + end + end + + context "on windows", :windows_only do + # according to Chef::Resource::File, windows state attributes are rights + deny_rights + pending "it describes its state" + end + + it "returns the file path as its identity" do + @resource.identity.should == "/tmp/foo.txt" + end + + end + + describe "when access controls are set on windows", :windows_only => true do + before do + @resource.rights :read, "Everyone" + @resource.rights :full_control, "DOMAIN\User" + end + it "describes its state including windows ACL attributes" do + state = @resource.state + state[:rights].should == [ {:permissions => :read, :principals => "Everyone"}, + {:permissions => :full_control, :principals => "DOMAIN\User"} ] + end + end + +end diff --git a/spec/unit/resource/freebsd_package_spec.rb b/spec/unit/resource/freebsd_package_spec.rb new file mode 100644 index 0000000000..697f5fff06 --- /dev/null +++ b/spec/unit/resource/freebsd_package_spec.rb @@ -0,0 +1,39 @@ +# +# 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::Resource::FreebsdPackage, "initialize" do + + before(:each) do + @resource = Chef::Resource::FreebsdPackage.new("foo") + end + + it "should return a Chef::Resource::FreebsdPackage" do + @resource.should be_a_kind_of(Chef::Resource::FreebsdPackage) + end + + it "should set the resource_name to :freebsd_package" do + @resource.resource_name.should eql(:freebsd_package) + end + + it "should set the provider to Chef::Provider::Package::freebsd" do + @resource.provider.should eql(Chef::Provider::Package::Freebsd) + end +end + diff --git a/spec/unit/resource/gem_package_spec.rb b/spec/unit/resource/gem_package_spec.rb new file mode 100644 index 0000000000..2404afa03a --- /dev/null +++ b/spec/unit/resource/gem_package_spec.rb @@ -0,0 +1,49 @@ +# +# 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::Resource::GemPackage, "initialize" do + + before(:each) do + @resource = Chef::Resource::GemPackage.new("foo") + end + + it "should return a Chef::Resource::GemPackage" do + @resource.should be_a_kind_of(Chef::Resource::GemPackage) + end + + it "should set the resource_name to :gem_package" do + @resource.resource_name.should eql(:gem_package) + end + + it "should set the provider to Chef::Provider::Package::Rubygems" do + @resource.provider.should eql(Chef::Provider::Package::Rubygems) + end +end + +describe Chef::Resource::GemPackage, "gem_binary" do + before(:each) do + @resource = Chef::Resource::GemPackage.new("foo") + end + + it "should set the gem_binary variable to whatever is passed in" do + @resource.gem_binary("/opt/local/bin/gem") + @resource.gem_binary.should eql("/opt/local/bin/gem") + end +end diff --git a/spec/unit/resource/git_spec.rb b/spec/unit/resource/git_spec.rb new file mode 100644 index 0000000000..69b40ffd42 --- /dev/null +++ b/spec/unit/resource/git_spec.rb @@ -0,0 +1,46 @@ +# +# 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::Resource::Git do + + before(:each) do + @git = Chef::Resource::Git.new("my awesome webapp") + end + + it "is a kind of Scm Resource" do + @git.should be_a_kind_of(Chef::Resource::Scm) + @git.should be_an_instance_of(Chef::Resource::Git) + end + + it "uses the git provider" do + @git.provider.should eql(Chef::Provider::Git) + end + + it "uses aliases revision as branch" do + @git.branch "HEAD" + @git.revision.should eql("HEAD") + end + + it "aliases revision as reference" do + @git.reference "v1.0 tag" + @git.revision.should eql("v1.0 tag") + end + +end diff --git a/spec/unit/resource/group_spec.rb b/spec/unit/resource/group_spec.rb new file mode 100644 index 0000000000..7181750b1c --- /dev/null +++ b/spec/unit/resource/group_spec.rb @@ -0,0 +1,148 @@ +# +# Author:: AJ Christensen (<aj@opscode.com>) +# Author:: Tyler Cloke (<tyler@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::Resource::Group, "initialize" do + before(:each) do + @resource = Chef::Resource::Group.new("admin") + end + + it "should create a new Chef::Resource::Group" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Group) + end + + it "should set the resource_name to :group" do + @resource.resource_name.should eql(:group) + end + + it "should set the group_name equal to the argument to initialize" do + @resource.group_name.should eql("admin") + end + + it "should default gid to nil" do + @resource.gid.should eql(nil) + end + + it "should default members to an empty array" do + @resource.members.should eql([]) + end + + it "should alias users to members, also an empty array" do + @resource.users.should eql([]) + end + + it "should set action to :create" do + @resource.action.should eql(:create) + end + + %w{create remove modify manage}.each do |action| + it "should allow action #{action}" do + @resource.allowed_actions.detect { |a| a == action.to_sym }.should eql(action.to_sym) + end + end +end + +describe Chef::Resource::Group, "group_name" do + before(:each) do + @resource = Chef::Resource::Group.new("admin") + end + + it "should allow a string" do + @resource.group_name "pirates" + @resource.group_name.should eql("pirates") + end + + it "should not allow a hash" do + lambda { @resource.send(:group_name, { :aj => "is freakin awesome" }) }.should raise_error(ArgumentError) + end +end + +describe Chef::Resource::Group, "gid" do + before(:each) do + @resource = Chef::Resource::Group.new("admin") + end + + it "should allow an integer" do + @resource.gid 100 + @resource.gid.should eql(100) + end + + it "should not allow a hash" do + lambda { @resource.send(:gid, { :aj => "is freakin awesome" }) }.should raise_error(ArgumentError) + end +end + +describe Chef::Resource::Group, "members" do + before(:each) do + @resource = Chef::Resource::Group.new("admin") + end + + [ :users, :members].each do |method| + it "(#{method}) should allow and convert a string" do + @resource.send(method, "aj") + @resource.send(method).should eql(["aj"]) + end + + it "(#{method}) should allow an array" do + @resource.send(method, [ "aj", "adam" ]) + @resource.send(method).should eql( ["aj", "adam"] ) + end + + it "(#{method}) should not allow a hash" do + lambda { @resource.send(method, { :aj => "is freakin awesome" }) }.should raise_error(ArgumentError) + end + end +end + +describe Chef::Resource::Group, "append" do + before(:each) do + @resource = Chef::Resource::Group.new("admin") + end + + it "should default to false" do + @resource.append.should eql(false) + end + + it "should allow a boolean" do + @resource.append true + @resource.append.should eql(true) + end + + it "should not allow a hash" do + lambda { @resource.send(:gid, { :aj => "is freakin awesome" }) }.should raise_error(ArgumentError) + end + + describe "when it has members" do + before do + @resource.group_name("pokemon") + @resource.members(["blastoise", "pikachu"]) + end + + it "describes its state" do + state = @resource.state + state[:members].should eql(["blastoise", "pikachu"]) + end + + it "returns the group name as its identity" do + @resource.identity.should == "pokemon" + end + end +end diff --git a/spec/unit/resource/http_request_spec.rb b/spec/unit/resource/http_request_spec.rb new file mode 100644 index 0000000000..924cf66fc2 --- /dev/null +++ b/spec/unit/resource/http_request_spec.rb @@ -0,0 +1,59 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Tyler Cloke (<tyler@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::Resource::HttpRequest do + + before(:each) do + @resource = Chef::Resource::HttpRequest.new("fakey_fakerton") + end + + it "should create a new Chef::Resource::HttpRequest" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::HttpRequest) + end + + it "should set url to a string" do + @resource.url "http://slashdot.org" + @resource.url.should eql("http://slashdot.org") + end + + it "should set the message to the name by default" do + @resource.message.should eql("fakey_fakerton") + end + + it "should set message to a string" do + @resource.message "monkeybars" + @resource.message.should eql("monkeybars") + end + + describe "when it has a message and headers" do + before do + @resource.url("http://www.trololol.net") + @resource.message("Get sum post brah.") + @resource.headers({"head" => "tail"}) + end + + it "returns the url as its identity" do + @resource.identity.should == "http://www.trololol.net" + end + end + +end diff --git a/spec/unit/resource/ifconfig_spec.rb b/spec/unit/resource/ifconfig_spec.rb new file mode 100644 index 0000000000..2aac130978 --- /dev/null +++ b/spec/unit/resource/ifconfig_spec.rb @@ -0,0 +1,46 @@ +# +# Author:: Tyler Cloke (<tyler@opscode.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::Resource::Ifconfig do + + before(:each) do + @resource = Chef::Resource::Ifconfig.new("fakey_fakerton") + end + + describe "when it has target, hardware address, inet address, and a mask" do + before do + @resource.device("charmander") + @resource.target("team_rocket") + @resource.hwaddr("11.2223.223") + @resource.inet_addr("434.2343.23") + @resource.mask("255.255.545") + end + + it "describes its state" do + state = @resource.state + state[:inet_addr].should == "434.2343.23" + state[:mask].should == "255.255.545" + end + + it "returns the device as its identity" do + @resource.identity.should == "charmander" + end + end +end diff --git a/spec/unit/resource/ips_package_spec.rb b/spec/unit/resource/ips_package_spec.rb new file mode 100644 index 0000000000..92e62606de --- /dev/null +++ b/spec/unit/resource/ips_package_spec.rb @@ -0,0 +1,43 @@ +# +# 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' + +describe Chef::Resource::IpsPackage, "initialize" do + + before(:each) do + @resource = Chef::Resource::IpsPackage.new("crypto/gnupg") + end + + it "should return a Chef::Resource::IpsPackage" do + @resource.should be_a_kind_of(Chef::Resource::IpsPackage) + end + + it "should set the resource_name to :ips_package" do + @resource.resource_name.should eql(:ips_package) + end + + it "should set the provider to Chef::Provider::Package::Ips" do + @resource.provider.should eql(Chef::Provider::Package::Ips) + end + + it "should support accept_license" do + @resource.accept_license(true) + @resource.accept_license.should eql(true) + end +end diff --git a/spec/unit/resource/link_spec.rb b/spec/unit/resource/link_spec.rb new file mode 100644 index 0000000000..279a5b4e77 --- /dev/null +++ b/spec/unit/resource/link_spec.rb @@ -0,0 +1,118 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Tyler Cloke (<tyler@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::Resource::Link do + + before(:each) do + @resource = Chef::Resource::Link.new("fakey_fakerton") + end + + it "should create a new Chef::Resource::Link" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Link) + end + + it "should have a name" do + @resource.name.should eql("fakey_fakerton") + end + + it "should have a default action of 'create'" do + @resource.action.should eql(:create) + end + + { :create => false, :delete => false, :blues => true }.each do |action,bad_value| + it "should #{bad_value ? 'not' : ''} accept #{action.to_s}" do + if bad_value + lambda { @resource.action action }.should raise_error(ArgumentError) + else + lambda { @resource.action action }.should_not raise_error(ArgumentError) + end + end + end + + it "should use the object name as the target_file by default" do + @resource.target_file.should eql("fakey_fakerton") + end + + it "should accept a string as the link source via 'to'" do + lambda { @resource.to "/tmp" }.should_not raise_error(ArgumentError) + end + + it "should not accept a Hash for the link source via 'to'" do + lambda { @resource.to Hash.new }.should raise_error(ArgumentError) + end + + it "should allow you to set a link source via 'to'" do + @resource.to "/tmp/foo" + @resource.to.should eql("/tmp/foo") + end + + it "should allow you to specify the link type" do + @resource.link_type "symbolic" + @resource.link_type.should eql(:symbolic) + end + + it "should default to a symbolic link" do + @resource.link_type.should eql(:symbolic) + end + + it "should accept a hard link_type" do + @resource.link_type :hard + @resource.link_type.should eql(:hard) + end + + it "should reject any other link_type but :hard and :symbolic" do + lambda { @resource.link_type "x-men" }.should raise_error(ArgumentError) + end + + it "should accept a group name or id for group" do + lambda { @resource.group "root" }.should_not raise_error(ArgumentError) + lambda { @resource.group 123 }.should_not raise_error(ArgumentError) + lambda { @resource.group "root*goo" }.should raise_error(ArgumentError) + end + + it "should accept a user name or id for owner" do + lambda { @resource.owner "root" }.should_not raise_error(ArgumentError) + lambda { @resource.owner 123 }.should_not raise_error(ArgumentError) + lambda { @resource.owner "root*goo" }.should raise_error(ArgumentError) + end + + describe "when it has to, link_type, owner, and group" do + before do + @resource.target_file("/var/target.tar") + @resource.to("/to/dir/file.tar") + @resource.link_type(:symbolic) + @resource.owner("root") + @resource.group("0664") + end + + it "describes its state" do + state = @resource.state + state[:to].should == "/to/dir/file.tar" + state[:owner].should == "root" + state[:group].should == "0664" + end + + it "returns the target file as its identity" do + @resource.identity.should == "/var/target.tar" + end + end +end diff --git a/spec/unit/resource/log_spec.rb b/spec/unit/resource/log_spec.rb new file mode 100644 index 0000000000..bc5ac13078 --- /dev/null +++ b/spec/unit/resource/log_spec.rb @@ -0,0 +1,61 @@ +# +# Author:: Cary Penniman (<cary@rightscale.com>) +# Author:: Tyler Cloke (<tyler@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::Resource::Log do + + before(:each) do + @log_str = "this is my string to log" + @resource = Chef::Resource::Log.new(@log_str) + end + + it "should create a new Chef::Resource::Log" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Log) + end + + it "should have a name of log" do + @resource.resource_name.should == :log + end + + it "should allow you to set a log string" do + @resource.name.should == @log_str + end + + it "should accept a vaild level option" do + @resource.level :debug + @resource.level :info + @resource.level :warn + @resource.level :error + @resource.level :fatal + lambda { @resource.level :unsupported }.should raise_error(ArgumentError) + end + + describe "when the identity is defined" do + before do + @resource = Chef::Resource::Log.new("ery day I'm loggin-in") + end + + it "returns the log string as its identity" do + @resource.identity.should == "ery day I'm loggin-in" + end + end +end + diff --git a/spec/unit/resource/macports_package_spec.rb b/spec/unit/resource/macports_package_spec.rb new file mode 100644 index 0000000000..7420fafeb5 --- /dev/null +++ b/spec/unit/resource/macports_package_spec.rb @@ -0,0 +1,37 @@ +# +# 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::Resource::MacportsPackage, "initialize" do + before(:each) do + @resource = Chef::Resource::MacportsPackage.new("foo") + end + + it "should return a Chef::Resource::MacportsPackage" do + @resource.should be_a_kind_of(Chef::Resource::MacportsPackage) + end + + it "should set the resource_name to :macports_package" do + @resource.resource_name.should eql(:macports_package) + end + + it "should set the provider to Chef::Provider::Package::Macports" do + @resource.provider.should eql(Chef::Provider::Package::Macports) + end +end diff --git a/spec/unit/resource/mdadm_spec.rb b/spec/unit/resource/mdadm_spec.rb new file mode 100644 index 0000000000..c4e6704ceb --- /dev/null +++ b/spec/unit/resource/mdadm_spec.rb @@ -0,0 +1,102 @@ +# +# Author:: Joe Williams (<joe@joetify.com>) +# Author:: Tyler Cloke (<tyler@opscode.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::Resource::Mdadm do + + before(:each) do + @resource = Chef::Resource::Mdadm.new("fakey_fakerton") + end + + it "should create a new Chef::Resource::Mdadm" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Mdadm) + end + + it "should have a resource name of :mdadm" do + @resource.resource_name.should eql(:mdadm) + end + + it "should have a default action of create" do + @resource.action.should eql(:create) + end + + it "should accept create, assemble, stop as actions" do + lambda { @resource.action :create }.should_not raise_error(ArgumentError) + lambda { @resource.action :assemble }.should_not raise_error(ArgumentError) + lambda { @resource.action :stop }.should_not raise_error(ArgumentError) + end + + it "should allow you to set the raid_device attribute" do + @resource.raid_device "/dev/md3" + @resource.raid_device.should eql("/dev/md3") + end + + it "should allow you to set the chunk attribute" do + @resource.chunk 256 + @resource.chunk.should eql(256) + end + + it "should allow you to set the level attribute" do + @resource.level 1 + @resource.level.should eql(1) + end + + it "should allow you to set the metadata attribute" do + @resource.metadata "1.2" + @resource.metadata.should eql("1.2") + end + + it "should allow you to set the bitmap attribute" do + @resource.metadata "internal" + @resource.metadata.should eql("internal") + end + + it "should allow you to set the devices attribute" do + @resource.devices ["/dev/sda", "/dev/sdb"] + @resource.devices.should eql(["/dev/sda", "/dev/sdb"]) + end + + it "should allow you to set the exists attribute" do + @resource.exists true + @resource.exists.should eql(true) + end + + describe "when it has devices, level, and chunk" do + before do + @resource.raid_device("raider") + @resource.devices(["device1", "device2"]) + @resource.level(1) + @resource.chunk(42) + end + + it "describes its state" do + state = @resource.state + state[:devices].should eql(["device1", "device2"]) + state[:level].should == 1 + state[:chunk].should == 42 + end + + it "returns the raid device as its identity" do + @resource.identity.should == "raider" + end + end + +end diff --git a/spec/unit/resource/mount_spec.rb b/spec/unit/resource/mount_spec.rb new file mode 100644 index 0000000000..498f33a7ba --- /dev/null +++ b/spec/unit/resource/mount_spec.rb @@ -0,0 +1,158 @@ +# +# Author:: Joshua Timberman (<joshua@opscode.com>) +# Author:: Tyler Cloke (<tyler@opscode.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::Resource::Mount do + before(:each) do + @resource = Chef::Resource::Mount.new("filesystem") + end + + it "should create a new Chef::Resource::Mount" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Mount) + end + + it "should have a name" do + @resource.name.should eql("filesystem") + end + + it "should set mount_point to the name" do + @resource.mount_point.should eql("filesystem") + end + + it "should have a default action of mount" do + @resource.action.should eql(:mount) + end + + it "should accept mount, umount and remount as actions" do + lambda { @resource.action :mount }.should_not raise_error(ArgumentError) + lambda { @resource.action :umount }.should_not raise_error(ArgumentError) + lambda { @resource.action :remount }.should_not raise_error(ArgumentError) + lambda { @resource.action :brooklyn }.should raise_error(ArgumentError) + end + + it "should allow you to set the device attribute" do + @resource.device "/dev/sdb3" + @resource.device.should eql("/dev/sdb3") + end + + it "should allow you to set the fstype attribute" do + @resource.fstype "nfs" + @resource.fstype.should eql("nfs") + end + + it "should allow you to set the dump attribute" do + @resource.dump 1 + @resource.dump.should eql(1) + end + + it "should allow you to set the pass attribute" do + @resource.pass 1 + @resource.pass.should eql(1) + end + + it "should set the options attribute to defaults" do + @resource.options.should eql(["defaults"]) + end + + it "should allow options to be sent as a string, and convert to array" do + @resource.options "rw,noexec" + @resource.options.should be_a_kind_of(Array) + end + + it "should allow options attribute as an array" do + @resource.options ["ro", "nosuid"] + @resource.options.should be_a_kind_of(Array) + end + + it "should accept true for mounted" do + @resource.mounted(true) + @resource.mounted.should eql(true) + end + + it "should accept false for mounted" do + @resource.mounted(false) + @resource.mounted.should eql(false) + end + + it "should set mounted to false by default" do + @resource.mounted.should eql(false) + end + + it "should not accept a string for mounted" do + lambda { @resource.mounted("poop") }.should raise_error(ArgumentError) + end + + it "should accept true for enabled" do + @resource.enabled(true) + @resource.enabled.should eql(true) + end + + it "should accept false for enabled" do + @resource.enabled(false) + @resource.enabled.should eql(false) + end + + it "should set enabled to false by default" do + @resource.enabled.should eql(false) + end + + it "should not accept a string for enabled" do + lambda { @resource.enabled("poop") }.should raise_error(ArgumentError) + end + + it "should default all feature support to false" do + support_hash = { :remount => false } + @resource.supports.should == support_hash + end + + it "should allow you to set feature support as an array" do + support_array = [ :remount ] + support_hash = { :remount => true } + @resource.supports(support_array) + @resource.supports.should == support_hash + end + + it "should allow you to set feature support as a hash" do + support_hash = { :remount => true } + @resource.supports(support_hash) + @resource.supports.should == support_hash + end + + describe "when it has mount point, device type, and fstype" do + before do + @resource.device("charmander") + @resource.mount_point("123.456") + @resource.device_type(:device) + @resource.fstype("ranked") + end + + it "describes its state" do + state = @resource.state + state[:mount_point].should == "123.456" + state[:device_type].should eql(:device) + state[:fstype].should == "ranked" + end + + it "returns the device as its identity" do + @resource.identity.should == "charmander" + end + end +end diff --git a/spec/unit/resource/ohai_spec.rb b/spec/unit/resource/ohai_spec.rb new file mode 100644 index 0000000000..82df61e509 --- /dev/null +++ b/spec/unit/resource/ohai_spec.rb @@ -0,0 +1,62 @@ +# +# 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' + +describe Chef::Resource::Ohai do + + before(:each) do + @resource = Chef::Resource::Ohai.new("ohai_reload") + end + + it "should create a new Chef::Resource::Ohai" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Ohai) + end + + it "should have a resource name of :ohai" do + @resource.resource_name.should eql(:ohai) + end + + it "should have a default action of create" do + @resource.action.should eql(:reload) + end + + it "should allow you to set the plugin attribute" do + @resource.plugin "passwd" + @resource.plugin.should eql("passwd") + end + + describe "when it has a plugin value" do + before do + @resource.name("test") + @resource.plugin("passwd") + end + + it "describes its state" do + state = @resource.state + state[:plugin].should == "passwd" + end + + it "returns the name as its identity" do + @resource.identity.should == "test" + end + end + + +end diff --git a/spec/unit/resource/package_spec.rb b/spec/unit/resource/package_spec.rb new file mode 100644 index 0000000000..3f9cc7a408 --- /dev/null +++ b/spec/unit/resource/package_spec.rb @@ -0,0 +1,80 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Tyler Cloke (<tyler@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::Resource::Package do + + before(:each) do + @resource = Chef::Resource::Package.new("emacs") + end + + it "should create a new Chef::Resource::Package" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Package) + end + + it "should set the package_name to the first argument to new" do + @resource.package_name.should eql("emacs") + end + + it "should accept a string for the package name" do + @resource.package_name "something" + @resource.package_name.should eql("something") + end + + it "should accept a string for the version" do + @resource.version "something" + @resource.version.should eql("something") + end + + it "should accept a string for the response file" do + @resource.response_file "something" + @resource.response_file.should eql("something") + end + + it "should accept a string for the source" do + @resource.source "something" + @resource.source.should eql("something") + end + + it "should accept a string for the options" do + @resource.options "something" + @resource.options.should eql("something") + end + + describe "when it has a package_name and version" do + before do + @resource.package_name("tomcat") + @resource.version("10.9.8") + @resource.options("-al") + end + + it "describes its state" do + state = @resource.state + state[:version].should == "10.9.8" + state[:options].should == "-al" + end + + it "returns the file path as its identity" do + @resource.identity.should == "tomcat" + end + + end +end diff --git a/spec/unit/resource/pacman_package_spec.rb b/spec/unit/resource/pacman_package_spec.rb new file mode 100644 index 0000000000..53ecd296c9 --- /dev/null +++ b/spec/unit/resource/pacman_package_spec.rb @@ -0,0 +1,38 @@ +# +# 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::Resource::PacmanPackage, "initialize" do + + before(:each) do + @resource = Chef::Resource::PacmanPackage.new("foo") + end + + it "should return a Chef::Resource::PacmanPackage" do + @resource.should be_a_kind_of(Chef::Resource::PacmanPackage) + end + + it "should set the resource_name to :pacman_package" do + @resource.resource_name.should eql(:pacman_package) + end + + it "should set the provider to Chef::Provider::Package::Pacman" do + @resource.provider.should eql(Chef::Provider::Package::Pacman) + end +end diff --git a/spec/unit/resource/perl_spec.rb b/spec/unit/resource/perl_spec.rb new file mode 100644 index 0000000000..f0313e6fb9 --- /dev/null +++ b/spec/unit/resource/perl_spec.rb @@ -0,0 +1,40 @@ +# +# 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::Resource::Perl do + + before(:each) do + @resource = Chef::Resource::Perl.new("fakey_fakerton") + end + + it "should create a new Chef::Resource::Perl" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Perl) + end + + it "should have a resource name of :perl" do + @resource.resource_name.should eql(:perl) + end + + it "should have an interpreter of perl" do + @resource.interpreter.should eql("perl") + end + +end diff --git a/spec/unit/resource/portage_package_spec.rb b/spec/unit/resource/portage_package_spec.rb new file mode 100644 index 0000000000..da086d95ba --- /dev/null +++ b/spec/unit/resource/portage_package_spec.rb @@ -0,0 +1,38 @@ +# +# 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 File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "spec_helper")) + +describe Chef::Resource::PortagePackage, "initialize" do + + before(:each) do + @resource = Chef::Resource::PortagePackage.new("foo") + end + + it "should return a Chef::Resource::PortagePackage" do + @resource.should be_a_kind_of(Chef::Resource::PortagePackage) + end + + it "should set the resource_name to :portage_package" do + @resource.resource_name.should eql(:portage_package) + end + + it "should set the provider to Chef::Provider::Package::Portage" do + @resource.provider.should eql(Chef::Provider::Package::Portage) + end +end diff --git a/spec/unit/resource/python_spec.rb b/spec/unit/resource/python_spec.rb new file mode 100644 index 0000000000..ff9547db9a --- /dev/null +++ b/spec/unit/resource/python_spec.rb @@ -0,0 +1,40 @@ +# +# 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::Resource::Python do + + before(:each) do + @resource = Chef::Resource::Python.new("fakey_fakerton") + end + + it "should create a new Chef::Resource::Python" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Python) + end + + it "should have a resource name of :python" do + @resource.resource_name.should eql(:python) + end + + it "should have an interpreter of python" do + @resource.interpreter.should eql("python") + end + +end diff --git a/spec/unit/resource/remote_directory_spec.rb b/spec/unit/resource/remote_directory_spec.rb new file mode 100644 index 0000000000..e4fa8fc4e3 --- /dev/null +++ b/spec/unit/resource/remote_directory_spec.rb @@ -0,0 +1,97 @@ +# +# 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::Resource::RemoteDirectory do + + before(:each) do + @resource = Chef::Resource::RemoteDirectory.new("/etc/dunk") + end + + it "should create a new Chef::Resource::RemoteDirectory" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::RemoteDirectory) + end + + it "should set the path to the first argument to new" do + @resource.path.should eql("/etc/dunk") + end + + it "should accept a string for the remote directory source" do + @resource.source "foo" + @resource.source.should eql("foo") + end + + it "should have the basename of the remote directory resource as the default source" do + @resource.source.should eql("dunk") + end + + it "should accept a number for the remote files backup" do + @resource.files_backup 1 + @resource.files_backup.should eql(1) + end + + it "should accept false for the remote files backup" do + @resource.files_backup false + @resource.files_backup.should eql(false) + end + + it "should accept 3 or 4 digets for the files_mode" do + @resource.files_mode 100 + @resource.files_mode.should eql(100) + @resource.files_mode 1000 + @resource.files_mode.should eql(1000) + end + + it "should accept a string or number for the files group" do + @resource.files_group "heart" + @resource.files_group.should eql("heart") + @resource.files_group 1000 + @resource.files_group.should eql(1000) + end + + it "should accept a string or number for the files owner" do + @resource.files_owner "heart" + @resource.files_owner.should eql("heart") + @resource.files_owner 1000 + @resource.files_owner.should eql(1000) + end + + describe "when it has cookbook, files owner, files mode, and source" do + before do + @resource.path("/var/path/") + @resource.cookbook("pokemon.rb") + @resource.files_owner("root") + @resource.files_group("supergroup") + @resource.files_mode("0664") + @resource.source("/var/source/") + end + + it "describes its state" do + state = @resource.state + state[:files_owner].should == "root" + state[:files_group].should == "supergroup" + state[:files_mode].should == "0664" + end + + it "returns the path as its identity" do + @resource.identity.should == "/var/path/" + end + end +end diff --git a/spec/unit/resource/remote_file_spec.rb b/spec/unit/resource/remote_file_spec.rb new file mode 100644 index 0000000000..d91f80d1a7 --- /dev/null +++ b/spec/unit/resource/remote_file_spec.rb @@ -0,0 +1,123 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Tyler Cloke (<tyler@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::Resource::RemoteFile do + + before(:each) do + @resource = Chef::Resource::RemoteFile.new("fakey_fakerton") + end + + describe "initialize" do + it "should create a new Chef::Resource::RemoteFile" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::File) + @resource.should be_a_kind_of(Chef::Resource::RemoteFile) + end + end + + it "says its provider is RemoteFile when the source is an absolute URI" do + @resource.source("http://www.google.com/robots.txt") + @resource.provider.should == Chef::Provider::RemoteFile + Chef::Platform.find_provider(:noplatform, 'noversion', @resource).should == Chef::Provider::RemoteFile + end + + + describe "source" do + it "does not have a default value for 'source'" do + @resource.source.should be_nil + end + + it "should accept a URI for the remote file source" do + @resource.source "http://opscode.com/" + @resource.source.should eql([ "http://opscode.com/" ]) + end + + it "should accept an array of URIs for the remote file source" do + @resource.source([ "http://opscode.com/", "http://puppetlabs.com/" ]) + @resource.source.should eql([ "http://opscode.com/", "http://puppetlabs.com/" ]) + end + + it "should accept an multiple URIs as arguments for the remote file source" do + @resource.source("http://opscode.com/", "http://puppetlabs.com/") + @resource.source.should eql([ "http://opscode.com/", "http://puppetlabs.com/" ]) + end + + it "does not accept a non-URI as the source" do + lambda { @resource.source("not-a-uri") }.should raise_error(Chef::Exceptions::InvalidRemoteFileURI) + end + + it "should raise and exception when source is an empty array" do + lambda { @resource.source([]) }.should raise_error(ArgumentError) + end + + end + + describe "checksum" do + it "should accept a string for the checksum object" do + @resource.checksum "asdf" + @resource.checksum.should eql("asdf") + end + + it "should default to nil" do + @resource.checksum.should == nil + end + end + + describe "when it has group, mode, owner, source, and checksum" do + before do + if Chef::Platform.windows? + @resource.path("C:/temp/origin/file.txt") + @resource.rights(:read, "Everyone") + @resource.deny_rights(:full_control, "Clumsy_Sam") + else + @resource.path("/this/path/") + @resource.group("pokemon") + @resource.mode("0664") + @resource.owner("root") + end + @resource.source("https://www.google.com/images/srpr/logo3w.png") + @resource.checksum("1"*26) + end + + it "describes its state" do + state = @resource.state + if Chef::Platform.windows? + puts state + state[:rights].should == [{:permissions => :read, :principals => "Everyone"}] + state[:deny_rights].should == [{:permissions => :full_control, :principals => "Clumsy_Sam"}] + else + state[:group].should == "pokemon" + state[:mode].should == "0664" + state[:owner].should == "root" + state[:checksum].should == "1"*26 + end + end + + it "returns the path as its identity" do + if Chef::Platform.windows? + @resource.identity.should == "C:/temp/origin/file.txt" + else + @resource.identity.should == "/this/path/" + end + end + end + +end diff --git a/spec/unit/resource/route_spec.rb b/spec/unit/resource/route_spec.rb new file mode 100644 index 0000000000..54d5275411 --- /dev/null +++ b/spec/unit/resource/route_spec.rb @@ -0,0 +1,107 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org) +# Author:: Tyler Cloke (<tyler@opscode.com>) +# Copyright:: Copyright (c) 2008 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::Resource::Route do + + before(:each) do + @resource = Chef::Resource::Route.new("10.0.0.10") + end + + it "should create a new Chef::Resource::Route" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Route) + end + + it "should have a name" do + @resource.name.should eql("10.0.0.10") + end + + it "should have a default action of 'add'" do + @resource.action.should eql(:add) + end + + it "should accept add or delete for action" do + lambda { @resource.action :add }.should_not raise_error(ArgumentError) + lambda { @resource.action :delete }.should_not raise_error(ArgumentError) + lambda { @resource.action :lolcat }.should raise_error(ArgumentError) + end + + it "should use the object name as the target by default" do + @resource.target.should eql("10.0.0.10") + end + + it "should allow you to specify the netmask" do + @resource.netmask "255.255.255.0" + @resource.netmask.should eql("255.255.255.0") + end + + it "should allow you to specify the gateway" do + @resource.gateway "10.0.0.1" + @resource.gateway.should eql("10.0.0.1") + end + + it "should allow you to specify the metric" do + @resource.metric 10 + @resource.metric.should eql(10) + end + + it "should allow you to specify the device" do + @resource.device "eth0" + @resource.device.should eql("eth0") + end + + it "should allow you to specify the route type" do + @resource.route_type "host" + @resource.route_type.should eql(:host) + end + + it "should default to a host route type" do + @resource.route_type.should eql(:host) + end + + it "should accept a net route type" do + @resource.route_type :net + @resource.route_type.should eql(:net) + end + + it "should reject any other route_type but :host and :net" do + lambda { @resource.route_type "lolcat" }.should raise_error(ArgumentError) + end + + describe "when it has netmask, gateway, and device" do + before do + @resource.target("charmander") + @resource.netmask("lemask") + @resource.gateway("111.111.111") + @resource.device("forcefield") + end + + it "describes its state" do + state = @resource.state + state[:netmask].should == "lemask" + state[:gateway].should == "111.111.111" + end + + it "returns the target as its identity" do + @resource.identity.should == "charmander" + end + end +end diff --git a/spec/unit/resource/rpm_package_spec.rb b/spec/unit/resource/rpm_package_spec.rb new file mode 100644 index 0000000000..d59dc6b29c --- /dev/null +++ b/spec/unit/resource/rpm_package_spec.rb @@ -0,0 +1,38 @@ +# +# Author:: Thomas Bishop (<bishop.thomas@gmail.com>) +# Copyright:: Copyright (c) 2010 Thomas Bishop +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +describe Chef::Resource::RpmPackage, "initialize" do + + before(:each) do + @resource = Chef::Resource::RpmPackage.new("foo") + end + + it "should return a Chef::Resource::RpmPackage" do + @resource.should be_a_kind_of(Chef::Resource::RpmPackage) + end + + it "should set the resource_name to :rpm_package" do + @resource.resource_name.should eql(:rpm_package) + end + + it "should set the provider to Chef::Provider::Package::Rpm" do + @resource.provider.should eql(Chef::Provider::Package::Rpm) + end +end diff --git a/spec/unit/resource/ruby_block_spec.rb b/spec/unit/resource/ruby_block_spec.rb new file mode 100644 index 0000000000..637d4fe34d --- /dev/null +++ b/spec/unit/resource/ruby_block_spec.rb @@ -0,0 +1,61 @@ +# +# Author:: AJ Christensen (<aj@opscode.com>) +# Author:: Tyler Cloke (<tyler@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::Resource::RubyBlock do + + before(:each) do + @resource = Chef::Resource::RubyBlock.new("fakey_fakerton") + end + + it "should create a new Chef::Resource::RubyBlock" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::RubyBlock) + end + + it "should have a default action of 'create'" do + @resource.action.should eql("run") + end + + it "should have a resource name of :ruby_block" do + @resource.resource_name.should eql(:ruby_block) + end + + it "should accept a ruby block/proc/.. for the 'block' parameter" do + @resource.block do + "foo" + end.call.should eql("foo") + end + + it "allows the action to be 'create'" do + @resource.action :create + @resource.action.should == [:create] + end + + describe "when it has been initialized with block code" do + before do + @resource.block_name("puts 'harrrr'") + end + + it "returns the block as its identity" do + @resource.identity.should == "puts 'harrrr'" + end + end +end diff --git a/spec/unit/resource/ruby_spec.rb b/spec/unit/resource/ruby_spec.rb new file mode 100644 index 0000000000..48dfd90898 --- /dev/null +++ b/spec/unit/resource/ruby_spec.rb @@ -0,0 +1,40 @@ +# +# 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::Resource::Ruby do + + before(:each) do + @resource = Chef::Resource::Ruby.new("fakey_fakerton") + end + + it "should create a new Chef::Resource::Ruby" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Ruby) + end + + it "should have a resource name of :ruby" do + @resource.resource_name.should eql(:ruby) + end + + it "should have an interpreter of ruby" do + @resource.interpreter.should eql("ruby") + end + +end diff --git a/spec/unit/resource/scm_spec.rb b/spec/unit/resource/scm_spec.rb new file mode 100644 index 0000000000..488d335342 --- /dev/null +++ b/spec/unit/resource/scm_spec.rb @@ -0,0 +1,159 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Author:: Tyler Cloke (<tyler@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::Resource::Scm do + + before(:each) do + @resource = Chef::Resource::Scm.new("my awesome app") + end + + it "should be a SCM resource" do + @resource.should be_a_kind_of(Chef::Resource::Scm) + end + + it "supports :checkout, :export, :sync, :diff, and :log actions" do + @resource.allowed_actions.should include(:checkout) + @resource.allowed_actions.should include(:export) + @resource.allowed_actions.should include(:sync) + @resource.allowed_actions.should include(:diff) + @resource.allowed_actions.should include(:log) + end + + it "takes the destination path as a string" do + @resource.destination "/path/to/deploy/dir" + @resource.destination.should eql("/path/to/deploy/dir") + end + + it "takes a string for the repository URL" do + @resource.repository "git://github.com/opscode/chef.git" + @resource.repository.should eql("git://github.com/opscode/chef.git") + end + + it "takes a string for the revision" do + @resource.revision "abcdef" + @resource.revision.should eql("abcdef") + end + + it "defaults to the ``HEAD'' revision" do + @resource.revision.should eql("HEAD") + end + + it "takes a string for the user to run as" do + @resource.user "dr_deploy" + @resource.user.should eql("dr_deploy") + end + + it "also takes an integer for the user to run as" do + @resource.user 0 + @resource.user.should eql(0) + end + + it "takes a string for the group to run as, defaulting to nil" do + @resource.group.should be_nil + @resource.group "opsdevs" + @resource.group.should == "opsdevs" + end + + it "also takes an integer for the group to run as" do + @resource.group 23 + @resource.group.should == 23 + end + + it "has a svn_username String attribute" do + @resource.svn_username "moartestsplz" + @resource.svn_username.should eql("moartestsplz") + end + + it "has a svn_password String attribute" do + @resource.svn_password "taftplz" + @resource.svn_password.should eql("taftplz") + end + + it "has a svn_arguments String attribute" do + @resource.svn_arguments "--more-taft plz" + @resource.svn_arguments.should eql("--more-taft plz") + end + + it "has a svn_info_args String attribute" do + @resource.svn_info_args.should be_nil + @resource.svn_info_args("--no-moar-plaintext-creds yep") + @resource.svn_info_args.should == "--no-moar-plaintext-creds yep" + end + + it "takes the depth as an integer for shallow clones" do + @resource.depth 5 + @resource.depth.should == 5 + lambda {@resource.depth "five"}.should raise_error(ArgumentError) + end + + it "defaults to nil depth for a full clone" do + @resource.depth.should be_nil + end + + it "takes a boolean for #enable_submodules" do + @resource.enable_submodules true + @resource.enable_submodules.should be_true + lambda {@resource.enable_submodules "lolz"}.should raise_error(ArgumentError) + end + + it "defaults to not enabling submodules" do + @resource.enable_submodules.should be_false + end + + it "takes a string for the remote" do + @resource.remote "opscode" + @resource.remote.should eql("opscode") + lambda {@resource.remote 1337}.should raise_error(ArgumentError) + end + + it "defaults to ``origin'' for the remote" do + @resource.remote.should == "origin" + end + + it "takes a string for the ssh wrapper" do + @resource.ssh_wrapper "with_ssh_fu" + @resource.ssh_wrapper.should eql("with_ssh_fu") + end + + it "defaults to nil for the ssh wrapper" do + @resource.ssh_wrapper.should be_nil + end + + describe "when it has repository, revision, user, and group" do + before do + @resource.destination("hell") + @resource.repository("apt") + @resource.revision("1.2.3") + @resource.user("root") + @resource.group("super_adventure_club") + end + + it "describes its state" do + state = @resource.state + state[:revision].should == "1.2.3" + end + + it "returns the destination as its identity" do + @resource.identity.should == "hell" + end + end + +end diff --git a/spec/unit/resource/script_spec.rb b/spec/unit/resource/script_spec.rb new file mode 100644 index 0000000000..569602008b --- /dev/null +++ b/spec/unit/resource/script_spec.rb @@ -0,0 +1,69 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Tyler Cloke (<tyler@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::Resource::Script do + + before(:each) do + @resource = Chef::Resource::Script.new("fakey_fakerton") + end + + it "should create a new Chef::Resource::Script" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Script) + end + + it "should have a resource name of :script" do + @resource.resource_name.should eql(:script) + end + + it "should set command to the argument provided to new" do + @resource.command.should eql("fakey_fakerton") + end + + it "should accept a string for the code" do + @resource.code "hey jude" + @resource.code.should eql("hey jude") + end + + it "should accept a string for the interpreter" do + @resource.interpreter "naaaaNaNaNaaNaaNaaNaa" + @resource.interpreter.should eql("naaaaNaNaNaaNaaNaaNaa") + end + + it "should accept a string for the flags" do + @resource.flags "-f" + @resource.flags.should eql("-f") + end + + describe "when it has interpreter and flags" do + before do + @resource.command("grep") + @resource.interpreter("gcc") + @resource.flags("-al") + end + + it "returns the command as its identity" do + @resource.identity.should == "grep" + end + end + + +end diff --git a/spec/unit/resource/service_spec.rb b/spec/unit/resource/service_spec.rb new file mode 100644 index 0000000000..d7a90e8c7a --- /dev/null +++ b/spec/unit/resource/service_spec.rb @@ -0,0 +1,165 @@ +# +# Author:: AJ Christensen (<aj@hjksolutions.com>) +# Author:: Tyler Cloke (<tyler@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::Resource::Service do + + before(:each) do + @resource = Chef::Resource::Service.new("chef") + end + + it "should create a new Chef::Resource::Service" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::Service) + end + + it "should set the service_name to the first argument to new" do + @resource.service_name.should eql("chef") + end + + it "should set the pattern to be the service name by default" do + @resource.pattern.should eql("chef") + end + + it "should accept a string for the service name" do + @resource.service_name "something" + @resource.service_name.should eql("something") + end + + it "should accept a string for the service pattern" do + @resource.pattern ".*" + @resource.pattern.should eql(".*") + end + + it "should not accept a regexp for the service pattern" do + lambda { + @resource.pattern /.*/ + }.should raise_error(ArgumentError) + end + + it "should accept a string for the service start command" do + @resource.start_command "/etc/init.d/chef start" + @resource.start_command.should eql("/etc/init.d/chef start") + end + + it "should not accept a regexp for the service start command" do + lambda { + @resource.start_command /.*/ + }.should raise_error(ArgumentError) + end + + it "should accept a string for the service stop command" do + @resource.stop_command "/etc/init.d/chef stop" + @resource.stop_command.should eql("/etc/init.d/chef stop") + end + + it "should not accept a regexp for the service stop command" do + lambda { + @resource.stop_command /.*/ + }.should raise_error(ArgumentError) + end + + it "should accept a string for the service status command" do + @resource.status_command "/etc/init.d/chef status" + @resource.status_command.should eql("/etc/init.d/chef status") + end + + it "should not accept a regexp for the service status command" do + lambda { + @resource.status_command /.*/ + }.should raise_error(ArgumentError) + end + + it "should accept a string for the service restart command" do + @resource.restart_command "/etc/init.d/chef restart" + @resource.restart_command.should eql("/etc/init.d/chef restart") + end + + it "should not accept a regexp for the service restart command" do + lambda { + @resource.restart_command /.*/ + }.should raise_error(ArgumentError) + end + + it "should accept a string for the service reload command" do + @resource.reload_command "/etc/init.d/chef reload" + @resource.reload_command.should eql("/etc/init.d/chef reload") + end + + it "should not accept a regexp for the service reload command" do + lambda { + @resource.reload_command /.*/ + }.should raise_error(ArgumentError) + end + + %w{enabled running}.each do |attrib| + it "should accept true for #{attrib}" do + @resource.send(attrib, true) + @resource.send(attrib).should eql(true) + end + + it "should accept false for #{attrib}" do + @resource.send(attrib, false) + @resource.send(attrib).should eql(false) + end + + it "should not accept a string for #{attrib}" do + lambda { @resource.send(attrib, "poop") }.should raise_error(ArgumentError) + end + + it "should default all the feature support to false" do + support_hash = { :status => false, :restart => false, :reload=> false } + @resource.supports.should == support_hash + end + + it "should allow you to set what features this resource supports as a array" do + support_array = [ :status, :restart ] + support_hash = { :status => true, :restart => true, :reload => false } + @resource.supports(support_array) + @resource.supports.should == support_hash + end + + it "should allow you to set what features this resource supports as a hash" do + support_hash = { :status => true, :restart => true, :reload => false } + @resource.supports(support_hash) + @resource.supports.should == support_hash + end + end + + describe "when it has pattern and supports" do + before do + @resource.service_name("superfriend") + @resource.enabled(true) + @resource.running(false) + end + + it "describes its state" do + state = @resource.state + state[:enabled].should eql(true) + state[:running].should eql(false) + end + + it "returns the service name as its identity" do + @resource.identity.should == "superfriend" + end + end + + +end diff --git a/spec/unit/resource/smartos_package_spec.rb b/spec/unit/resource/smartos_package_spec.rb new file mode 100644 index 0000000000..ca815320ad --- /dev/null +++ b/spec/unit/resource/smartos_package_spec.rb @@ -0,0 +1,38 @@ +# +# Author:: Thomas Bishop (<bishop.thomas@gmail.com>) +# Copyright:: Copyright (c) 2010 Thomas Bishop +# 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")) + +describe Chef::Resource::SmartOSPackage, "initialize" do + + before(:each) do + @resource = Chef::Resource::SmartOSPackage.new("foo") + end + + it "should return a Chef::Resource::SmartOSPackage" do + @resource.should be_a_kind_of(Chef::Resource::SmartOSPackage) + end + + it "should set the resource_name to :smartos_package" do + @resource.resource_name.should eql(:smartos_package) + end + + it "should set the provider to Chef::Provider::Package::SmartOS" do + @resource.provider.should eql(Chef::Provider::Package::SmartOS) + end +end diff --git a/spec/unit/resource/subversion_spec.rb b/spec/unit/resource/subversion_spec.rb new file mode 100644 index 0000000000..650eb010a6 --- /dev/null +++ b/spec/unit/resource/subversion_spec.rb @@ -0,0 +1,58 @@ +# +# 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::Resource::Subversion do + + before do + @svn = Chef::Resource::Subversion.new("ohai, svn project!") + end + + it "is a subclass of Resource::Scm" do + @svn.should be_an_instance_of(Chef::Resource::Subversion) + @svn.should be_a_kind_of(Chef::Resource::Scm) + end + + it "uses the subversion provider" do + @svn.provider.should eql(Chef::Provider::Subversion) + end + + it "allows the force_export action" do + @svn.allowed_actions.should include(:force_export) + end + + it "sets svn info arguments to --no-auth-cache by default" do + @svn.svn_info_args.should == '--no-auth-cache' + end + + it "resets svn info arguments to nil when given false in the setter" do + @svn.svn_info_args(false) + @svn.svn_info_args.should be_nil + end + + it "sets svn arguments to --no-auth-cache by default" do + @svn.svn_arguments.should == '--no-auth-cache' + end + + it "resets svn arguments to nil when given false in the setter" do + @svn.svn_arguments(false) + @svn.svn_arguments.should be_nil + end + +end diff --git a/spec/unit/resource/template_spec.rb b/spec/unit/resource/template_spec.rb new file mode 100644 index 0000000000..f3313611db --- /dev/null +++ b/spec/unit/resource/template_spec.rb @@ -0,0 +1,108 @@ +# +# 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::Resource::Template do + + before(:each) do + @resource = Chef::Resource::Template.new("fakey_fakerton") + end + + describe "initialize" do + it "should create a new Chef::Resource::Template" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::File) + @resource.should be_a_kind_of(Chef::Resource::Template) + end + end + + describe "source" do + it "should accept a string for the template source" do + @resource.source "something" + @resource.source.should eql("something") + end + + it "should have a default based on the param name with .erb appended" do + @resource.source.should eql("fakey_fakerton.erb") + end + + it "should use only the basename of the file as the default" do + r = Chef::Resource::Template.new("/tmp/obit/fakey_fakerton") + r.source.should eql("fakey_fakerton.erb") + end + end + + describe "variables" do + it "should accept a hash for the variable list" do + @resource.variables({ :reluctance => :awkward }) + @resource.variables.should == { :reluctance => :awkward } + end + end + + describe "cookbook" do + it "should accept a string for the cookbook name" do + @resource.cookbook("foo") + @resource.cookbook.should == "foo" + end + + it "should default to nil" do + @resource.cookbook.should == nil + end + end + + describe "local" do + it "should accept a boolean for whether a template is local or remote" do + @resource.local(true) + @resource.local.should == true + end + + it "should default to false" do + @resource.local.should == false + end + end + + describe "when it has a path, owner, group, mode, and checksum" do + before do + @resource.path("/tmp/foo.txt") + @resource.owner("root") + @resource.group("wheel") + @resource.mode("0644") + @resource.checksum("1" * 64) + end + + context "on unix", :unix_only do + it "describes its state" do + state = @resource.state + state[:owner].should == "root" + state[:group].should == "wheel" + state[:mode].should == "0644" + state[:checksum].should == "1" * 64 + end + end + + context "on windows", :windows_only do + # according to Chef::Resource::File, windows state attributes are rights + deny_rights + pending "it describes its state" + end + + it "returns the file path as its identity" do + @resource.identity.should == "/tmp/foo.txt" + end + end +end diff --git a/spec/unit/resource/timestamped_deploy_spec.rb b/spec/unit/resource/timestamped_deploy_spec.rb new file mode 100644 index 0000000000..89b881830a --- /dev/null +++ b/spec/unit/resource/timestamped_deploy_spec.rb @@ -0,0 +1,28 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2009 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' + +describe Chef::Resource::TimestampedDeploy do + + it "defaults to the TimestampedDeploy provider" do + @resource = Chef::Resource::TimestampedDeploy.new("stuff") + @resource.provider.should == Chef::Provider::Deploy::Timestamped + end + +end diff --git a/spec/unit/resource/user_spec.rb b/spec/unit/resource/user_spec.rb new file mode 100644 index 0000000000..3361921173 --- /dev/null +++ b/spec/unit/resource/user_spec.rb @@ -0,0 +1,122 @@ +# +# 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::Resource::User, "initialize" do + before(:each) do + @resource = Chef::Resource::User.new("adam") + end + + it "should create a new Chef::Resource::User" do + @resource.should be_a_kind_of(Chef::Resource) + @resource.should be_a_kind_of(Chef::Resource::User) + end + + it "should set the resource_name to :user" do + @resource.resource_name.should eql(:user) + end + + it "should set the username equal to the argument to initialize" do + @resource.username.should eql("adam") + end + + %w{comment uid gid home shell password}.each do |attrib| + it "should set #{attrib} to nil" do + @resource.send(attrib).should eql(nil) + end + end + + it "should set action to :create" do + @resource.action.should eql(:create) + end + + it "should set supports[:manage_home] to false" do + @resource.supports[:manage_home].should eql(false) + end + + it "should set supports[:non_unique] to false" do + @resource.supports[:non_unique].should eql(false) + end + + %w{create remove modify manage lock unlock}.each do |action| + it "should allow action #{action}" do + @resource.allowed_actions.detect { |a| a == action.to_sym }.should eql(action.to_sym) + end + end +end + +%w{username comment home shell password}.each do |attrib| + describe Chef::Resource::User, attrib do + before(:each) do + @resource = Chef::Resource::User.new("adam") + end + + it "should allow a string" do + @resource.send(attrib, "adam") + @resource.send(attrib).should eql("adam") + end + + it "should not allow a hash" do + lambda { @resource.send(attrib, { :woot => "i found it" }) }.should raise_error(ArgumentError) + end + end +end + +%w{uid gid}.each do |attrib| + describe Chef::Resource::User, attrib do + before(:each) do + @resource = Chef::Resource::User.new("adam") + end + + it "should allow a string" do + @resource.send(attrib, "100") + @resource.send(attrib).should eql("100") + end + + it "should allow an integer" do + @resource.send(attrib, 100) + @resource.send(attrib).should eql(100) + end + + it "should not allow a hash" do + lambda { @resource.send(attrib, { :woot => "i found it" }) }.should raise_error(ArgumentError) + end + end + + describe "when it has uid, gid, and home" do + before do + @resource = Chef::Resource::User.new("root") + @resource.uid(123) + @resource.gid(456) + @resource.home("/usr/local/root/") + end + + it "describes its state" do + state = @resource.state + state[:uid].should == 123 + state[:gid].should == 456 + state[:home].should == "/usr/local/root/" + end + + it "returns the username as its identity" do + @resource.identity.should == "root" + end + end + +end diff --git a/spec/unit/resource/yum_package_spec.rb b/spec/unit/resource/yum_package_spec.rb new file mode 100644 index 0000000000..a0f4aaee55 --- /dev/null +++ b/spec/unit/resource/yum_package_spec.rb @@ -0,0 +1,85 @@ +# +# 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::Resource::YumPackage, "initialize" do + + before(:each) do + @resource = Chef::Resource::YumPackage.new("foo") + end + + it "should return a Chef::Resource::YumPackage" do + @resource.should be_a_kind_of(Chef::Resource::YumPackage) + end + + it "should set the resource_name to :yum_package" do + @resource.resource_name.should eql(:yum_package) + end + + it "should set the provider to Chef::Provider::Package::Yum" do + @resource.provider.should eql(Chef::Provider::Package::Yum) + end +end + +describe Chef::Resource::YumPackage, "arch" do + before(:each) do + @resource = Chef::Resource::YumPackage.new("foo") + end + + it "should set the arch variable to whatever is passed in" do + @resource.arch("i386") + @resource.arch.should eql("i386") + end +end + +describe Chef::Resource::YumPackage, "flush_cache" do + before(:each) do + @resource = Chef::Resource::YumPackage.new("foo") + end + + it "should default the flush timing to false" do + flush_hash = { :before => false, :after => false } + @resource.flush_cache.should == flush_hash + end + + it "should allow you to set the flush timing with an array" do + flush_array = [ :before, :after ] + flush_hash = { :before => true, :after => true } + @resource.flush_cache(flush_array) + @resource.flush_cache.should == flush_hash + end + + it "should allow you to set the flush timing with a hash" do + flush_hash = { :before => true, :after => true } + @resource.flush_cache(flush_hash) + @resource.flush_cache.should == flush_hash + end +end + +describe Chef::Resource::YumPackage, "allow_downgrade" do + before(:each) do + @resource = Chef::Resource::YumPackage.new("foo") + end + + it "should allow you to specify whether allow_downgrade is true or false" do + lambda { @resource.allow_downgrade true }.should_not raise_error(ArgumentError) + lambda { @resource.allow_downgrade false }.should_not raise_error(ArgumentError) + lambda { @resource.allow_downgrade "monkey" }.should raise_error(ArgumentError) + end +end diff --git a/spec/unit/resource_collection/stepable_iterator_spec.rb b/spec/unit/resource_collection/stepable_iterator_spec.rb new file mode 100644 index 0000000000..1da9de20cc --- /dev/null +++ b/spec/unit/resource_collection/stepable_iterator_spec.rb @@ -0,0 +1,144 @@ +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2009 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' + +describe Chef::ResourceCollection::StepableIterator do + CRSI = Chef::ResourceCollection::StepableIterator + + it "has an empty array for its collection by default" do + CRSI.new.collection.should == [] + end + + describe "doing basic iteration" do + before do + @simple_collection = [1,2,3,4] + @iterator = CRSI.for_collection(@simple_collection) + end + + it "re-initializes the instance with a collection" do + @iterator.collection.should equal(@simple_collection) + @iterator.size.should == 4 + end + + it "iterates over the collection" do + sum = 0 + @iterator.each do |int| + sum += int + end + sum.should == 10 + end + + it "iterates over the collection with each_index" do + collected_by_index = [] + @iterator.each_index do |idx| + collected_by_index << @simple_collection[idx] + end + collected_by_index.should == @simple_collection + collected_by_index.should_not equal(@simple_collection) + end + + it "iterates over the collection with index and element" do + collected = {} + @iterator.each_with_index do |element, index| + collected[index] = element + end + collected.should == {0=>1, 1=>2, 2=>3, 3=>4} + end + + end + + describe "pausing and resuming iteration" do + + before do + @collection = [] + @snitch_var = nil + @collection << lambda { @snitch_var = 23 } + @collection << lambda { @iterator.pause } + @collection << lambda { @snitch_var = 42 } + + @iterator = CRSI.for_collection(@collection) + @iterator.each { |proc| proc.call } + end + + it "allows the iteration to be paused" do + @snitch_var.should == 23 + end + + it "allows the iteration to be resumed" do + @snitch_var.should == 23 + @iterator.resume + @snitch_var.should == 42 + end + + it "allows iteration to be rewound" do + @iterator.skip_back(2) + @iterator.resume + @snitch_var.should == 23 + @iterator.resume + @snitch_var.should == 42 + end + + it "allows iteration to be fast forwarded" do + @iterator.skip_forward + @iterator.resume + @snitch_var.should == 23 + end + + it "allows iteration to be rewound" do + @snitch_var = nil + @iterator.rewind + @iterator.position.should == 0 + @iterator.resume + @snitch_var.should == 23 + end + + it "allows iteration to be stepped" do + @snitch_var = nil + @iterator.rewind + @iterator.step + @iterator.position.should == 1 + @snitch_var.should == 23 + end + + it "doesn't step if there are no more steps" do + @iterator.step.should == 3 + lambda {@iterator.step}.should_not raise_error + @iterator.step.should be_nil + end + + it "allows the iteration to start by being stepped" do + @snitch_var = nil + @iterator = CRSI.for_collection(@collection) + @iterator.iterate_on(:element) { |proc| proc.call } + @iterator.step + @iterator.position.should == 1 + @snitch_var.should == 23 + end + + it "should work correctly when elements are added to the collection during iteration" do + @collection.insert(2, lambda { @snitch_var = 815}) + @collection.insert(3, lambda { @iterator.pause }) + @iterator.resume + @snitch_var.should == 815 + @iterator.resume + @snitch_var.should == 42 + end + + end + +end diff --git a/spec/unit/resource_collection_spec.rb b/spec/unit/resource_collection_spec.rb new file mode 100644 index 0000000000..909ba71e00 --- /dev/null +++ b/spec/unit/resource_collection_spec.rb @@ -0,0 +1,257 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Copyright:: Copyright (c) 2008, 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::ResourceCollection do + + before(:each) do + @rc = Chef::ResourceCollection.new() + @resource = Chef::Resource::ZenMaster.new("makoto") + end + + describe "initialize" do + it "should return a Chef::ResourceCollection" do + @rc.should be_kind_of(Chef::ResourceCollection) + end + end + + describe "[]" do + it "should accept Chef::Resources through [index]" do + lambda { @rc[0] = @resource }.should_not raise_error + lambda { @rc[0] = "string" }.should raise_error + end + + it "should allow you to fetch Chef::Resources by position" do + @rc[0] = @resource + @rc[0].should eql(@resource) + end + end + + describe "push" do + it "should accept Chef::Resources through pushing" do + lambda { @rc.push(@resource) }.should_not raise_error + lambda { @rc.push("string") }.should raise_error + end + end + + describe "<<" do + it "should accept the << operator" do + lambda { @rc << @resource }.should_not raise_error + end + end + + describe "insert" do + it "should accept only Chef::Resources" do + lambda { @rc.insert(@resource) }.should_not raise_error + lambda { @rc.insert("string") }.should raise_error + end + + it "should append resources to the end of the collection when not executing a run" do + zmr = Chef::Resource::ZenMaster.new("there is no spoon") + @rc.insert(@resource) + @rc.insert(zmr) + @rc[0].should eql(@resource) + @rc[1].should eql(zmr) + end + + it "should insert resources to the middle of the collection if called while executing a run" do + resource_to_inject = Chef::Resource::ZenMaster.new("there is no spoon") + zmr = Chef::Resource::ZenMaster.new("morpheus") + dummy = Chef::Resource::ZenMaster.new("keanu reeves") + @rc.insert(zmr) + @rc.insert(dummy) + + @rc.execute_each_resource do |resource| + @rc.insert(resource_to_inject) if resource == zmr + end + + @rc[0].should eql(zmr) + @rc[1].should eql(resource_to_inject) + @rc[2].should eql(dummy) + end + end + + describe "each" do + it "should allow you to iterate over every resource in the collection" do + load_up_resources + results = Array.new + lambda { + @rc.each do |r| + results << r.name + end + }.should_not raise_error + results.each_index do |i| + case i + when 0 + results[i].should eql("dog") + when 1 + results[i].should eql("cat") + when 2 + results[i].should eql("monkey") + end + end + end + end + + describe "each_index" do + it "should allow you to iterate over every resource by index" do + load_up_resources + results = Array.new + lambda { + @rc.each_index do |i| + results << @rc[i].name + end + }.should_not raise_error() + results.each_index do |i| + case i + when 0 + results[i].should eql("dog") + when 1 + results[i].should eql("cat") + when 2 + results[i].should eql("monkey") + end + end + end + end + + describe "lookup" do + it "should allow you to find resources by name via lookup" do + zmr = Chef::Resource::ZenMaster.new("dog") + @rc << zmr + @rc.lookup(zmr.to_s).should eql(zmr) + + zmr = Chef::Resource::ZenMaster.new("cat") + @rc[0] = zmr + @rc.lookup(zmr).should eql(zmr) + + zmr = Chef::Resource::ZenMaster.new("monkey") + @rc.push(zmr) + @rc.lookup(zmr).should eql(zmr) + end + + it "should raise an exception if you send something strange to lookup" do + lambda { @rc.lookup(:symbol) }.should raise_error(ArgumentError) + end + + it "should raise an exception if it cannot find a resource with lookup" do + lambda { @rc.lookup("zen_master[dog]") }.should raise_error(Chef::Exceptions::ResourceNotFound) + end + end + + describe "resources" do + + it "should find a resource by symbol and name (:zen_master => monkey)" do + load_up_resources + @rc.resources(:zen_master => "monkey").name.should eql("monkey") + end + + it "should find a resource by symbol and array of names (:zen_master => [a,b])" do + load_up_resources + results = @rc.resources(:zen_master => [ "monkey", "dog" ]) + results.length.should eql(2) + check_by_names(results, "monkey", "dog") + end + + it "should find resources of multiple kinds (:zen_master => a, :file => b)" do + load_up_resources + results = @rc.resources(:zen_master => "monkey", :file => "something") + results.length.should eql(2) + check_by_names(results, "monkey", "something") + end + + it "should find a resource by string zen_master[a]" do + load_up_resources + @rc.resources("zen_master[monkey]").name.should eql("monkey") + end + + it "should find resources by strings of zen_master[a,b]" do + load_up_resources + results = @rc.resources("zen_master[monkey,dog]") + results.length.should eql(2) + check_by_names(results, "monkey", "dog") + end + + it "should find resources of multiple types by strings of zen_master[a]" do + load_up_resources + results = @rc.resources("zen_master[monkey]", "file[something]") + results.length.should eql(2) + check_by_names(results, "monkey", "something") + end + + it "should raise an exception if you pass a bad name to resources" do + lambda { @rc.resources("michael jackson") }.should raise_error(ArgumentError) + end + + it "should raise an exception if you pass something other than a string or hash to resource" do + lambda { @rc.resources([Array.new]) }.should raise_error(ArgumentError) + end + + it "raises an error when attempting to find a resource that does not exist" do + lambda {@rc.find("script[nonesuch]")}.should raise_error(Chef::Exceptions::ResourceNotFound) + end + + end + + describe "to_json" do + it "should serialize to json" do + json = @rc.to_json + json.should =~ /json_class/ + json.should =~ /instance_vars/ + end + end + + describe "self.from_json" do + it "should deserialize itself from json" do + @rc << @resource + json = @rc.to_json + s_rc = Chef::JSONCompat.from_json(json) + s_rc.should be_a_kind_of(Chef::ResourceCollection) + s_rc[0].name.should eql(@resource.name) + end + end + + describe "provides access to the raw resources array" do + it "returns the resources via the all_resources method" do + @rc.all_resources.should equal(@rc.instance_variable_get(:@resources)) + end + end + + describe "provides access to stepable iterator" do + it "returns the iterator object" do + @rc.instance_variable_set(:@iterator, :fooboar) + @rc.iterator.should == :fooboar + end + end + + def check_by_names(results, *names) + names.each do |res_name| + results.detect{ |res| res.name == res_name }.should_not eql(nil) + end + end + + def load_up_resources + %w{dog cat monkey}.each do |n| + @rc << Chef::Resource::ZenMaster.new(n) + end + @rc << Chef::Resource::File.new("something") + end + +end diff --git a/spec/unit/resource_definition_spec.rb b/spec/unit/resource_definition_spec.rb new file mode 100644 index 0000000000..a29c15a1e3 --- /dev/null +++ b/spec/unit/resource_definition_spec.rb @@ -0,0 +1,119 @@ +# +# 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::ResourceDefinition do + before(:each) do + @def = Chef::ResourceDefinition.new() + end + + describe "initialize" do + it "should be a Chef::ResourceDefinition" do + @def.should be_a_kind_of(Chef::ResourceDefinition) + end + + it "should not initialize a new node if one is not provided" do + @def.node.should eql(nil) + end + + it "should accept a node as an argument" do + node = Chef::Node.new + node.name("bobo") + @def = Chef::ResourceDefinition.new(node) + @def.node.name.should == "bobo" + end + end + + describe "node" do + it "should set the node with node=" do + node = Chef::Node.new + node.name("bobo") + @def.node = node + @def.node.name.should == "bobo" + end + + it "should return the node" do + @def.node = Chef::Node.new + @def.node.should be_a_kind_of(Chef::Node) + end + end + + it "should accept a new definition with a symbol for a name" do + lambda { + @def.define :smoke do + end + }.should_not raise_error(ArgumentError) + lambda { + @def.define "george washington" do + end + }.should raise_error(ArgumentError) + @def.name.should eql(:smoke) + end + + it "should accept a new definition with a hash" do + lambda { + @def.define :smoke, :cigar => "cuban", :cigarette => "marlboro" do + end + }.should_not raise_error(ArgumentError) + end + + it "should expose the prototype hash params in the params hash" do + @def.define :smoke, :cigar => "cuban", :cigarette => "marlboro" do; end + @def.params[:cigar].should eql("cuban") + @def.params[:cigarette].should eql("marlboro") + end + + it "should store the block passed to define as a proc under recipe" do + @def.define :smoke do + "I am what I am" + end + @def.recipe.should be_a_kind_of(Proc) + @def.recipe.call.should eql("I am what I am") + end + + it "should set paramaters based on method_missing" do + @def.mind "to fly" + @def.params[:mind].should eql("to fly") + end + + it "should raise an exception if prototype_params is not a hash" do + lambda { + @def.define :monkey, Array.new do + end + }.should raise_error(ArgumentError) + end + + it "should raise an exception if define is called without a block" do + lambda { + @def.define :monkey + }.should raise_error(ArgumentError) + end + + it "should load a description from a file" do + @def.from_file(File.join(CHEF_SPEC_DATA, "definitions", "test.rb")) + @def.name.should eql(:rico_suave) + @def.params[:rich].should eql("smooth") + end + + it "should turn itself into a string based on the name with to_s" do + @def.name = :woot + @def.to_s.should eql("woot") + end + +end diff --git a/spec/unit/resource_platform_map_spec.rb b/spec/unit/resource_platform_map_spec.rb new file mode 100644 index 0000000000..99673d868f --- /dev/null +++ b/spec/unit/resource_platform_map_spec.rb @@ -0,0 +1,164 @@ +# +# Author:: Seth Chisamore (<schisamo@opscode.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::Resource::PlatformMap do + + before(:each) do + @platform_map = Chef::Resource::PlatformMap.new({ + :windows => { + "6.1" => { + :file => "softiefile", + :else => "thing" + }, + :default => { + :file => Chef::Resource::File, + :ping => "pong", + :cat => "nice" + } + }, + :pop_tron => { + }, + :default => { + :soundwave => "lazerbeak", + :directory => Chef::Resource::Directory, + } + }) + end + + describe 'filtering the map' do + it "returns resources for platform and version" do + pmap = @platform_map.filter("Windows", "6.1") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql("softiefile") + end + + it "returns platform default resources if version does not exist" do + pmap = @platform_map.filter("windows", "1") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql(Chef::Resource::File) + end + + it "returns global default resources if none exist for plaform" do + pmap = @platform_map.filter("pop_tron", "1") + pmap.should be_a_kind_of(Hash) + pmap[:directory].should eql(Chef::Resource::Directory) + end + + it "returns global default resources if platform does not exist" do + pmap = @platform_map.filter("BeOS", "1") + pmap.should be_a_kind_of(Hash) + pmap[:soundwave].should eql("lazerbeak") + end + + it "returns a merged map of platform version and plaform default resources" do + pmap = @platform_map.filter("Windows", "6.1") + pmap[:file].should eql("softiefile") + pmap[:ping].should eql("pong") + end + + it "returns a merged map of platform specific version and global defaults" do + pmap = @platform_map.filter("Windows", "6.1") + pmap[:file].should eql("softiefile") + pmap[:soundwave].should eql("lazerbeak") + end + end + + describe 'finding a resource' do + it "returns a resource for a platform directly by short name" do + @platform_map.get(:file, "windows", "6.1").should eql("softiefile") + end + + it "returns a default resource if platform and version don't exist" do + @platform_map.get(:remote_file).should eql(Chef::Resource::RemoteFile) + end + + it "raises an exception if a resource cannot be found" do + lambda { @platform_map.get(:coffee, "windows", "6.1")}.should raise_error(NameError) + end + + it "returns a resource with a Chef::Resource object" do + kitty = Chef::Resource::Cat.new("loulou") + @platform_map.get(kitty, "windows", "6.1").should eql("nice") + end + end + + describe 'building the map' do + it "allows passing of a resource map at creation time" do + @new_map = Chef::Resource::PlatformMap.new({:the_dude => {:default => 'abides'}}) + @new_map.map[:the_dude][:default].should eql("abides") + end + + it "defaults to a resource map with :default key" do + @new_map = Chef::Resource::PlatformMap.new + @new_map.map.has_key?(:default) + end + + it "updates the resource map with a map" do + @platform_map.set( + :platform => :darwin, + :version => "9.2.2", + :short_name => :file, + :resource => "masterful" + ) + @platform_map.map[:darwin]["9.2.2"][:file].should eql("masterful") + + @platform_map.set( + :platform => :darwin, + :short_name => :file, + :resource => "masterful" + ) + @platform_map.map[:darwin][:default][:file].should eql("masterful") + + @platform_map.set( + :short_name => :file, + :resource => "masterful" + ) + @platform_map.map[:default][:file].should eql("masterful") + + @platform_map.set( + :platform => :hero, + :version => "9.2.2", + :short_name => :file, + :resource => "masterful" + ) + @platform_map.map[:hero]["9.2.2"][:file].should eql("masterful") + + @platform_map.set( + :short_name => :file, + :resource => "masterful" + ) + @platform_map.map[:default][:file].should eql("masterful") + + @platform_map.set( + :short_name => :file, + :resource => "masterful" + ) + @platform_map.map[:default][:file].should eql("masterful") + + @platform_map.set( + :platform => :neurosis, + :short_name => :package, + :resource => "masterful" + ) + @platform_map.map[:neurosis][:default][:package].should eql("masterful") + end + end + +end diff --git a/spec/unit/resource_reporter_spec.rb b/spec/unit/resource_reporter_spec.rb new file mode 100644 index 0000000000..bc8e3995f7 --- /dev/null +++ b/spec/unit/resource_reporter_spec.rb @@ -0,0 +1,580 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Author:: Prajakta Purohit (<prajakta@opscode.com>) +# Author:: Tyler Cloke (<tyler@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 File.expand_path("../../spec_helper", __FILE__) +require 'chef/resource_reporter' + +describe Chef::ResourceReporter do + before(:all) do + @reporting_toggle_default = Chef::Config[:enable_reporting] + Chef::Config[:enable_reporting] = true + end + + after(:all) do + Chef::Config[:enable_reporting] = @reporting_toggle_default + end + + before do + @node = Chef::Node.new + @node.name("spitfire") + @rest_client = mock("Chef::REST (mock)") + @rest_client.stub!(:post_rest).and_return(true) + @resource_reporter = Chef::ResourceReporter.new(@rest_client) + @new_resource = Chef::Resource::File.new("/tmp/a-file.txt") + @new_resource.cookbook_name = "monkey" + @cookbook_version = mock("Cookbook::Version", :version => "1.2.3") + @new_resource.stub!(:cookbook_version).and_return(@cookbook_version) + @current_resource = Chef::Resource::File.new("/tmp/a-file.txt") + end + + context "when first created" do + + it "has no updated resources" do + @resource_reporter.should have(0).updated_resources + end + + it "reports a successful run" do + @resource_reporter.status.should == "success" + end + + it "assumes the resource history feature is supported" do + @resource_reporter.reporting_enabled?.should be_true + end + + it "should have no error_descriptions" do + @resource_reporter.error_descriptions.should eq({}) + # @resource_reporter.error_descriptions.should be_empty + # @resource_reporter.should have(0).error_descriptions + end + + end + + context "after the chef run completes" do + + before do + end + + it "reports a successful run" do + pending "refactor how node gets set." + @resource_reporter.status.should == "success" + end + end + + context "when chef fails" do + before do + @rest_client.stub!(:create_url).and_return("reports/nodes/spitfire/runs/ABC123"); + @rest_client.stub!(:raw_http_request).and_return({"result"=>"ok"}); + @rest_client.stub!(:post_rest).and_return({"uri"=>"https://example.com/reports/nodes/spitfire/runs/ABC123"}); + + @resource_reporter.node_load_completed(@node, :expanded_run_list, :config) + end + + context "before converging any resources" do + before do + @exception = Exception.new + @resource_reporter.run_failed(@exception) + end + + it "sets the run status to 'failure'" do + @resource_reporter.status.should == "failure" + end + + it "keeps the exception data" do + @resource_reporter.exception.should == @exception + end + end + + context "when a resource fails before loading current state" do + before do + @exception = Exception.new + @exception.set_backtrace(caller) + @resource_reporter.resource_action_start(@new_resource, :create) + @resource_reporter.resource_failed(@new_resource, :create, @exception) + @resource_reporter.resource_completed(@new_resource) + end + + it "collects the resource as an updated resource" do + @resource_reporter.should have(1).updated_resources + end + + it "collects the desired state of the resource" do + update_record = @resource_reporter.updated_resources.first + update_record.new_resource.should == @new_resource + end + end + + # TODO: make sure a resource that is skipped because of `not_if` doesn't + # leave us in a bad state. + + context "once the a resource's current state is loaded" do + before do + @resource_reporter.resource_action_start(@new_resource, :create) + @resource_reporter.resource_current_state_loaded(@new_resource, :create, @current_resource) + end + + context "and the resource was not updated" do + before do + @resource_reporter.resource_up_to_date(@new_resource, :create) + end + + it "has no updated resources" do + @resource_reporter.should have(0).updated_resources + end + end + + context "and the resource was updated" do + before do + @new_resource.content("this is the old content") + @current_resource.content("this is the new hotness") + @resource_reporter.resource_updated(@new_resource, :create) + @resource_reporter.resource_completed(@new_resource) + end + + it "collects the updated resource" do + @resource_reporter.should have(1).updated_resources + end + + it "collects the old state of the resource" do + update_record = @resource_reporter.updated_resources.first + update_record.current_resource.should == @current_resource + end + + it "collects the new state of the resource" do + update_record = @resource_reporter.updated_resources.first + update_record.new_resource.should == @new_resource + end + + context "and a subsequent resource fails before loading current resource" do + before do + @next_new_resource = Chef::Resource::Service.new("apache2") + @exception = Exception.new + @exception.set_backtrace(caller) + @resource_reporter.resource_failed(@next_new_resource, :create, @exception) + @resource_reporter.resource_completed(@next_new_resource) + end + + it "collects the desired state of the failed resource" do + failed_resource_update = @resource_reporter.updated_resources.last + failed_resource_update.new_resource.should == @next_new_resource + end + + it "does not have the current state of the failed resource" do + failed_resource_update = @resource_reporter.updated_resources.last + failed_resource_update.current_resource.should be_nil + end + end + end + + # Some providers, such as RemoteDirectory and some LWRPs use other + # resources for their implementation. These should be hidden from reporting + # since we only care about the top-level resource and not the sub-resources + # used for implementation. + context "and a nested resource is updated" do + before do + @implementation_resource = Chef::Resource::CookbookFile.new("/preseed-file.txt") + @resource_reporter.resource_action_start(@implementation_resource , :create) + @resource_reporter.resource_current_state_loaded(@implementation_resource, :create, @implementation_resource) + @resource_reporter.resource_updated(@implementation_resource, :create) + @resource_reporter.resource_completed(@implementation_resource) + @resource_reporter.resource_updated(@new_resource, :create) + @resource_reporter.resource_completed(@new_resource) + end + + it "does not collect data about the nested resource" do + @resource_reporter.should have(1).updated_resources + end + end + + context "and a nested resource runs but is not updated" do + before do + @implementation_resource = Chef::Resource::CookbookFile.new("/preseed-file.txt") + @resource_reporter.resource_action_start(@implementation_resource , :create) + @resource_reporter.resource_current_state_loaded(@implementation_resource, :create, @implementation_resource) + @resource_reporter.resource_up_to_date(@implementation_resource, :create) + @resource_reporter.resource_completed(@implementation_resource) + @resource_reporter.resource_updated(@new_resource, :create) + @resource_reporter.resource_completed(@new_resource) + end + + it "does not collect data about the nested resource" do + @resource_reporter.should have(1).updated_resources + end + end + + context "and the resource failed to converge" do + before do + @exception = Exception.new + @exception.set_backtrace(caller) + @resource_reporter.resource_failed(@new_resource, :create, @exception) + @resource_reporter.resource_completed(@new_resource) + end + + it "collects the resource as an updated resource" do + @resource_reporter.should have(1).updated_resources + end + + it "collects the desired state of the resource" do + update_record = @resource_reporter.updated_resources.first + update_record.new_resource.should == @new_resource + end + + it "collects the current state of the resource" do + update_record = @resource_reporter.updated_resources.first + update_record.current_resource.should == @current_resource + end + end + + end + end + + describe "when generating a report for the server" do + + before do + @rest_client.stub!(:create_url).and_return("reports/nodes/spitfire/runs/ABC123"); + @rest_client.stub!(:raw_http_request).and_return({"result"=>"ok"}); + @rest_client.stub!(:post_rest).and_return({"uri"=>"https://example.com/reports/nodes/spitfire/runs/ABC123"}); + + @resource_reporter.node_load_completed(@node, :expanded_run_list, :config) + end + + context "for a successful client run" do + before do + # TODO: add inputs to generate expected output. + + # expected_data = { + # "action" : "end", + # "resources" : [ + # { + # "type" : "file", + # "id" : "/etc/passwd", + # "name" : "User Defined Resource Block Name", + # "duration" : "1200", + # "result" : "modified", + # "before" : { + # "state" : "exists", + # "group" : "root", + # "owner" : "root", + # "checksum" : "xyz" + # }, + # "after" : { + # "state" : "modified", + # "group" : "root", + # "owner" : "root", + # "checksum" : "abc" + # }, + # "delta" : "" + # }, + # {...} + # ], + # "status" : "success" + # "data" : "" + # } + @resource_reporter.resource_action_start(@new_resource, :create) + @resource_reporter.resource_current_state_loaded(@new_resource, :create, @current_resource) + @resource_reporter.resource_updated(@new_resource, :create) + @resource_reporter.resource_completed(@new_resource) + @report = @resource_reporter.prepare_run_data + @first_update_report = @report["resources"].first + end + + it "includes the run's status" do + @report.should have_key("status") + end + + it "includes a list of updated resources" do + @report.should have_key("resources") + end + + it "includes an updated resource's type" do + @first_update_report.should have_key("type") + end + + it "includes an updated resource's initial state" do + @first_update_report["before"].should == @current_resource.state + end + + it "includes an updated resource's final state" do + @first_update_report["after"].should == @new_resource.state + end + + it "includes the resource's name" do + @first_update_report["name"].should == @new_resource.name + end + + it "includes the resource's id attribute" do + @first_update_report["id"].should == @new_resource.identity + end + + it "includes the elapsed time for the resource to converge" do + # TODO: API takes integer number of milliseconds as a string. This + # should be an int. + @first_update_report.should have_key("duration") + @first_update_report["duration"].to_i.should be_within(100).of(0) + end + + it "includes the action executed by the resource" do + # TODO: rename as "action" + @first_update_report["result"].should == "create" + end + + it "includes the cookbook name of the resource" do + @first_update_report.should have_key("cookbook_name") + @first_update_report["cookbook_name"].should == "monkey" + end + + it "includes the cookbook version of the resource" do + @first_update_report.should have_key("cookbook_version") + @first_update_report["cookbook_version"].should == "1.2.3" + end + + it "includes the total resource count" do + @report.should have_key("total_res_count") + @report["total_res_count"].should == "1" + end + + it "includes the data hash" do + @report.should have_key("data") + @report["data"].should == {} + end + + it "includes the run_list" do + @report.should have_key("run_list") + @report["run_list"].should == @node.run_list.to_json + end + end + + context "for an unsuccessful run" do + + before do + @backtrace = ["foo.rb:1 in `foo!'","bar.rb:2 in `bar!","'baz.rb:3 in `baz!'"] + @node = Chef::Node.new + @node.name("spitfire") + @exception = mock("ArgumentError") + @exception.stub!(:inspect).and_return("Net::HTTPServerException") + @exception.stub!(:message).and_return("Object not found") + @exception.stub!(:backtrace).and_return(@backtrace) + @resource_reporter.run_list_expand_failed(@node, @exception) + @resource_reporter.run_failed(@exception) + @report = @resource_reporter.prepare_run_data + end + + it "includes the exception type in the event data" do + @report.should have_key("data") + @report["data"]["exception"].should have_key("class") + @report["data"]["exception"]["class"].should == "Net::HTTPServerException" + end + + it "includes the exception message in the event data" do + @report["data"]["exception"].should have_key("message") + @report["data"]["exception"]["message"].should == "Object not found" + end + + it "includes the exception trace in the event data" do + @report["data"]["exception"].should have_key("backtrace") + @report["data"]["exception"]["backtrace"].should == @backtrace.to_json + end + + it "includes the error inspector output in the event data" do + @report["data"]["exception"].should have_key("description") + @report["data"]["exception"]["description"].should include({"title"=>"Error expanding the run_list:", "sections"=>[["Unexpected Error:", "RSpec::Mocks::Mock: Object not found"]]}) + end + + end + + end + + describe "when updating resource history on the server" do + before do + end + + context "when the server does not support storing resource history" do + before do + # 404 getting the run_id + @response = Net::HTTPNotFound.new("a response body", "404", "Not Found") + @error = Net::HTTPServerException.new("404 message", @response) + @rest_client.should_receive(:post_rest). + with("reports/nodes/spitfire/runs", {:action => :begin}). + and_raise(@error) + @resource_reporter.node_load_completed(@node, :expanded_run_list, :config) + end + + it "assumes the feature is not enabled" do + @resource_reporter.reporting_enabled?.should be_false + end + + it "does not send a resource report to the server" do + @rest_client.should_not_receive(:post_rest) + @resource_reporter.run_completed(@node) + end + + end + + context "when the server returns a 500 to the client" do + before do + # 500 getting the run_id + @response = Net::HTTPInternalServerError.new("a response body", "500", "Internal Server Error") + @error = Net::HTTPServerException.new("500 message", @response) + @rest_client.should_receive(:post_rest). + with("reports/nodes/spitfire/runs", {:action => :begin}). + and_raise(@error) + @resource_reporter.node_load_completed(@node, :expanded_run_list, :config) + end + + it "assumes the feature is not enabled" do + @resource_reporter.reporting_enabled?.should be_false + end + + it "does not send a resource report to the server" do + @rest_client.should_not_receive(:post_rest) + @resource_reporter.run_completed(@node) + end + + end + + context "when the server returns a 500 to the client and enable_reporting_url_fatals is true" do + before do + @enable_reporting_url_fatals = Chef::Config[:enable_reporting_url_fatals] + Chef::Config[:enable_reporting_url_fatals] = true + # 500 getting the run_id + @response = Net::HTTPInternalServerError.new("a response body", "500", "Internal Server Error") + @error = Net::HTTPServerException.new("500 message", @response) + @rest_client.should_receive(:post_rest). + with("reports/nodes/spitfire/runs", {:action => :begin}). + and_raise(@error) + end + + after do + Chef::Config[:enable_reporting_url_fatals] = @enable_reporting_url_fatals + end + + it "fails the run" do + lambda { + @resource_reporter.node_load_completed(@node, :expanded_run_list, :config) + }.should raise_error(Net::HTTPServerException) + end + + end + + context "after creating the run history document" do + before do + response = {"uri"=>"https://example.com/reports/nodes/spitfire/runs/ABC123"} + @rest_client.should_receive(:post_rest). + with("reports/nodes/spitfire/runs", {:action => :begin}). + and_return(response) + + @resource_reporter.node_load_completed(@node, :expanded_run_list, :config) + end + + it "creates a run document on the server at the start of the run" do + @resource_reporter.run_id.should == "ABC123" + end + + it "updates the run document with resource updates at the end of the run" do + # update some resources... + @resource_reporter.resource_action_start(@new_resource, :create) + @resource_reporter.resource_current_state_loaded(@new_resource, :create, @current_resource) + @resource_reporter.resource_updated(@new_resource, :create) + + @expected_data = @resource_reporter.prepare_run_data + + post_url = "https://chef_server/example_url" + response = {"result"=>"ok"} + + @rest_client.should_receive(:create_url). + with("reports/nodes/spitfire/runs/ABC123"). + ordered. + and_return(post_url) + @rest_client.should_receive(:raw_http_request).ordered do |method, url, headers, data| + method.should eq(:POST) + url.should eq(post_url) + headers.should eq({'Content-Encoding' => 'gzip'}) + data_stream = Zlib::GzipReader.new(StringIO.new(data)) + data = data_stream.read + data.should eq(@expected_data.to_json) + response + end + + @resource_reporter.run_completed(@node) + end + end + + context "after creating the run history document when summary_only is set to true" do + before do + response = {"uri"=>"https://example.com/reports/nodes/spitfire/runs/ABC123", "summary_only"=>"true"} + @rest_client.should_receive(:post_rest). + with("reports/nodes/spitfire/runs", {:action => :begin}). + and_return(response) + + @resource_reporter.node_load_completed(@node, :expanded_run_list, :config) + end + + it "enables summary only reporting" do + @resource_reporter.summary_only.should == "true" + end + + it "updates the run document with resource updates at the end of the run" do + # update some resources... + @resource_reporter.resource_action_start(@new_resource, :create) + @resource_reporter.resource_current_state_loaded(@new_resource, :create, @current_resource) + @resource_reporter.resource_updated(@new_resource, :create) + + post_url = "reports/nodes/spitfire/runs/ABC123" + response = {"result"=>"ok"} + + @rest_client.should_receive(:post_rest).ordered do |url, data| + url.should eq(post_url) + data.should have_key("action") + data["action"].should == "end" + data.should have_key("status") + data.should have_key("resources") + data["resources"].should == [] + data.should have_key("total_res_count") + data["total_res_count"].should == "1" + data.should have_key("data") + data["data"].should == {} + data.should have_key("updated_res_count") + data["updated_res_count"].should == "0" + data.should have_key("post_size") + response + end + + @resource_reporter.run_completed(@node) + end + end + + context "after creating the run history document when summary_only is set to false" do + before do + response = {"uri"=>"https://example.com/reports/nodes/spitfire/runs/ABC123", "summary_only"=>"false"} + @rest_client.should_receive(:post_rest). + with("reports/nodes/spitfire/runs", {:action => :begin}). + and_return(response) + + @resource_reporter.node_load_completed(@node, :expanded_run_list, :config) + end + + it "disables summary_only reporting" do + @resource_reporter.summary_only.should == "false" + end + end + + end + +end diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb new file mode 100644 index 0000000000..71d8a4cccf --- /dev/null +++ b/spec/unit/resource_spec.rb @@ -0,0 +1,706 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Author:: Tim Hinderliter (<tim@opscode.com>) +# Author:: Seth Chisamore (<schisamo@opscode.com>) +# Copyright:: Copyright (c) 2008-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' + +class ResourceTestHarness < Chef::Resource + provider_base Chef::Provider::Package +end + +describe Chef::Resource do + before(:each) do + @cookbook_repo_path = File.join(CHEF_SPEC_DATA, 'cookbooks') + @cookbook_collection = Chef::CookbookCollection.new(Chef::CookbookLoader.new(@cookbook_repo_path)) + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, @cookbook_collection, @events) + @resource = Chef::Resource.new("funk", @run_context) + end + + describe "when declaring the identity attribute" do + it "has no identity attribute by default" do + Chef::Resource.identity_attr.should be_nil + end + + it "sets an identity attribute" do + resource_class = Class.new(Chef::Resource) + resource_class.identity_attr(:path) + resource_class.identity_attr.should == :path + end + + it "inherits an identity attribute from a superclass" do + resource_class = Class.new(Chef::Resource) + resource_subclass = Class.new(resource_class) + resource_class.identity_attr(:package_name) + resource_subclass.identity_attr.should == :package_name + end + + it "overrides the identity attribute from a superclass when the identity attr is set" do + resource_class = Class.new(Chef::Resource) + resource_subclass = Class.new(resource_class) + resource_class.identity_attr(:package_name) + resource_subclass.identity_attr(:something_else) + resource_subclass.identity_attr.should == :something_else + end + end + + describe "when no identity attribute has been declared" do + before do + @resource_sans_id = Chef::Resource.new("my-name") + end + + # Would rather force identity attributes to be set for everything, + # but that's not plausible for back compat reasons. + it "uses the name as the identity" do + @resource_sans_id.identity.should == "my-name" + end + end + + describe "when an identity attribute has been declared" do + before do + @file_resource_class = Class.new(Chef::Resource) do + identity_attr :path + attr_accessor :path + end + + @file_resource = @file_resource_class.new("identity-attr-test") + @file_resource.path = "/tmp/foo.txt" + end + + it "gives the value of its identity attribute" do + @file_resource.identity.should == "/tmp/foo.txt" + end + end + + describe "when declaring state attributes" do + it "has no state_attrs by default" do + Chef::Resource.state_attrs.should be_empty + end + + it "sets a list of state attributes" do + resource_class = Class.new(Chef::Resource) + resource_class.state_attrs(:checksum, :owner, :group, :mode) + resource_class.state_attrs.should =~ [:checksum, :owner, :group, :mode] + end + + it "inherits state attributes from the superclass" do + resource_class = Class.new(Chef::Resource) + resource_subclass = Class.new(resource_class) + resource_class.state_attrs(:checksum, :owner, :group, :mode) + resource_subclass.state_attrs.should =~ [:checksum, :owner, :group, :mode] + end + + it "combines inherited state attributes with non-inherited state attributes" do + resource_class = Class.new(Chef::Resource) + resource_subclass = Class.new(resource_class) + resource_class.state_attrs(:checksum, :owner) + resource_subclass.state_attrs(:group, :mode) + resource_subclass.state_attrs.should =~ [:checksum, :owner, :group, :mode] + end + + end + + describe "when a set of state attributes has been declared" do + before do + @file_resource_class = Class.new(Chef::Resource) do + + state_attrs :checksum, :owner, :group, :mode + + attr_accessor :checksum + attr_accessor :owner + attr_accessor :group + attr_accessor :mode + end + + @file_resource = @file_resource_class.new("describe-state-test") + @file_resource.checksum = "abc123" + @file_resource.owner = "root" + @file_resource.group = "wheel" + @file_resource.mode = "0644" + end + + it "describes its state" do + resource_state = @file_resource.state + resource_state.keys.should =~ [:checksum, :owner, :group, :mode] + resource_state[:checksum].should == "abc123" + resource_state[:owner].should == "root" + resource_state[:group].should == "wheel" + resource_state[:mode].should == "0644" + end + end + + describe "load_prior_resource" do + before(:each) do + @prior_resource = Chef::Resource.new("funk") + @prior_resource.supports(:funky => true) + @prior_resource.source_line + @prior_resource.allowed_actions << :funkytown + @prior_resource.action(:funkytown) + @resource.allowed_actions << :funkytown + @run_context.resource_collection << @prior_resource + end + + it "should load the attributes of a prior resource" do + @resource.load_prior_resource + @resource.supports.should == { :funky => true } + end + + it "should not inherit the action from the prior resource" do + @resource.load_prior_resource + @resource.action.should_not == @prior_resource.action + end + end + + describe "name" do + it "should have a name" do + @resource.name.should eql("funk") + end + + it "should let you set a new name" do + @resource.name "monkey" + @resource.name.should eql("monkey") + end + + it "should not be valid without a name" do + lambda { @resource.name false }.should raise_error(ArgumentError) + end + + it "should always have a string for name" do + lambda { @resource.name Hash.new }.should raise_error(ArgumentError) + end + end + + describe "noop" do + it "should accept true or false for noop" do + lambda { @resource.noop true }.should_not raise_error(ArgumentError) + lambda { @resource.noop false }.should_not raise_error(ArgumentError) + lambda { @resource.noop "eat it" }.should raise_error(ArgumentError) + end + end + + describe "notifies" do + it "should make notified resources appear in the actions hash" do + @run_context.resource_collection << Chef::Resource::ZenMaster.new("coffee") + @resource.notifies :reload, @run_context.resource_collection.find(:zen_master => "coffee") + @resource.delayed_notifications.detect{|e| e.resource.name == "coffee" && e.action == :reload}.should_not be_nil + end + + it "should make notified resources be capable of acting immediately" do + @run_context.resource_collection << Chef::Resource::ZenMaster.new("coffee") + @resource.notifies :reload, @run_context.resource_collection.find(:zen_master => "coffee"), :immediate + @resource.immediate_notifications.detect{|e| e.resource.name == "coffee" && e.action == :reload}.should_not be_nil + end + + it "should raise an exception if told to act in other than :delay or :immediate(ly)" do + @run_context.resource_collection << Chef::Resource::ZenMaster.new("coffee") + lambda { + @resource.notifies :reload, @run_context.resource_collection.find(:zen_master => "coffee"), :someday + }.should raise_error(ArgumentError) + end + + it "should allow multiple notified resources appear in the actions hash" do + @run_context.resource_collection << Chef::Resource::ZenMaster.new("coffee") + @resource.notifies :reload, @run_context.resource_collection.find(:zen_master => "coffee") + @resource.delayed_notifications.detect{|e| e.resource.name == "coffee" && e.action == :reload}.should_not be_nil + + @run_context.resource_collection << Chef::Resource::ZenMaster.new("beans") + @resource.notifies :reload, @run_context.resource_collection.find(:zen_master => "beans") + @resource.delayed_notifications.detect{|e| e.resource.name == "beans" && e.action == :reload}.should_not be_nil + end + + it "creates a notification for a resource that is not yet in the resource collection" do + @resource.notifies(:restart, :service => 'apache') + expected_notification = Chef::Resource::Notification.new({:service => "apache"}, :restart, @resource) + @resource.delayed_notifications.should include(expected_notification) + end + + it "notifies another resource immediately" do + @resource.notifies_immediately(:restart, :service => 'apache') + expected_notification = Chef::Resource::Notification.new({:service => "apache"}, :restart, @resource) + @resource.immediate_notifications.should include(expected_notification) + end + + it "notifies a resource to take action at the end of the chef run" do + @resource.notifies_delayed(:restart, :service => "apache") + expected_notification = Chef::Resource::Notification.new({:service => "apache"}, :restart, @resource) + @resource.delayed_notifications.should include(expected_notification) + end + end + + describe "subscribes" do + it "should make resources appear in the actions hash of subscribed nodes" do + @run_context.resource_collection << Chef::Resource::ZenMaster.new("coffee") + zr = @run_context.resource_collection.find(:zen_master => "coffee") + @resource.subscribes :reload, zr + zr.delayed_notifications.detect{|e| e.resource.name == "funk" && e.action == :reload}.should_not be_nil + end + + it "should make resources appear in the actions hash of subscribed nodes" do + @run_context.resource_collection << Chef::Resource::ZenMaster.new("coffee") + zr = @run_context.resource_collection.find(:zen_master => "coffee") + @resource.subscribes :reload, zr + zr.delayed_notifications.detect{|e| e.resource.name == @resource.name && e.action == :reload}.should_not be_nil + + @run_context.resource_collection << Chef::Resource::ZenMaster.new("bean") + zrb = @run_context.resource_collection.find(:zen_master => "bean") + zrb.subscribes :reload, zr + zr.delayed_notifications.detect{|e| e.resource.name == @resource.name && e.action == :reload}.should_not be_nil + end + + it "should make subscribed resources be capable of acting immediately" do + @run_context.resource_collection << Chef::Resource::ZenMaster.new("coffee") + zr = @run_context.resource_collection.find(:zen_master => "coffee") + @resource.subscribes :reload, zr, :immediately + zr.immediate_notifications.detect{|e| e.resource.name == @resource.name && e.action == :reload}.should_not be_nil + end + end + + describe "to_s" do + it "should become a string like resource_name[name]" do + zm = Chef::Resource::ZenMaster.new("coffee") + zm.to_s.should eql("zen_master[coffee]") + end + end + + describe "is" do + it "should return the arguments passed with 'is'" do + zm = Chef::Resource::ZenMaster.new("coffee") + zm.is("one", "two", "three").should == %w|one two three| + end + + it "should allow arguments preceeded by is to methods" do + @resource.noop(@resource.is(true)) + @resource.noop.should eql(true) + end + end + + describe "to_json" do + it "should serialize to json" do + json = @resource.to_json + json.should =~ /json_class/ + json.should =~ /instance_vars/ + end + end + + describe "to_hash" do + it "should convert to a hash" do + hash = @resource.to_hash + expected_keys = [ :allowed_actions, :params, :provider, :updated, + :updated_by_last_action, :before, :supports, + :noop, :ignore_failure, :name, :source_line, + :action, :retries, :retry_delay, :elapsed_time] + (hash.keys - expected_keys).should == [] + (expected_keys - hash.keys).should == [] + hash[:name].should eql("funk") + end + end + + describe "self.json_create" do + it "should deserialize itself from json" do + json = @resource.to_json + serialized_node = Chef::JSONCompat.from_json(json) + serialized_node.should be_a_kind_of(Chef::Resource) + serialized_node.name.should eql(@resource.name) + end + end + + describe "supports" do + it "should allow you to set what features this resource supports" do + support_hash = { :one => :two } + @resource.supports(support_hash) + @resource.supports.should eql(support_hash) + end + + it "should return the current value of supports" do + @resource.supports.should == {} + end + end + + describe "ignore_failure" do + it "should default to throwing an error if a provider fails for a resource" do + @resource.ignore_failure.should == false + end + + it "should allow you to set whether a provider should throw exceptions with ignore_failure" do + @resource.ignore_failure(true) + @resource.ignore_failure.should == true + end + + it "should allow you to epic_fail" do + @resource.epic_fail(true) + @resource.epic_fail.should == true + end + end + + describe "retries" do + it "should default to not retrying if a provider fails for a resource" do + @resource.retries.should == 0 + end + + it "should allow you to set how many retries a provider should attempt after a failure" do + @resource.retries(2) + @resource.retries.should == 2 + end + + it "should default to a retry delay of 2 seconds" do + @resource.retry_delay.should == 2 + end + + it "should allow you to set the retry delay" do + @resource.retry_delay(10) + @resource.retry_delay.should == 10 + end + end + + describe "setting the base provider class for the resource" do + + it "defaults to Chef::Provider for the base class" do + Chef::Resource.provider_base.should == Chef::Provider + end + + it "allows the base provider to be overriden by a " do + ResourceTestHarness.provider_base.should == Chef::Provider::Package + end + + end + + it "supports accessing the node via the @node instance variable [DEPRECATED]" do + @resource.instance_variable_get(:@node).inspect.should == @node.inspect + end + + it "runs an action by finding its provider, loading the current resource and then running the action" do + pending + end + + describe "when updated by a provider" do + before do + @resource.updated_by_last_action(true) + end + + it "records that it was updated" do + @resource.should be_updated + end + + it "records that the last action updated the resource" do + @resource.should be_updated_by_last_action + end + + describe "and then run again without being updated" do + before do + @resource.updated_by_last_action(false) + end + + it "reports that it is updated" do + @resource.should be_updated + end + + it "reports that it was not updated by the last action" do + @resource.should_not be_updated_by_last_action + end + + end + + end + + describe "when invoking its action" do + + before do + @resource = Chef::Resource.new("provided", @run_context) + @resource.provider = Chef::Provider::SnakeOil + @node.automatic_attrs[:platform] = "fubuntu" + @node.automatic_attrs[:platform_version] = '10.04' + end + + it "does not run only_if if no only_if command is given" do + @resource.not_if.clear + @resource.run_action(:purr) + end + + it "runs runs an only_if when one is given" do + snitch_variable = nil + @resource.only_if { snitch_variable = true } + @resource.only_if.first.positivity.should == :only_if + #Chef::Mixin::Command.should_receive(:only_if).with(true, {}).and_return(false) + @resource.run_action(:purr) + snitch_variable.should be_true + end + + it "runs multiple only_if conditionals" do + snitch_var1, snitch_var2 = nil, nil + @resource.only_if { snitch_var1 = 1 } + @resource.only_if { snitch_var2 = 2 } + @resource.run_action(:purr) + snitch_var1.should == 1 + snitch_var2.should == 2 + end + + it "accepts command options for only_if conditionals" do + Chef::Resource::Conditional.any_instance.should_receive(:evaluate_command).at_least(1).times + @resource.only_if("true", :cwd => '/tmp') + @resource.only_if.first.command_opts.should == {:cwd => '/tmp'} + @resource.run_action(:purr) + end + + it "runs not_if as a command when it is a string" do + Chef::Resource::Conditional.any_instance.should_receive(:evaluate_command).at_least(1).times + @resource.not_if "pwd" + @resource.run_action(:purr) + end + + it "runs not_if as a block when it is a ruby block" do + Chef::Resource::Conditional.any_instance.should_receive(:evaluate_block).at_least(1).times + @resource.not_if { puts 'foo' } + @resource.run_action(:purr) + end + + it "does not run not_if if no not_if command is given" do + @resource.run_action(:purr) + end + + it "accepts command options for not_if conditionals" do + @resource.not_if("pwd" , :cwd => '/tmp') + @resource.not_if.first.command_opts.should == {:cwd => '/tmp'} + end + + it "accepts multiple not_if conditionals" do + snitch_var1, snitch_var2 = true, true + @resource.not_if {snitch_var1 = nil} + @resource.not_if {snitch_var2 = false} + @resource.run_action(:purr) + snitch_var1.should be_nil + snitch_var2.should be_false + end + + end + + describe "building the platform map" do + + it 'adds mappings for a single platform' do + klz = Class.new(Chef::Resource) + Chef::Resource.platform_map.should_receive(:set).with( + :platform => :autobots, :short_name => :dinobot, :resource => klz + ) + klz.provides :dinobot, :on_platforms => ['autobots'] + end + + it 'adds mappings for multiple platforms' do + klz = Class.new(Chef::Resource) + Chef::Resource.platform_map.should_receive(:set).twice + klz.provides :energy, :on_platforms => ['autobots','decepticons'] + end + + it 'adds mappings for all platforms' do + klz = Class.new(Chef::Resource) + Chef::Resource.platform_map.should_receive(:set).with( + :short_name => :tape_deck, :resource => klz + ) + klz.provides :tape_deck + end + + end + + describe "lookups from the platform map" do + + before(:each) do + @node = Chef::Node.new + @node.name("bumblebee") + @node.automatic[:platform] = "autobots" + @node.automatic[:platform_version] = "6.1" + Object.const_set('Soundwave', Class.new(Chef::Resource)) + Object.const_set('Grimlock', Class.new(Chef::Resource){ provides :dinobot, :on_platforms => ['autobots'] }) + end + + after(:each) do + Object.send(:remove_const, :Soundwave) + Object.send(:remove_const, :Grimlock) + end + + describe "resource_for_platform" do + it 'return a resource by short_name and platform' do + Chef::Resource.resource_for_platform(:dinobot,'autobots','6.1').should eql(Grimlock) + end + it "returns a resource by short_name if nothing else matches" do + Chef::Resource.resource_for_node(:soundwave, @node).should eql(Soundwave) + end + end + + describe "resource_for_node" do + it "returns a resource by short_name and node" do + Chef::Resource.resource_for_node(:dinobot, @node).should eql(Grimlock) + end + it "returns a resource by short_name if nothing else matches" do + Chef::Resource.resource_for_node(:soundwave, @node).should eql(Soundwave) + end + end + + end +end + +describe Chef::Resource::Notification do + before do + @notification = Chef::Resource::Notification.new(:service_apache, :restart, :template_httpd_conf) + end + + it "has a resource to be notified" do + @notification.resource.should == :service_apache + end + + it "has an action to take on the service" do + @notification.action.should == :restart + end + + it "has a notifying resource" do + @notification.notifying_resource.should == :template_httpd_conf + end + + it "is a duplicate of another notification with the same target resource and action" do + other = Chef::Resource::Notification.new(:service_apache, :restart, :sync_web_app_code) + @notification.duplicates?(other).should be_true + end + + it "is not a duplicate of another notification if the actions differ" do + other = Chef::Resource::Notification.new(:service_apache, :enable, :install_apache) + @notification.duplicates?(other).should be_false + end + + it "is not a duplicate of another notification if the target resources differ" do + other = Chef::Resource::Notification.new(:service_sshd, :restart, :template_httpd_conf) + @notification.duplicates?(other).should be_false + end + + it "raises an ArgumentError if you try to check a non-ducktype object for duplication" do + lambda {@notification.duplicates?(:not_a_notification)}.should raise_error(ArgumentError) + end + + it "takes no action to resolve a resource reference that doesn't need to be resolved" do + @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat") + @notification.resource = @keyboard_cat + @long_cat = Chef::Resource::Cat.new("long_cat") + @notification.notifying_resource = @long_cat + @resource_collection = Chef::ResourceCollection.new + # would raise an error since the resource is not in the collection + @notification.resolve_resource_reference(@resource_collection) + @notification.resource.should == @keyboard_cat + end + + it "resolves a lazy reference to a resource" do + @notification.resource = {:cat => "keyboard_cat"} + @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat") + @resource_collection = Chef::ResourceCollection.new + @resource_collection << @keyboard_cat + @long_cat = Chef::Resource::Cat.new("long_cat") + @notification.notifying_resource = @long_cat + @notification.resolve_resource_reference(@resource_collection) + @notification.resource.should == @keyboard_cat + end + + it "resolves a lazy reference to its notifying resource" do + @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat") + @notification.resource = @keyboard_cat + @notification.notifying_resource = {:cat => "long_cat"} + @long_cat = Chef::Resource::Cat.new("long_cat") + @resource_collection = Chef::ResourceCollection.new + @resource_collection << @long_cat + @notification.resolve_resource_reference(@resource_collection) + @notification.notifying_resource.should == @long_cat + end + + it "resolves lazy references to both its resource and its notifying resource" do + @notification.resource = {:cat => "keyboard_cat"} + @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat") + @resource_collection = Chef::ResourceCollection.new + @resource_collection << @keyboard_cat + @notification.notifying_resource = {:cat => "long_cat"} + @long_cat = Chef::Resource::Cat.new("long_cat") + @resource_collection << @long_cat + @notification.resolve_resource_reference(@resource_collection) + @notification.resource.should == @keyboard_cat + @notification.notifying_resource.should == @long_cat + end + + it "raises a RuntimeError if you try to reference multiple resources" do + @notification.resource = {:cat => ["keyboard_cat", "cheez_cat"]} + @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat") + @cheez_cat = Chef::Resource::Cat.new("cheez_cat") + @resource_collection = Chef::ResourceCollection.new + @resource_collection << @keyboard_cat + @resource_collection << @cheez_cat + @long_cat = Chef::Resource::Cat.new("long_cat") + @notification.notifying_resource = @long_cat + lambda {@notification.resolve_resource_reference(@resource_collection)}.should raise_error(RuntimeError) + end + + it "raises a RuntimeError if you try to reference multiple notifying resources" do + @notification.notifying_resource = {:cat => ["long_cat", "cheez_cat"]} + @long_cat = Chef::Resource::Cat.new("long_cat") + @cheez_cat = Chef::Resource::Cat.new("cheez_cat") + @resource_collection = Chef::ResourceCollection.new + @resource_collection << @long_cat + @resource_collection << @cheez_cat + @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat") + @notification.resource = @keyboard_cat + lambda {@notification.resolve_resource_reference(@resource_collection)}.should raise_error(RuntimeError) + end + + it "raises a RuntimeError if it can't find a resource in the resource collection when resolving a lazy reference" do + @notification.resource = {:cat => "keyboard_cat"} + @cheez_cat = Chef::Resource::Cat.new("cheez_cat") + @resource_collection = Chef::ResourceCollection.new + @resource_collection << @cheez_cat + @long_cat = Chef::Resource::Cat.new("long_cat") + @notification.notifying_resource = @long_cat + lambda {@notification.resolve_resource_reference(@resource_collection)}.should raise_error(RuntimeError) + end + + it "raises a RuntimeError if it can't find a notifying resource in the resource collection when resolving a lazy reference" do + @notification.notifying_resource = {:cat => "long_cat"} + @cheez_cat = Chef::Resource::Cat.new("cheez_cat") + @resource_collection = Chef::ResourceCollection.new + @resource_collection << @cheez_cat + @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat") + @notification.resource = @keyboard_cat + lambda {@notification.resolve_resource_reference(@resource_collection)}.should raise_error(RuntimeError) + end + + it "raises an ArgumentError if improper syntax is used in the lazy reference to its resource" do + @notification.resource = "cat => keyboard_cat" + @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat") + @resource_collection = Chef::ResourceCollection.new + @resource_collection << @keyboard_cat + @long_cat = Chef::Resource::Cat.new("long_cat") + @notification.notifying_resource = @long_cat + lambda {@notification.resolve_resource_reference(@resource_collection)}.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if improper syntax is used in the lazy reference to its notifying resource" do + @notification.notifying_resource = "cat => long_cat" + @long_cat = Chef::Resource::Cat.new("long_cat") + @resource_collection = Chef::ResourceCollection.new + @resource_collection << @long_cat + @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat") + @notification.resource = @keyboard_cat + lambda {@notification.resolve_resource_reference(@resource_collection)}.should raise_error(ArgumentError) + end + + # Create test to resolve lazy references to both notifying resource and dest. resource + # Create tests to check proper error raising + +end diff --git a/spec/unit/rest/auth_credentials_spec.rb b/spec/unit/rest/auth_credentials_spec.rb new file mode 100644 index 0000000000..d4e533919f --- /dev/null +++ b/spec/unit/rest/auth_credentials_spec.rb @@ -0,0 +1,419 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# 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 'uri' +require 'net/https' + +KEY_DOT_PEM=<<-END_RSA_KEY +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA49TA0y81ps0zxkOpmf5V4/c4IeR5yVyQFpX3JpxO4TquwnRh +8VSUhrw8kkTLmB3cS39Db+3HadvhoqCEbqPE6915kXSuk/cWIcNozujLK7tkuPEy +YVsyTioQAddSdfe+8EhQVf3oHxaKmUd6waXrWqYCnhxgOjxocenREYNhZ/OETIei +PbOku47vB4nJK/0GhKBytL2XnsRgfKgDxf42BqAi1jglIdeq8lAWZNF9TbNBU21A +O1iuT7Pm6LyQujhggPznR5FJhXKRUARXBJZawxpGV4dGtdcahwXNE4601aXPra+x +PcRd2puCNoEDBzgVuTSsLYeKBDMSfs173W1QYwIDAQABAoIBAGF05q7vqOGbMaSD +2Q7YbuE/JTHKTBZIlBI1QC2x+0P5GDxyEFttNMOVzcs7xmNhkpRw8eX1LrInrpMk +WsIBKAFFEfWYlf0RWtRChJjNl+szE9jQxB5FJnWtJH/FHa78tR6PsF24aQyzVcJP +g0FGujBihwgfV0JSCNOBkz8MliQihjQA2i8PGGmo4R4RVzGfxYKTIq9vvRq/+QEa +Q4lpVLoBqnENpnY/9PTl6JMMjW2b0spbLjOPVwDaIzXJ0dChjNXo15K5SHI5mALJ +I5gN7ODGb8PKUf4619ez194FXq+eob5YJdilTFKensIUvt3YhP1ilGMM+Chi5Vi/ +/RCTw3ECgYEA9jTw4wv9pCswZ9wbzTaBj9yZS3YXspGg26y6Ohq3ZmvHz4jlT6uR +xK+DDcUiK4072gci8S4Np0fIVS7q6ivqcOdzXPrTF5/j+MufS32UrBbUTPiM1yoO +ECcy+1szl/KoLEV09bghPbvC58PFSXV71evkaTETYnA/F6RK12lEepcCgYEA7OSy +bsMrGDVU/MKJtwqyGP9ubA53BorM4Pp9VVVSCrGGVhb9G/XNsjO5wJC8J30QAo4A +s59ZzCpyNRy046AB8jwRQuSwEQbejSdeNgQGXhZ7aIVUtuDeFFdaIz/zjVgxsfj4 +DPOuzieMmJ2MLR4F71ocboxNoDI7xruPSE8dDhUCgYA3vx732cQxgtHwAkeNPJUz +dLiE/JU7CnxIoSB9fYUfPLI+THnXgzp7NV5QJN2qzMzLfigsQcg3oyo6F2h7Yzwv +GkjlualIRRzCPaCw4Btkp7qkPvbs1QngIHALt8fD1N69P3DPHkTwjG4COjKWgnJq +qoHKS6Fe/ZlbigikI6KsuwKBgQCTlSLoyGRHr6oj0hqz01EDK9ciMJzMkZp0Kvn8 +OKxlBxYW+jlzut4MQBdgNYtS2qInxUoAnaz2+hauqhSzntK3k955GznpUatCqx0R +b857vWviwPX2/P6+E3GPdl8IVsKXCvGWOBZWTuNTjQtwbDzsUepWoMgXnlQJSn5I +YSlLxQKBgQD16Gw9kajpKlzsPa6XoQeGmZALT6aKWJQlrKtUQIrsIWM0Z6eFtX12 +2jjHZ0awuCQ4ldqwl8IfRogWMBkHOXjTPVK0YKWWlxMpD/5+bGPARa5fir8O1Zpo +Y6S6MeZ69Rp89ma4ttMZ+kwi1+XyHqC/dlcVRW42Zl5Dc7BALRlJjQ== +-----END RSA PRIVATE KEY----- + END_RSA_KEY + + +describe Chef::REST::AuthCredentials do + before do + @key_file_fixture = CHEF_SPEC_DATA + '/ssl/private_key.pem' + @key = OpenSSL::PKey::RSA.new(IO.read(@key_file_fixture).strip) + @auth_credentials = Chef::REST::AuthCredentials.new("client-name", @key) + end + + it "has a client name" do + @auth_credentials.client_name.should == "client-name" + end + + it "loads the private key when initialized with the path to the key" do + @auth_credentials.key.should respond_to(:private_encrypt) + @auth_credentials.key.to_s.should == KEY_DOT_PEM + end + + describe "when loading the private key" do + it "strips extra whitespace before checking the key" do + key_file_fixture = CHEF_SPEC_DATA + '/ssl/private_key_with_whitespace.pem' + lambda {Chef::REST::AuthCredentials.new("client-name", @key_file_fixture)}.should_not raise_error + end + end + + describe "generating signature headers for a request" do + before do + @request_time = Time.at(1270920860) + @request_params = {:http_method => :POST, :path => "/clients", :body => '{"some":"json"}', :host => "localhost"} + end + + it "generates signature headers for the request" do + Time.stub!(:now).and_return(@request_time) + actual = @auth_credentials.signature_headers(@request_params) + actual["HOST"].should == "localhost" + actual["X-OPS-AUTHORIZATION-1"].should == "kBssX1ENEwKtNYFrHElN9vYGWS7OeowepN9EsYc9csWfh8oUovryPKDxytQ/" + actual["X-OPS-AUTHORIZATION-2"].should == "Wc2/nSSyxdWJjjfHzrE+YrqNQTaArOA7JkAf5p75eTUonCWcvNPjFrZVgKGS" + actual["X-OPS-AUTHORIZATION-3"].should == "yhzHJQh+lcVA9wwARg5Hu9q+ddS8xBOdm3Vp5atl5NGHiP0loiigMYvAvzPO" + actual["X-OPS-AUTHORIZATION-4"].should == "r9853eIxwYMhn5hLGhAGFQznJbE8+7F/lLU5Zmk2t2MlPY8q3o1Q61YD8QiJ" + actual["X-OPS-AUTHORIZATION-5"].should == "M8lIt53ckMyUmSU0DDURoiXLVkE9mag/6Yq2tPNzWq2AdFvBqku9h2w+DY5k" + actual["X-OPS-AUTHORIZATION-6"].should == "qA5Rnzw5rPpp3nrWA9jKkPw4Wq3+4ufO2Xs6w7GCjA==" + actual["X-OPS-CONTENT-HASH"].should == "1tuzs5XKztM1ANrkGNPah6rW9GY=" + actual["X-OPS-SIGN"].should =~ %r{(version=1\.0)|(algorithm=sha1;version=1.0;)} + actual["X-OPS-TIMESTAMP"].should == "2010-04-10T17:34:20Z" + actual["X-OPS-USERID"].should == "client-name" + + end + + describe "when configured for version 1.1 of the authn protocol" do + before do + Chef::Config[:authentication_protocol_version] = "1.1" + end + + after do + Chef::Config[:authentication_protocol_version] = "1.0" + end + + it "generates the correct signature for version 1.1" do + Time.stub!(:now).and_return(@request_time) + actual = @auth_credentials.signature_headers(@request_params) + actual["HOST"].should == "localhost" + actual["X-OPS-CONTENT-HASH"].should == "1tuzs5XKztM1ANrkGNPah6rW9GY=" + actual["X-OPS-SIGN"].should == "algorithm=sha1;version=1.1;" + actual["X-OPS-TIMESTAMP"].should == "2010-04-10T17:34:20Z" + actual["X-OPS-USERID"].should == "client-name" + + # mixlib-authN will test the actual signature stuff for each version of + # the protocol so we won't test it again here. + end + end + end +end + +describe Chef::REST::RESTRequest do + def new_request(method=nil) + method ||= :POST + Chef::REST::RESTRequest.new(method, @url, @req_body, @headers) + end + + before do + @auth_credentials = Chef::REST::AuthCredentials.new("client-name", CHEF_SPEC_DATA + '/ssl/private_key.pem') + @url = URI.parse("http://chef.example.com:4000/?q=chef_is_awesome") + @req_body = '{"json_data":"as_a_string"}' + @headers = {"Content-type" =>"application/json", "Accept"=>"application/json", "Accept-Encoding" => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE} + @request = Chef::REST::RESTRequest.new(:POST, @url, @req_body, @headers) + end + + it "stores the url it was created with" do + @request.url.should == @url + end + + it "stores the HTTP method" do + @request.method.should == :POST + end + + it "adds the chef version header" do + @request.headers.should == @headers.merge("X-Chef-Version" => ::Chef::VERSION) + end + + describe "configuring the HTTP request" do + it "configures GET requests" do + @req_body = nil + rest_req = new_request(:GET) + rest_req.http_request.should be_a_kind_of(Net::HTTP::Get) + rest_req.http_request.path.should == "/?q=chef_is_awesome" + rest_req.http_request.body.should be_nil + end + + it "configures POST requests, including the body" do + @request.http_request.should be_a_kind_of(Net::HTTP::Post) + @request.http_request.path.should == "/?q=chef_is_awesome" + @request.http_request.body.should == @req_body + end + + it "configures PUT requests, including the body" do + rest_req = new_request(:PUT) + rest_req.http_request.should be_a_kind_of(Net::HTTP::Put) + rest_req.http_request.path.should == "/?q=chef_is_awesome" + rest_req.http_request.body.should == @req_body + end + + it "configures DELETE requests" do + rest_req = new_request(:DELETE) + rest_req.http_request.should be_a_kind_of(Net::HTTP::Delete) + rest_req.http_request.path.should == "/?q=chef_is_awesome" + rest_req.http_request.body.should be_nil + end + + it "configures HTTP basic auth" do + @url = URI.parse("http://homie:theclown@chef.example.com:4000/?q=chef_is_awesome") + rest_req = new_request(:GET) + rest_req.http_request.to_hash["authorization"].should == ["Basic aG9taWU6dGhlY2xvd24="] + end + end + + describe "configuring the HTTP client" do + it "configures the HTTP client for the host and port" do + http_client = new_request.http_client + http_client.address.should == "chef.example.com" + http_client.port.should == 4000 + end + + it "configures the HTTP client with the read timeout set in the config file" do + Chef::Config[:rest_timeout] = 9001 + new_request.http_client.read_timeout.should == 9001 + end + + describe "for SSL" do + before do + Chef::Config[:ssl_client_cert] = nil + Chef::Config[:ssl_client_key] = nil + Chef::Config[:ssl_ca_path] = nil + Chef::Config[:ssl_ca_file] = nil + end + + after do + Chef::Config[:ssl_client_cert] = nil + Chef::Config[:ssl_client_key] = nil + Chef::Config[:ssl_ca_path] = nil + Chef::Config[:ssl_verify_mode] = :verify_none + Chef::Config[:ssl_ca_file] = nil + end + + describe "when configured with :ssl_verify_mode set to :verify peer" do + before do + @url = URI.parse("https://chef.example.com:4443/") + Chef::Config[:ssl_verify_mode] = :verify_peer + @request = new_request + end + + it "configures the HTTP client to use SSL when given a URL with the https protocol" do + @request.http_client.use_ssl?.should be_true + end + + it "sets the OpenSSL verify mode to verify_peer" do + @request.http_client.verify_mode.should == OpenSSL::SSL::VERIFY_PEER + end + + it "raises a ConfigurationError if :ssl_ca_path is set to a path that doesn't exist" do + Chef::Config[:ssl_ca_path] = "/dev/null/nothing_here" + lambda {new_request}.should raise_error(Chef::Exceptions::ConfigurationError) + end + + it "should set the CA path if that is set in the configuration" do + Chef::Config[:ssl_ca_path] = File.join(CHEF_SPEC_DATA, "ssl") + new_request.http_client.ca_path.should == File.join(CHEF_SPEC_DATA, "ssl") + end + + it "raises a ConfigurationError if :ssl_ca_file is set to a file that does not exist" do + Chef::Config[:ssl_ca_file] = "/dev/null/nothing_here" + lambda {new_request}.should raise_error(Chef::Exceptions::ConfigurationError) + end + + it "should set the CA file if that is set in the configuration" do + Chef::Config[:ssl_ca_file] = CHEF_SPEC_DATA + '/ssl/5e707473.0' + new_request.http_client.ca_file.should == CHEF_SPEC_DATA + '/ssl/5e707473.0' + end + end + + describe "when configured with :ssl_verify_mode set to :verify peer" do + before do + @url = URI.parse("https://chef.example.com:4443/") + Chef::Config[:ssl_verify_mode] = :verify_none + end + + it "sets the OpenSSL verify mode to :verify_none" do + new_request.http_client.verify_mode.should == OpenSSL::SSL::VERIFY_NONE + end + end + + describe "when configured with a client certificate" do + before {@url = URI.parse("https://chef.example.com:4443/")} + + it "raises ConfigurationError if the certificate file doesn't exist" do + Chef::Config[:ssl_client_cert] = "/dev/null/nothing_here" + Chef::Config[:ssl_client_key] = CHEF_SPEC_DATA + '/ssl/chef-rspec.key' + lambda {new_request}.should raise_error(Chef::Exceptions::ConfigurationError) + end + + it "raises ConfigurationError if the certificate file doesn't exist" do + Chef::Config[:ssl_client_cert] = CHEF_SPEC_DATA + '/ssl/chef-rspec.cert' + Chef::Config[:ssl_client_key] = "/dev/null/nothing_here" + lambda {new_request}.should raise_error(Chef::Exceptions::ConfigurationError) + end + + it "raises a ConfigurationError if one of :ssl_client_cert and :ssl_client_key is set but not both" do + Chef::Config[:ssl_client_cert] = "/dev/null/nothing_here" + Chef::Config[:ssl_client_key] = nil + lambda {new_request}.should raise_error(Chef::Exceptions::ConfigurationError) + end + + it "configures the HTTP client's cert and private key" do + Chef::Config[:ssl_client_cert] = CHEF_SPEC_DATA + '/ssl/chef-rspec.cert' + Chef::Config[:ssl_client_key] = CHEF_SPEC_DATA + '/ssl/chef-rspec.key' + http_client = new_request.http_client + http_client.cert.to_s.should == OpenSSL::X509::Certificate.new(IO.read(CHEF_SPEC_DATA + '/ssl/chef-rspec.cert')).to_s + http_client.key.to_s.should == IO.read(CHEF_SPEC_DATA + '/ssl/chef-rspec.key') + end + end + end + + describe "for proxy" do + before do + Chef::Config[:http_proxy] = "http://proxy.example.com:3128" + Chef::Config[:https_proxy] = "http://sproxy.example.com:3129" + Chef::Config[:http_proxy_user] = nil + Chef::Config[:https_proxy_pass] = nil + Chef::Config[:https_proxy_user] = nil + Chef::Config[:https_proxy_pass] = nil + Chef::Config[:no_proxy] = nil + end + + after do + Chef::Config[:http_proxy] = nil + Chef::Config[:https_proxy] = nil + Chef::Config[:http_proxy_user] = nil + Chef::Config[:https_proxy_pass] = nil + Chef::Config[:https_proxy_user] = nil + Chef::Config[:https_proxy_pass] = nil + Chef::Config[:no_proxy] = nil + end + + describe "with :no_proxy nil" do + it "configures the proxy address and port when using http scheme" do + http_client = new_request.http_client + http_client.proxy?.should == true + http_client.proxy_address.should == "proxy.example.com" + http_client.proxy_port.should == 3128 + http_client.proxy_user.should be_nil + http_client.proxy_pass.should be_nil + end + + it "configures the proxy address and port when using https scheme" do + @url.scheme = "https" + http_client = new_request.http_client + http_client.proxy?.should == true + http_client.proxy_address.should == "sproxy.example.com" + http_client.proxy_port.should == 3129 + http_client.proxy_user.should be_nil + http_client.proxy_pass.should be_nil + end + end + + describe "with :no_proxy set" do + before do + Chef::Config[:no_proxy] = "10.*,*.example.com" + end + + it "does not configure the proxy address and port when using http scheme" do + http_client = new_request.http_client + http_client.proxy?.should == false + http_client.proxy_address.should be_nil + http_client.proxy_port.should be_nil + http_client.proxy_user.should be_nil + http_client.proxy_pass.should be_nil + end + + it "does not configure the proxy address and port when using https scheme" do + @url.scheme = "https" + http_client = new_request.http_client + http_client.proxy?.should == false + http_client.proxy_address.should be_nil + http_client.proxy_port.should be_nil + http_client.proxy_user.should be_nil + http_client.proxy_pass.should be_nil + end + end + + describe "with :http_proxy_user and :http_proxy_pass set" do + before do + Chef::Config[:http_proxy_user] = "homie" + Chef::Config[:http_proxy_pass] = "theclown" + end + + after do + Chef::Config[:http_proxy_user] = nil + Chef::Config[:http_proxy_pass] = nil + end + + it "configures the proxy user and pass when using http scheme" do + http_client = new_request.http_client + http_client.proxy?.should == true + http_client.proxy_user.should == "homie" + http_client.proxy_pass.should == "theclown" + end + + it "does not configure the proxy user and pass when using https scheme" do + @url.scheme = "https" + http_client = new_request.http_client + http_client.proxy?.should == true + http_client.proxy_user.should be_nil + http_client.proxy_pass.should be_nil + end + end + + describe "with :https_proxy_user and :https_proxy_pass set" do + before do + Chef::Config[:https_proxy_user] = "homie" + Chef::Config[:https_proxy_pass] = "theclown" + end + + after do + Chef::Config[:https_proxy_user] = nil + Chef::Config[:https_proxy_pass] = nil + end + + it "does not configure the proxy user and pass when using http scheme" do + http_client = new_request.http_client + http_client.proxy?.should == true + http_client.proxy_user.should be_nil + http_client.proxy_pass.should be_nil + end + + it "configures the proxy user and pass when using https scheme" do + @url.scheme = "https" + http_client = new_request.http_client + http_client.proxy?.should == true + http_client.proxy_user.should == "homie" + http_client.proxy_pass.should == "theclown" + end + end + end + end + +end diff --git a/spec/unit/rest_spec.rb b/spec/unit/rest_spec.rb new file mode 100644 index 0000000000..5eacf89eed --- /dev/null +++ b/spec/unit/rest_spec.rb @@ -0,0 +1,661 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# 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 'uri' +require 'net/https' +require 'stringio' + +SIGNING_KEY_DOT_PEM="-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA49TA0y81ps0zxkOpmf5V4/c4IeR5yVyQFpX3JpxO4TquwnRh +8VSUhrw8kkTLmB3cS39Db+3HadvhoqCEbqPE6915kXSuk/cWIcNozujLK7tkuPEy +YVsyTioQAddSdfe+8EhQVf3oHxaKmUd6waXrWqYCnhxgOjxocenREYNhZ/OETIei +PbOku47vB4nJK/0GhKBytL2XnsRgfKgDxf42BqAi1jglIdeq8lAWZNF9TbNBU21A +O1iuT7Pm6LyQujhggPznR5FJhXKRUARXBJZawxpGV4dGtdcahwXNE4601aXPra+x +PcRd2puCNoEDBzgVuTSsLYeKBDMSfs173W1QYwIDAQABAoIBAGF05q7vqOGbMaSD +2Q7YbuE/JTHKTBZIlBI1QC2x+0P5GDxyEFttNMOVzcs7xmNhkpRw8eX1LrInrpMk +WsIBKAFFEfWYlf0RWtRChJjNl+szE9jQxB5FJnWtJH/FHa78tR6PsF24aQyzVcJP +g0FGujBihwgfV0JSCNOBkz8MliQihjQA2i8PGGmo4R4RVzGfxYKTIq9vvRq/+QEa +Q4lpVLoBqnENpnY/9PTl6JMMjW2b0spbLjOPVwDaIzXJ0dChjNXo15K5SHI5mALJ +I5gN7ODGb8PKUf4619ez194FXq+eob5YJdilTFKensIUvt3YhP1ilGMM+Chi5Vi/ +/RCTw3ECgYEA9jTw4wv9pCswZ9wbzTaBj9yZS3YXspGg26y6Ohq3ZmvHz4jlT6uR +xK+DDcUiK4072gci8S4Np0fIVS7q6ivqcOdzXPrTF5/j+MufS32UrBbUTPiM1yoO +ECcy+1szl/KoLEV09bghPbvC58PFSXV71evkaTETYnA/F6RK12lEepcCgYEA7OSy +bsMrGDVU/MKJtwqyGP9ubA53BorM4Pp9VVVSCrGGVhb9G/XNsjO5wJC8J30QAo4A +s59ZzCpyNRy046AB8jwRQuSwEQbejSdeNgQGXhZ7aIVUtuDeFFdaIz/zjVgxsfj4 +DPOuzieMmJ2MLR4F71ocboxNoDI7xruPSE8dDhUCgYA3vx732cQxgtHwAkeNPJUz +dLiE/JU7CnxIoSB9fYUfPLI+THnXgzp7NV5QJN2qzMzLfigsQcg3oyo6F2h7Yzwv +GkjlualIRRzCPaCw4Btkp7qkPvbs1QngIHALt8fD1N69P3DPHkTwjG4COjKWgnJq +qoHKS6Fe/ZlbigikI6KsuwKBgQCTlSLoyGRHr6oj0hqz01EDK9ciMJzMkZp0Kvn8 +OKxlBxYW+jlzut4MQBdgNYtS2qInxUoAnaz2+hauqhSzntK3k955GznpUatCqx0R +b857vWviwPX2/P6+E3GPdl8IVsKXCvGWOBZWTuNTjQtwbDzsUepWoMgXnlQJSn5I +YSlLxQKBgQD16Gw9kajpKlzsPa6XoQeGmZALT6aKWJQlrKtUQIrsIWM0Z6eFtX12 +2jjHZ0awuCQ4ldqwl8IfRogWMBkHOXjTPVK0YKWWlxMpD/5+bGPARa5fir8O1Zpo +Y6S6MeZ69Rp89ma4ttMZ+kwi1+XyHqC/dlcVRW42Zl5Dc7BALRlJjQ== +-----END RSA PRIVATE KEY-----" + +describe Chef::REST do + before(:each) do + @log_stringio = StringIO.new + Chef::Log.init(@log_stringio) + + Chef::REST::CookieJar.stub!(:instance).and_return({}) + @base_url = "http://chef.example.com:4000" + @monkey_uri = URI.parse("http://chef.example.com:4000/monkey") + @rest = Chef::REST.new(@base_url, nil, nil) + + Chef::REST::CookieJar.instance.clear + end + + + describe "calling an HTTP verb on a path or absolute URL" do + it "adds a relative URL to the base url it was initialized with" do + @rest.create_url("foo/bar/baz").should == URI.parse(@base_url + "/foo/bar/baz") + end + + it "replaces the base URL when given an absolute URL" do + @rest.create_url("http://chef-rulez.example.com:9000").should == URI.parse("http://chef-rulez.example.com:9000") + end + + it "makes a :GET request with the composed url object" do + @rest.should_receive(:api_request).with(:GET, @monkey_uri, {}) + @rest.get_rest("monkey") + end + + it "makes a :GET reqest for a streaming download with the composed url" do + @rest.should_receive(:streaming_request).with(@monkey_uri, {}) + @rest.get_rest("monkey", true) + end + + it "makes a :DELETE request with the composed url object" do + @rest.should_receive(:api_request).with(:DELETE, @monkey_uri, {}) + @rest.delete_rest("monkey") + end + + it "makes a :POST request with the composed url object and data" do + @rest.should_receive(:api_request).with(:POST, @monkey_uri, {}, "data") + @rest.post_rest("monkey", "data") + end + + it "makes a :PUT request with the composed url object and data" do + @rest.should_receive(:api_request).with(:PUT, @monkey_uri, {}, "data") + @rest.put_rest("monkey", "data") + end + end + + + describe "when configured to authenticate to the Chef server" do + before do + @url = URI.parse("http://chef.example.com:4000") + Chef::Config[:node_name] = "webmonkey.example.com" + Chef::Config[:client_key] = CHEF_SPEC_DATA + "/ssl/private_key.pem" + @rest = Chef::REST.new(@url) + end + + it "configures itself to use the node_name and client_key in the config by default" do + @rest.client_name.should == "webmonkey.example.com" + @rest.signing_key_filename.should == CHEF_SPEC_DATA + "/ssl/private_key.pem" + end + + it "provides access to the raw key data" do + @rest.signing_key.should == SIGNING_KEY_DOT_PEM + end + + it "does not error out when initialized without credentials" do + @rest = Chef::REST.new(@url, nil, nil) #should_not raise_error hides the bt from you, so screw it. + @rest.client_name.should be_nil + @rest.signing_key.should be_nil + end + + it "indicates that requests should not be signed when it has no credentials" do + @rest = Chef::REST.new(@url, nil, nil) + @rest.sign_requests?.should be_false + end + + it "raises PrivateKeyMissing when the key file doesn't exist" do + lambda {Chef::REST.new(@url, "client-name", "/dev/null/nothing_here")}.should raise_error(Chef::Exceptions::PrivateKeyMissing) + end + + it "raises InvalidPrivateKey when the key file doesnt' look like a key" do + invalid_key_file = CHEF_SPEC_DATA + "/bad-config.rb" + lambda {Chef::REST.new(@url, "client-name", invalid_key_file)}.should raise_error(Chef::Exceptions::InvalidPrivateKey) + end + + it "can take private key as a sting :raw_key in options during initializaton" do + Chef::REST.new(@url, "client-name", nil, :raw_key => SIGNING_KEY_DOT_PEM).signing_key.should == SIGNING_KEY_DOT_PEM + end + + it "raises InvalidPrivateKey when the key passed as string :raw_key in options doesnt' look like a key" do + lambda {Chef::REST.new(@url, "client-name", nil, :raw_key => "bad key string")}.should raise_error(Chef::Exceptions::InvalidPrivateKey) + end + + end + + context "when making REST requests" do + before(:each) do + Chef::Config[:ssl_client_cert] = nil + Chef::Config[:ssl_client_key] = nil + @url = URI.parse("https://one:80/?foo=bar") + + @http_response = Net::HTTPSuccess.new("1.1", "200", "successful rest req") + @http_response.stub!(:read_body) + @http_response.stub!(:body).and_return("ninja") + @http_response.add_field("Content-Length", "5") + + @http_client = Net::HTTP.new(@url.host, @url.port) + Net::HTTP.stub!(:new).and_return(@http_client) + @http_client.stub!(:request).and_yield(@http_response).and_return(@http_response) + + @base_headers = { 'Accept' => 'application/json', + 'X-Chef-Version' => Chef::VERSION, + 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE} + @req_with_body_headers = @base_headers.merge("Content-Type" => "application/json", "Content-Length" => '13') + end + + describe "using the run_request API" do + it "should build a new HTTP GET request" do + request = Net::HTTP::Get.new(@url.path) + Net::HTTP::Get.should_receive(:new).with("/?foo=bar", @base_headers).and_return(request) + @rest.run_request(:GET, @url, {}) + end + + it "should build a new HTTP POST request" do + request = Net::HTTP::Post.new(@url.path) + + Net::HTTP::Post.should_receive(:new).with("/?foo=bar", @req_with_body_headers).and_return(request) + @rest.run_request(:POST, @url, {}, {:one=>:two}) + request.body.should == '{"one":"two"}' + end + + it "should build a new HTTP PUT request" do + request = Net::HTTP::Put.new(@url.path) + expected_headers = @base_headers.merge("Content-Length" => '13') + Net::HTTP::Put.should_receive(:new).with("/?foo=bar", @req_with_body_headers).and_return(request) + @rest.run_request(:PUT, @url, {}, {:one=>:two}) + request.body.should == '{"one":"two"}' + end + + it "should build a new HTTP DELETE request" do + request = Net::HTTP::Delete.new(@url.path) + Net::HTTP::Delete.should_receive(:new).with("/?foo=bar", @base_headers).and_return(request) + @rest.run_request(:DELETE, @url) + end + + it "should raise an error if the method is not GET/PUT/POST/DELETE" do + lambda { @rest.api_request(:MONKEY, @url) }.should raise_error(ArgumentError) + end + + it "returns the response body when the response is successful but content-type is not JSON" do + @rest.run_request(:GET, @url).should == "ninja" + end + + it "should call read_body without a block if the request is not raw" do + @http_response.should_receive(:body) + @rest.run_request(:GET, @url, {}, nil, false) + end + + it "should inflate the body as to an object if JSON is returned" do + @http_response.add_field("content-type", "application/json") + Chef::JSONCompat.should_receive(:from_json).with("ninja").and_return("ohai2u_success") + @rest.run_request(:GET, @url, {}).should == "ohai2u_success" + end + + it "should return false on a Not Modified response" do + http_response = Net::HTTPNotModified.new("1.1", "304", "It's old Bob") + @http_client.stub!(:request).and_yield(http_response).and_return(http_response) + http_response.stub!(:read_body) + @rest.run_request(:GET, @url).should be_false + end + + %w[ HTTPFound HTTPMovedPermanently HTTPSeeOther HTTPUseProxy HTTPTemporaryRedirect HTTPMultipleChoice ].each do |resp_name| + it "should call run_request again on a #{resp_name} response" do + resp_cls = Net.const_get(resp_name) + resp_code = Net::HTTPResponse::CODE_TO_OBJ.keys.detect { |k| Net::HTTPResponse::CODE_TO_OBJ[k] == resp_cls } + http_response = resp_cls.new("1.1", resp_code, "bob somewhere else") + http_response.add_field("location", @url.path) + http_response.stub!(:read_body) + + @http_client.stub!(:request).and_yield(http_response).and_return(http_response) + lambda { @rest.run_request(:GET, @url) }.should raise_error(Chef::Exceptions::RedirectLimitExceeded) + end + end + + # CHEF-3140 + context "when configured to disable compression" do + before do + @rest = Chef::REST.new(@base_url, nil, nil, :disable_gzip => true) + end + + it "does not accept encoding gzip" do + @rest.send(:build_headers, :GET, @url, {}).should_not have_key("Accept-Encoding") + end + + it "does not decompress a response encoded as gzip" do + @http_response.add_field("content-encoding", "gzip") + request = Net::HTTP::Get.new(@url.path) + Net::HTTP::Get.should_receive(:new).and_return(request) + # will raise a Zlib error if incorrect + @rest.api_request(:GET, @url, {}).should == "ninja" + end + end + + it "should show the JSON error message on an unsuccessful request" do + http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth") + http_response.add_field("content-type", "application/json") + http_response.stub!(:body).and_return('{ "error":[ "Ears get sore!", "Not even four" ] }') + http_response.stub!(:read_body) + @http_client.stub!(:request).and_yield(http_response).and_return(http_response) + @rest.stub!(:sleep) + lambda {@rest.run_request(:GET, @url)}.should raise_error(Net::HTTPFatalError) + @log_stringio.string.should match(Regexp.escape('WARN: HTTP Request Returned 500 drooling from inside of mouth: Ears get sore!, Not even four')) + end + + it "should raise an exception on an unsuccessful request" do + @http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth") + http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth") + http_response.stub!(:read_body) + @rest.stub!(:sleep) + @http_client.stub!(:request).and_yield(http_response).and_return(http_response) + lambda {@rest.run_request(:GET, @url)}.should raise_error(Net::HTTPFatalError) + end + + it "adds the rest_request object to any http exception raised" do + @http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth") + http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth") + http_response.stub!(:read_body) + @rest.stub!(:sleep) + @http_client.stub!(:request).and_yield(http_response).and_return(http_response) + exception = begin + @rest.api_request(:GET, @url, {}) + rescue => e + e + end + + e.chef_rest_request.url.should == @url + e.chef_rest_request.method.should == :GET + end + + describe "streaming downloads to a tempfile" do + before do + @tempfile = Tempfile.open("chef-rspec-rest_spec-line-#{__LINE__}--") + Tempfile.stub!(:new).with("chef-rest").and_return(@tempfile) + Tempfile.stub!(:open).and_return(@tempfile) + + @request_mock = {} + Net::HTTP::Get.stub!(:new).and_return(@request_mock) + + @http_response_mock = mock("Net::HTTP Response mock") + end + + after do + @tempfile.rspec_reset + @tempfile.close! + end + + it "should build a new HTTP GET request without the application/json accept header" do + expected_headers = {'X-Chef-Version' => Chef::VERSION, 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE} + Net::HTTP::Get.should_receive(:new).with("/?foo=bar", expected_headers).and_return(@request_mock) + @rest.run_request(:GET, @url, {}, false, nil, true) + end + + it "should create a tempfile for the output of a raw request" do + @rest.run_request(:GET, @url, {}, false, nil, true).should equal(@tempfile) + end + + it "should read the body of the response in chunks on a raw request" do + @http_response.should_receive(:read_body).and_return(true) + @rest.run_request(:GET, @url, {}, false, nil, true) + end + + it "should populate the tempfile with the value of the raw request" do + @http_response_mock.stub!(:read_body).and_yield("ninja") + @tempfile.should_receive(:write).with("ninja").once.and_return(true) + @rest.run_request(:GET, @url, {}, false, nil, true) + end + + it "should close the tempfile if we're doing a raw request" do + @tempfile.should_receive(:close).once.and_return(true) + @rest.run_request(:GET, @url, {}, false, nil, true) + end + + it "should not raise a divide by zero exception if the size is 0" do + @http_response_mock.stub!(:header).and_return({ 'Content-Length' => "5" }) + @http_response_mock.stub!(:read_body).and_yield('') + lambda { @rest.run_request(:GET, @url, {}, false, nil, true) }.should_not raise_error(ZeroDivisionError) + end + + it "should not raise a divide by zero exception if the Content-Length is 0" do + @http_response_mock.stub!(:header).and_return({ 'Content-Length' => "0" }) + @http_response_mock.stub!(:read_body).and_yield("ninja") + lambda { @rest.run_request(:GET, @url, {}, false, nil, true) }.should_not raise_error(ZeroDivisionError) + end + + end + + end + + describe "as JSON API requests" do + before do + @request_mock = {} + Net::HTTP::Get.stub!(:new).and_return(@request_mock) + + @base_headers = {"Accept" => "application/json", + "X-Chef-Version" => Chef::VERSION, + "Accept-Encoding" => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE + } + end + + it "should always include the X-Chef-Version header" do + Net::HTTP::Get.should_receive(:new).with("/?foo=bar", @base_headers).and_return(@request_mock) + @rest.api_request(:GET, @url, {}) + end + + it "sets the user agent to chef-client" do + # must reset to default b/c knife changes the UA + Chef::REST::RESTRequest.user_agent = Chef::REST::RESTRequest::DEFAULT_UA + @rest.api_request(:GET, @url, {}) + @request_mock['User-Agent'].should match /^Chef Client\/#{Chef::VERSION}/ + end + + context "when configured with custom http headers" do + before(:each) do + @custom_headers = { + 'X-Custom-ChefSecret' => 'sharpknives', + 'X-Custom-RequestPriority' => 'extremely low' + } + Chef::Config[:custom_http_headers] = @custom_headers + end + + after(:each) do + Chef::Config[:custom_http_headers] = nil + end + + it "should set them on the http request" do + url_string = an_instance_of(String) + header_hash = hash_including(@custom_headers) + Net::HTTP::Get.should_receive(:new).with(url_string, header_hash) + @rest.api_request(:GET, @url, {}) + end + end + + it "should set the cookie for this request if one exists for the given host:port" do + Chef::REST::CookieJar.instance["#{@url.host}:#{@url.port}"] = "cookie monster" + Net::HTTP::Get.should_receive(:new).with("/?foo=bar", @base_headers.merge('Cookie' => "cookie monster")).and_return(@request_mock) + @rest.api_request(:GET, @url, {}) + end + + it "should build a new HTTP GET request" do + Net::HTTP::Get.should_receive(:new).with("/?foo=bar", @base_headers).and_return(@request_mock) + @rest.api_request(:GET, @url, {}) + end + + it "should build a new HTTP POST request" do + request = Net::HTTP::Post.new(@url.path) + expected_headers = @base_headers.merge("Content-Type" => 'application/json', 'Content-Length' => '13') + + Net::HTTP::Post.should_receive(:new).with("/?foo=bar", expected_headers).and_return(request) + @rest.api_request(:POST, @url, {}, {:one=>:two}) + request.body.should == '{"one":"two"}' + end + + it "should build a new HTTP PUT request" do + request = Net::HTTP::Put.new(@url.path) + expected_headers = @base_headers.merge("Content-Type" => 'application/json', 'Content-Length' => '13') + Net::HTTP::Put.should_receive(:new).with("/?foo=bar",expected_headers).and_return(request) + @rest.api_request(:PUT, @url, {}, {:one=>:two}) + request.body.should == '{"one":"two"}' + end + + it "should build a new HTTP DELETE request" do + Net::HTTP::Delete.should_receive(:new).with("/?foo=bar", @base_headers).and_return(@request_mock) + @rest.api_request(:DELETE, @url) + end + + it "should raise an error if the method is not GET/PUT/POST/DELETE" do + lambda { @rest.api_request(:MONKEY, @url) }.should raise_error(ArgumentError) + end + + it "returns nil when the response is successful but content-type is not JSON" do + @rest.api_request(:GET, @url).should == "ninja" + end + + it "should inflate the body as to an object if JSON is returned" do + @http_response.add_field('content-type', "application/json") + @http_response.stub!(:body).and_return('{"ohai2u":"json_api"}') + @rest.api_request(:GET, @url, {}).should == {"ohai2u"=>"json_api"} + end + + %w[ HTTPFound HTTPMovedPermanently HTTPSeeOther HTTPUseProxy HTTPTemporaryRedirect HTTPMultipleChoice ].each do |resp_name| + it "should call api_request again on a #{resp_name} response" do + resp_cls = Net.const_get(resp_name) + resp_code = Net::HTTPResponse::CODE_TO_OBJ.keys.detect { |k| Net::HTTPResponse::CODE_TO_OBJ[k] == resp_cls } + http_response = Net::HTTPFound.new("1.1", resp_code, "bob is somewhere else again") + http_response.add_field("location", @url.path) + http_response.stub!(:read_body) + + @http_client.stub!(:request).and_yield(http_response).and_return(http_response) + + lambda { @rest.api_request(:GET, @url) }.should raise_error(Chef::Exceptions::RedirectLimitExceeded) + end + end + + it "should show the JSON error message on an unsuccessful request" do + http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth") + http_response.add_field("content-type", "application/json") + http_response.stub!(:body).and_return('{ "error":[ "Ears get sore!", "Not even four" ] }') + http_response.stub!(:read_body) + @rest.stub!(:sleep) + @http_client.stub!(:request).and_yield(http_response).and_return(http_response) + + lambda {@rest.run_request(:GET, @url)}.should raise_error(Net::HTTPFatalError) + @log_stringio.string.should match(Regexp.escape('WARN: HTTP Request Returned 500 drooling from inside of mouth: Ears get sore!, Not even four')) + end + + it "decompresses the JSON error message on an unsuccessful request" do + http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth") + http_response.add_field("content-type", "application/json") + http_response.add_field("content-encoding", "deflate") + unzipped_body = '{ "error":[ "Ears get sore!", "Not even four" ] }' + gzipped_body = Zlib::Deflate.deflate(unzipped_body, 1) + http_response.stub!(:body).and_return gzipped_body + http_response.stub!(:read_body) + @rest.stub!(:sleep) + @http_client.stub!(:request).and_yield(http_response).and_return(http_response) + + lambda {@rest.run_request(:GET, @url)}.should raise_error(Net::HTTPFatalError) + @log_stringio.string.should match(Regexp.escape('WARN: HTTP Request Returned 500 drooling from inside of mouth: Ears get sore!, Not even four')) + end + + it "should raise an exception on an unsuccessful request" do + http_response = Net::HTTPServerError.new("1.1", "500", "drooling from inside of mouth") + http_response.stub!(:body) + http_response.stub!(:read_body) + @rest.stub!(:sleep) + @http_client.stub!(:request).and_yield(http_response).and_return(http_response) + lambda {@rest.api_request(:GET, @url)}.should raise_error(Net::HTTPFatalError) + end + end + + context "when streaming downloads to a tempfile" do + before do + @tempfile = Tempfile.open("chef-rspec-rest_spec-line-#{__LINE__}--") + Tempfile.stub!(:new).with("chef-rest").and_return(@tempfile) + @request_mock = {} + Net::HTTP::Get.stub!(:new).and_return(@request_mock) + + @http_response = Net::HTTPSuccess.new("1.1",200, "it-works") + @http_response.stub!(:read_body) + @http_client.stub!(:request).and_yield(@http_response).and_return(@http_response) + end + + after do + @tempfile.rspec_reset + @tempfile.close! + end + + it " build a new HTTP GET request without the application/json accept header" do + expected_headers = {'X-Chef-Version' => Chef::VERSION, 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE} + Net::HTTP::Get.should_receive(:new).with("/?foo=bar", expected_headers).and_return(@request_mock) + @rest.streaming_request(@url, {}) + end + + it "returns a tempfile containing the streamed response body" do + @rest.streaming_request(@url, {}).should equal(@tempfile) + end + + it "writes the response body to a tempfile" do + @http_response.stub!(:read_body).and_yield("real").and_yield("ultimate").and_yield("power") + @rest.streaming_request(@url, {}) + IO.read(@tempfile.path).chomp.should == "realultimatepower" + end + + it "closes the tempfile" do + @rest.streaming_request(@url, {}) + @tempfile.should be_closed + end + + it "yields the tempfile containing the streamed response body and then unlinks it when given a block" do + @http_response.stub!(:read_body).and_yield("real").and_yield("ultimate").and_yield("power") + tempfile_path = nil + @rest.streaming_request(@url, {}) do |tempfile| + tempfile_path = tempfile.path + File.exist?(tempfile.path).should be_true + IO.read(@tempfile.path).chomp.should == "realultimatepower" + end + File.exist?(tempfile_path).should be_false + end + + it "does not raise a divide by zero exception if the content's actual size is 0" do + @http_response.add_field('Content-Length', "5") + @http_response.stub!(:read_body).and_yield('') + lambda { @rest.streaming_request(@url, {}) }.should_not raise_error(ZeroDivisionError) + end + + it "does not raise a divide by zero exception when the Content-Length is 0" do + @http_response.add_field('Content-Length', "0") + @http_response.stub!(:read_body).and_yield("ninja") + lambda { @rest.streaming_request(@url, {}) }.should_not raise_error(ZeroDivisionError) + end + + it "fetches a file and yields the tempfile it is streamed to" do + @http_response.stub!(:read_body).and_yield("real").and_yield("ultimate").and_yield("power") + tempfile_path = nil + @rest.fetch("cookbooks/a_cookbook") do |tempfile| + tempfile_path = tempfile.path + IO.read(@tempfile.path).chomp.should == "realultimatepower" + end + File.exist?(tempfile_path).should be_false + end + + it "closes and unlinks the tempfile if there is an error while streaming the content to the tempfile" do + path = @tempfile.path + path.should_not be_nil + @tempfile.stub!(:write).and_raise(IOError) + @rest.fetch("cookbooks/a_cookbook") {|tmpfile| "shouldn't get here"} + File.exists?(path).should be_false + end + + it "closes and unlinks the tempfile when the response is a redirect" do + Tempfile.rspec_reset + tempfile = mock("die", :path => "/tmp/ragefist", :close => true, :binmode => nil) + tempfile.should_receive(:close!).at_least(2).times + Tempfile.stub!(:new).with("chef-rest").and_return(tempfile) + + http_response = Net::HTTPFound.new("1.1", "302", "bob is taking care of that one for me today") + http_response.add_field("location", @url.path) + http_response.stub!(:read_body) + + @http_client.stub!(:request).and_yield(http_response).and_yield(@http_response).and_return(http_response, @http_response) + @rest.fetch("cookbooks/a_cookbook") {|tmpfile| "shouldn't get here"} + end + + it "passes the original block to the redirected request" do + Tempfile.rspec_reset + + http_response = Net::HTTPFound.new("1.1", "302", "bob is taking care of that one for me today") + http_response.add_field("location","/that-thing-is-here-now") + http_response.stub!(:read_body) + + block_called = false + @http_client.stub!(:request).and_yield(@http_response).and_return(http_response, @http_response) + @rest.fetch("cookbooks/a_cookbook") do |tmpfile| + block_called = true + end + block_called.should be_true + end + end + end + + context "when following redirects" do + before do + Chef::Config[:node_name] = "webmonkey.example.com" + Chef::Config[:client_key] = CHEF_SPEC_DATA + "/ssl/private_key.pem" + @rest = Chef::REST.new(@url) + end + + it "raises a RedirectLimitExceeded when redirected more than 10 times" do + redirected = lambda {@rest.follow_redirect { redirected.call }} + lambda {redirected.call}.should raise_error(Chef::Exceptions::RedirectLimitExceeded) + end + + it "does not count redirects from previous calls against the redirect limit" do + total_redirects = 0 + redirected = lambda do + @rest.follow_redirect do + total_redirects += 1 + redirected.call unless total_redirects >= 9 + end + end + lambda {redirected.call}.should_not raise_error(Chef::Exceptions::RedirectLimitExceeded) + total_redirects = 0 + lambda {redirected.call}.should_not raise_error(Chef::Exceptions::RedirectLimitExceeded) + end + + it "does not sign the redirected request when sign_on_redirect is false" do + @rest.sign_on_redirect = false + @rest.follow_redirect { @rest.sign_requests?.should be_false } + end + + it "resets sign_requests to the original value after following an unsigned redirect" do + @rest.sign_on_redirect = false + @rest.sign_requests?.should be_true + + @rest.follow_redirect { @rest.sign_requests?.should be_false } + @rest.sign_requests?.should be_true + end + + it "configures the redirect limit" do + total_redirects = 0 + redirected = lambda do + @rest.follow_redirect do + total_redirects += 1 + redirected.call unless total_redirects >= 9 + end + end + lambda {redirected.call}.should_not raise_error(Chef::Exceptions::RedirectLimitExceeded) + + total_redirects = 0 + @rest.redirect_limit = 3 + lambda {redirected.call}.should raise_error(Chef::Exceptions::RedirectLimitExceeded) + end + + end +end diff --git a/spec/unit/role_spec.rb b/spec/unit/role_spec.rb new file mode 100644 index 0000000000..764d586903 --- /dev/null +++ b/spec/unit/role_spec.rb @@ -0,0 +1,275 @@ +# +# 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 'chef/role' + +describe Chef::Role do + before(:each) do + @role = Chef::Role.new + @role.name("ops_master") + end + + it "has a name" do + @role.name("ops_master").should == "ops_master" + end + + it "does not accept a name with spaces" do + lambda { @role.name "ops master" }.should raise_error(ArgumentError) + end + + it "does not accept non-String objects for the name" do + lambda { @role.name({}) }.should raise_error(ArgumentError) + end + + describe "when a run list is set" do + + before do + @role.run_list(%w{ nginx recipe[ree] role[base]}) + end + + + it "returns the run list" do + @role.run_list.should == %w{ nginx recipe[ree] role[base]} + end + + describe "and per-environment run lists are set" do + before do + @role.name("base") + @role.run_list(%w{ recipe[nagios::client] recipe[tims-acl::bork]}) + @role.env_run_list["prod"] = Chef::RunList.new(*(@role.run_list.to_a << "recipe[prod-base]")) + @role.env_run_list["dev"] = Chef::RunList.new + end + + it "uses the default run list as *the* run_list" do + @role.run_list.should == Chef::RunList.new("recipe[nagios::client]", "recipe[tims-acl::bork]") + end + + it "gives the default run list as the when getting the _default run list" do + @role.run_list_for("_default").should == @role.run_list + end + + it "gives an environment specific run list" do + @role.run_list_for("prod").should == Chef::RunList.new("recipe[nagios::client]", "recipe[tims-acl::bork]", "recipe[prod-base]") + end + + it "gives the default run list when no run list exists for the given environment" do + @role.run_list_for("qa").should == @role.run_list + end + + it "gives the environment specific run list even if it is empty" do + @role.run_list_for("dev").should == Chef::RunList.new + end + + it "env_run_lists can only be set with _default run list in it" do + long_exception_name = Chef::Exceptions::InvalidEnvironmentRunListSpecification + lambda {@role.env_run_lists({})}.should raise_error(long_exception_name) + end + + end + + + describe "using the old #recipes API" do + it "should let you set the recipe array" do + @role.recipes([ "one", "two" ]).should == [ "one", "two" ] + end + + it "should let you return the recipe array" do + @role.recipes([ "one", "two" ]) + @role.recipes.should == [ "one", "two" ] + end + + it "should not list roles in the recipe array" do + @role.run_list([ "one", "role[two]"]) + @role.recipes.should == [ "recipe[one]", "role[two]" ] + end + + end + + end + + + + describe "default_attributes" do + it "should let you set the default attributes hash explicitly" do + @role.default_attributes({ :one => 'two' }).should == { :one => 'two' } + end + + it "should let you return the default attributes hash" do + @role.default_attributes({ :one => 'two' }) + @role.default_attributes.should == { :one => 'two' } + end + + it "should throw an ArgumentError if we aren't a kind of hash" do + lambda { @role.default_attributes(Array.new) }.should raise_error(ArgumentError) + end + end + + describe "override_attributes" do + it "should let you set the override attributes hash explicitly" do + @role.override_attributes({ :one => 'two' }).should == { :one => 'two' } + end + + it "should let you return the override attributes hash" do + @role.override_attributes({ :one => 'two' }) + @role.override_attributes.should == { :one => 'two' } + end + + it "should throw an ArgumentError if we aren't a kind of hash" do + lambda { @role.override_attributes(Array.new) }.should raise_error(ArgumentError) + end + end + + describe "update_from!" do + before(:each) do + @role.name('mars_volta') + @role.description('Great band!') + @role.run_list('one', 'two', 'role[a]') + @role.default_attributes({ :el_groupo => 'nuevo' }) + @role.override_attributes({ :deloused => 'in the comatorium' }) + + @example = Chef::Role.new + @example.name('newname') + @example.description('Really Great band!') + @example.run_list('alpha', 'bravo', 'role[alpha]') + @example.default_attributes({ :el_groupo => 'nuevo dos' }) + @example.override_attributes({ :deloused => 'in the comatorium XOXO' }) + end + + it "should update all fields except for name" do + @role.update_from!(@example) + @role.name.should == "mars_volta" + @role.description.should == @example.description + @role.run_list.should == @example.run_list + @role.default_attributes.should == @example.default_attributes + @role.override_attributes.should == @example.override_attributes + end + end + + describe "when serialized as JSON", :json => true do + before(:each) do + @role.name('mars_volta') + @role.description('Great band!') + @role.run_list('one', 'two', 'role[a]') + @role.default_attributes({ :el_groupo => 'nuevo' }) + @role.override_attributes({ :deloused => 'in the comatorium' }) + @serialized_role = Chef::JSONCompat.to_json(@role) + end + + it "should serialize to a json hash" do + Chef::JSONCompat.to_json(@role).should match(/^\{.+\}$/) + end + + it "includes the name in the JSON output" do + @serialized_role.should =~ /"name":"mars_volta"/ + end + + it "includes its description in the JSON" do + @serialized_role.should match(/"description":"Great band!"/) + end + + it "should include 'default_attributes'" do + @serialized_role.should =~ /"default_attributes":\{"el_groupo":"nuevo"\}/ + end + + it "should include 'override_attributes'" do + @serialized_role.should =~ /"override_attributes":\{"deloused":"in the comatorium"\}/ + end + + it "should include 'run_list'" do + #Activesupport messes with Chef json formatting + #This test should pass with and without activesupport + @serialized_role.should =~ /"run_list":\["recipe\[one\]","recipe\[two\]","role\[a\]"\]/ + end + + describe "and it has per-environment run lists" do + before do + @role.env_run_lists("_default" => ['one', 'two', 'role[a]'], "production" => ['role[monitoring]', 'role[auditing]', 'role[apache]'], "dev" => ["role[nginx]"]) + @serialized_role = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(@role), :create_additions => false) + end + + it "includes the per-environment run lists" do + #Activesupport messes with Chef json formatting + #This test should pass with and without activesupport + @serialized_role["env_run_lists"]["production"].should == ['role[monitoring]', 'role[auditing]', 'role[apache]'] + @serialized_role["env_run_lists"]["dev"].should == ["role[nginx]"] + end + + it "does not include the default environment in the per-environment run lists" do + @serialized_role["env_run_lists"].should_not have_key("_default") + end + + end + end + + describe "when created from JSON", :json => true do + before(:each) do + @role.name('mars_volta') + @role.description('Great band!') + @role.run_list('one', 'two', 'role[a]') + @role.default_attributes({ 'el_groupo' => 'nuevo' }) + @role.override_attributes({ 'deloused' => 'in the comatorium' }) + @deserial = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(@role)) + end + + it "should deserialize to a Chef::Role object" do + @deserial.should be_a_kind_of(Chef::Role) + end + + %w{ + name + description + default_attributes + override_attributes + run_list + }.each do |t| + it "should preserves the '#{t}' attribute from the JSON object" do + @deserial.send(t.to_sym).should == @role.send(t.to_sym) + end + end + end + + describe "when loading from disk" do + it "should return a Chef::Role object from JSON" do + File.should_receive(:exists?).with(File.join(Chef::Config[:role_path], 'lolcat.json')).exactly(1).times.and_return(true) + IO.should_receive(:read).with(File.join(Chef::Config[:role_path], 'lolcat.json')).and_return('{"name": "ceiling_cat", "json_class": "Chef::Role" }') + @role.should be_a_kind_of(Chef::Role) + @role.class.from_disk("lolcat") + end + + it "should return a Chef::Role object from a Ruby DSL" do + File.should_receive(:exists?).with(File.join(Chef::Config[:role_path], 'lolcat.json')).exactly(1).times.and_return(false) + File.should_receive(:exists?).with(File.join(Chef::Config[:role_path], 'lolcat.rb')).exactly(2).times.and_return(true) + File.should_receive(:readable?).with(File.join(Chef::Config[:role_path], 'lolcat.rb')).exactly(1).times.and_return(true) + ROLE_DSL=<<-EOR +name "ceiling_cat" +description "like Aliens, but furry" +EOR + IO.should_receive(:read).with(File.join(Chef::Config[:role_path], 'lolcat.rb')).and_return(ROLE_DSL) + @role.should be_a_kind_of(Chef::Role) + @role.class.from_disk("lolcat") + end + + it "should raise an exception if the file does not exist" do + File.should_receive(:exists?).with(File.join(Chef::Config[:role_path], 'lolcat.json')).exactly(1).times.and_return(false) + File.should_receive(:exists?).with(File.join(Chef::Config[:role_path], 'lolcat.rb')).exactly(1).times.and_return(false) + lambda {@role.class.from_disk("lolcat")}.should raise_error(Chef::Exceptions::RoleNotFound) + end + end +end + diff --git a/spec/unit/run_context_spec.rb b/spec/unit/run_context_spec.rb new file mode 100644 index 0000000000..51fa0e81f9 --- /dev/null +++ b/spec/unit/run_context_spec.rb @@ -0,0 +1,78 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Tim Hinderliter (<tim@opscode.com>) +# Author:: Christopher Walters (<cw@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' + +Chef::Log.level = :debug + +describe Chef::RunContext do + before(:each) do + @chef_repo_path = File.expand_path(File.join(CHEF_SPEC_DATA, "run_context", "cookbooks")) + cl = Chef::CookbookLoader.new(@chef_repo_path) + cl.load_cookbooks + @cookbook_collection = Chef::CookbookCollection.new(cl) + @node = Chef::Node.new + @node.run_list << "test" << "test::one" << "test::two" + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, @cookbook_collection, @events) + end + + it "has a cookbook collection" do + @run_context.cookbook_collection.should == @cookbook_collection + end + + it "has a node" do + @run_context.node.should == @node + end + + describe "after loading the cookbooks" do + before do + @run_context.load(@node.run_list.expand('_default')) + end + + it "should load all the definitions in the cookbooks for this node" do + @run_context.definitions.should have_key(:new_cat) + @run_context.definitions.should have_key(:new_badger) + @run_context.definitions.should have_key(:new_dog) + end + + it "should load all the recipes specified for this node" do + @run_context.resource_collection[0].to_s.should == "cat[einstein]" + @run_context.resource_collection[1].to_s.should == "cat[loulou]" + @run_context.resource_collection[2].to_s.should == "cat[birthday]" + @run_context.resource_collection[3].to_s.should == "cat[peanut]" + @run_context.resource_collection[4].to_s.should == "cat[fat peanut]" + end + + it "loads all the attribute files in the cookbook collection" do + @run_context.loaded_fully_qualified_attribute?("test", "george").should be_true + @node[:george].should == "washington" + end + + it "registers attributes files as loaded so they won't be reloaded" do + # This test unfortunately is pretty tightly intertwined with the + # implementation of how nodes load attribute files, but is the only + # convenient way to test this behavior. + @node.should_not_receive(:from_file) + @node.include_attribute("test::george") + end + end + +end diff --git a/spec/unit/run_list/run_list_expansion_spec.rb b/spec/unit/run_list/run_list_expansion_spec.rb new file mode 100644 index 0000000000..012ee9ea31 --- /dev/null +++ b/spec/unit/run_list/run_list_expansion_spec.rb @@ -0,0 +1,129 @@ +# +# 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' + +describe Chef::RunList::RunListExpansion do + before do + @run_list = Chef::RunList.new + @run_list << 'recipe[lobster]' << 'role[rage]' << 'recipe[fist]' + @expansion = Chef::RunList::RunListExpansion.new("_default", @run_list.run_list_items) + end + + describe "before expanding the run list" do + it "has an array of run list items" do + @expansion.run_list_items.should == @run_list.run_list_items + end + + it "has default_attrs" do + @expansion.default_attrs.should == Mash.new + end + + it "has override attrs" do + @expansion.override_attrs.should == Mash.new + end + + it "it has an empty list of recipes" do + @expansion.should have(0).recipes + end + + it "has not applied its roles" do + @expansion.applied_role?('rage').should be_false + end + end + + describe "after applying a role with environment-specific run lists" do + before do + @rage_role = Chef::Role.new.tap do |r| + r.name("rage") + r.env_run_lists('_default' => [], "prod" => ["recipe[prod-only]"]) + end + @expansion = Chef::RunList::RunListExpansion.new("prod", @run_list.run_list_items) + @expansion.should_receive(:fetch_role).and_return(@rage_role) + @expansion.expand + end + + it "has the correct list of recipes for the given environment" do + @expansion.recipes.should == ["lobster", "prod-only", "fist"] + end + + end + + describe "after applying a role" do + before do + @expansion.stub!(:fetch_role).and_return(Chef::Role.new) + @expansion.inflate_role('rage', "role[base]") + end + + it "tracks the applied role" do + @expansion.applied_role?('rage').should be_true + end + + it "does not inflate the role again" do + @expansion.inflate_role('rage', "role[base]").should be_false + end + end + + describe "after expanding a run list" do + before do + @first_role = Chef::Role.new + @first_role.run_list('role[mollusk]') + @first_role.default_attributes({'foo' => 'bar'}) + @first_role.override_attributes({'baz' => 'qux'}) + @second_role = Chef::Role.new + @second_role.run_list('recipe[crabrevenge]') + @second_role.default_attributes({'foo' => 'boo'}) + @second_role.override_attributes({'baz' => 'bux'}) + @expansion.stub!(:fetch_role).and_return(@first_role, @second_role) + @expansion.expand + end + + it "has the ordered list of recipes" do + @expansion.recipes.should == ['lobster', 'crabrevenge', 'fist'] + end + + it "has the merged attributes from the roles with outer roles overridding inner" do + @expansion.default_attrs.should == {'foo' => 'bar'} + @expansion.override_attrs.should == {'baz' => 'qux'} + end + + it "has the list of all roles applied" do + # this is the correct order, but 1.8 hash order is not stable + @expansion.roles.should =~ ['rage', 'mollusk'] + end + + end + + describe "after expanding a run list with a non existant role" do + before do + @expansion.stub!(:fetch_role) { @expansion.role_not_found('crabrevenge', "role[base]") } + @expansion.expand + end + + it "is invalid" do + @expansion.should be_invalid + @expansion.errors?.should be_true # aliases + end + + it "has a list of invalid role names" do + @expansion.errors.should include('crabrevenge') + end + + end + +end diff --git a/spec/unit/run_list/run_list_item_spec.rb b/spec/unit/run_list/run_list_item_spec.rb new file mode 100644 index 0000000000..62a17aa761 --- /dev/null +++ b/spec/unit/run_list/run_list_item_spec.rb @@ -0,0 +1,117 @@ +# +# 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' + +describe Chef::RunList::RunListItem do + + describe "when creating from a Hash" do + it "raises an exception when the hash doesn't have a :type key" do + lambda {Chef::RunList::RunListItem.new(:name => "tatft")}.should raise_error(ArgumentError) + end + + it "raises an exception when the hash doesn't have an :name key" do + lambda {Chef::RunList::RunListItem.new(:type => 'R') }.should raise_error(ArgumentError) + end + + it "sets the name and type as given in the hash" do + item = Chef::RunList::RunListItem.new(:type => 'fuuu', :name => 'uuuu') + item.to_s.should == 'fuuu[uuuu]' + end + + end + + describe "when creating an item from a string" do + it "parses a qualified recipe" do + item = Chef::RunList::RunListItem.new("recipe[rage]") + item.should be_a_recipe + item.should_not be_a_role + item.to_s.should == 'recipe[rage]' + item.name.should == 'rage' + end + + it "parses a qualified recipe with a version" do + item = Chef::RunList::RunListItem.new("recipe[rage@0.1.0]") + item.should be_a_recipe + item.should_not be_a_role + item.to_s.should == 'recipe[rage@0.1.0]' + item.name.should == 'rage' + item.version.should == '0.1.0' + end + + it "parses a qualified role" do + item = Chef::RunList::RunListItem.new("role[fist]") + item.should be_a_role + item.should_not be_a_recipe + item.to_s.should == 'role[fist]' + item.name.should == 'fist' + end + + it "parses an unqualified recipe" do + item = Chef::RunList::RunListItem.new("lobster") + item.should be_a_recipe + item.should_not be_a_role + item.to_s.should == 'recipe[lobster]' + item.name.should == 'lobster' + end + + it "raises an exception when the string has typo on the type part" do + lambda {Chef::RunList::RunListItem.new("Recipe[lobster]") }.should raise_error(ArgumentError) + end + + it "raises an exception when the string has extra space between the type and the name" do + lambda {Chef::RunList::RunListItem.new("recipe [lobster]") }.should raise_error(ArgumentError) + end + + it "raises an exception when the string does not close the bracket" do + lambda {Chef::RunList::RunListItem.new("recipe[lobster") }.should raise_error(ArgumentError) + end + end + + describe "comparing to other run list items" do + it "is equal to another run list item that has the same name and type" do + item1 = Chef::RunList::RunListItem.new('recipe[lrf]') + item2 = Chef::RunList::RunListItem.new('recipe[lrf]') + item1.should == item2 + end + + it "is not equal to another run list item with the same name and different type" do + item1 = Chef::RunList::RunListItem.new('recipe[lrf]') + item2 = Chef::RunList::RunListItem.new('role[lrf]') + item1.should_not == item2 + end + + it "is not equal to another run list item with the same type and different name" do + item1 = Chef::RunList::RunListItem.new('recipe[lrf]') + item2 = Chef::RunList::RunListItem.new('recipe[lobsterragefist]') + item1.should_not == item2 + end + + it "is not equal to another run list item with the same name and type but different version" do + item1 = Chef::RunList::RunListItem.new('recipe[lrf,0.1.0]') + item2 = Chef::RunList::RunListItem.new('recipe[lrf,0.2.0]') + item1.should_not == item2 + end + end + + describe "comparing to strings" do + it "is equal to a string if that string matches its to_s representation" do + Chef::RunList::RunListItem.new('recipe[lrf]').should == 'recipe[lrf]' + end + end +end diff --git a/spec/unit/run_list/versioned_recipe_list_spec.rb b/spec/unit/run_list/versioned_recipe_list_spec.rb new file mode 100644 index 0000000000..5cef32c32b --- /dev/null +++ b/spec/unit/run_list/versioned_recipe_list_spec.rb @@ -0,0 +1,123 @@ +# +# Author:: Stephen Delano (<stephen@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' + +describe Chef::RunList::VersionedRecipeList do + + describe "initialize" do + it "should create an empty array" do + l = Chef::RunList::VersionedRecipeList.new + l.should == [] + end + end + + describe "add_recipe" do + before(:each) do + @list = Chef::RunList::VersionedRecipeList.new + @list << "apt" + @list << "god" + @list << "apache2" + end + + it "should append the recipe to the end of the list" do + @list.add_recipe "rails" + @list.should == ["apt", "god", "apache2", "rails"] + end + + it "should not duplicate entries" do + @list.add_recipe "apt" + @list.should == ["apt", "god", "apache2"] + end + + it "should allow you to specify a version" do + @list.add_recipe "rails", "1.0.0" + @list.should == ["apt", "god", "apache2", "rails"] + @list.with_versions.should include({:name => "rails", :version => "1.0.0"}) + end + + it "should allow you to specify a version for a recipe that already exists" do + @list.add_recipe "apt", "1.2.3" + @list.should == ["apt", "god", "apache2"] + @list.with_versions.should include({:name => "apt", :version => "1.2.3"}) + end + + it "should allow you to specify the same version of a recipe twice" do + @list.add_recipe "rails", "1.0.0" + @list.add_recipe "rails", "1.0.0" + @list.with_versions.should include({:name => "rails", :version => "1.0.0"}) + end + + it "should allow you to spcify no version, even when a version already exists" do + @list.add_recipe "rails", "1.0.0" + @list.add_recipe "rails" + @list.with_versions.should include({:name => "rails", :version => "1.0.0"}) + end + + it "should not allow multiple versions of the same recipe" do + @list.add_recipe "rails", "1.0.0" + lambda {@list.add_recipe "rails", "0.1.0"}.should raise_error Chef::Exceptions::CookbookVersionConflict + end + end + + describe "with_versions" do + before(:each) do + @recipes = [ + {:name => "apt", :version => "1.0.0"}, + {:name => "god", :version => nil}, + {:name => "apache2", :version => "0.0.1"} + ] + @list = Chef::RunList::VersionedRecipeList.new + @recipes.each {|i| @list.add_recipe i[:name], i[:version]} + end + + it "should return an array of hashes with :name and :version" do + @list.with_versions.should == @recipes + end + + it "should retain the same order as the version-less list" do + with_versions = @list.with_versions + @list.each_with_index do |item, index| + with_versions[index][:name].should == item + end + end + end + + describe "with_version_constraints" do + before(:each) do + @recipes = [ + {:name => "apt", :version => "~> 1.2.0"}, + {:name => "god", :version => nil}, + {:name => "apache2", :version => "0.0.1"} + ] + @list = Chef::RunList::VersionedRecipeList.new + @recipes.each {|i| @list.add_recipe i[:name], i[:version]} + @constraints = @recipes.map do |x| + { :name => x[:name], + :version_constraint => Chef::VersionConstraint.new(x[:version]) + } + end + end + + it "should return an array of hashes with :name and :version_constraint" do + @list.with_version_constraints.each do |x| + x.should have_key :name + x[:version_constraint].should_not be nil + end + end + end +end diff --git a/spec/unit/run_list_spec.rb b/spec/unit/run_list_spec.rb new file mode 100644 index 0000000000..f18f21a82b --- /dev/null +++ b/spec/unit/run_list_spec.rb @@ -0,0 +1,312 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Seth Falcon (<seth@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Copyright:: Copyright (c) 2008-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' + +require 'chef/version_class' +require 'chef/version_constraint' + +describe Chef::RunList do + before(:each) do + @run_list = Chef::RunList.new + end + + describe "<<" do + it "should add a recipe to the run list and recipe list with the fully qualified name" do + @run_list << 'recipe[needy]' + @run_list.should include('recipe[needy]') + @run_list.recipes.should include("needy") + end + + it "should add a role to the run list and role list with the fully qualified name" do + @run_list << "role[woot]" + @run_list.should include('role[woot]') + @run_list.roles.should include('woot') + end + + it "should accept recipes that are unqualified" do + @run_list << "needy" + @run_list.should include('recipe[needy]') + @run_list.recipes.include?('needy').should == true + end + + it "should not allow duplicates" do + @run_list << "needy" + @run_list << "needy" + @run_list.run_list.length.should == 1 + @run_list.recipes.length.should == 1 + end + + it "should allow two versions of a recipe" do + @run_list << "recipe[needy@0.2.0]" + @run_list << "recipe[needy@0.1.0]" + @run_list.run_list.length.should == 2 + @run_list.recipes.length.should == 2 + @run_list.recipes.include?('needy').should == true + end + + it "should not allow duplicate versions of a recipe" do + @run_list << "recipe[needy@0.2.0]" + @run_list << "recipe[needy@0.2.0]" + @run_list.run_list.length.should == 1 + @run_list.recipes.length.should == 1 + end + end + + describe "add" do + # Testing only the basic functionality here + # since full behavior is tested above. + it "should add a recipe to the run_list" do + @run_list.add 'recipe[needy]' + @run_list.should include('recipe[needy]') + end + + it "should add a role to the run_list" do + @run_list.add 'role[needy]' + @run_list.should include('role[needy]') + end + end + + describe "==" do + it "should believe two RunLists are equal if they have the same members" do + @run_list << "foo" + r = Chef::RunList.new + r << "foo" + @run_list.should == r + end + + it "should believe a RunList is equal to an array named after it's members" do + @run_list << "foo" + @run_list << "baz" + @run_list.should == [ "foo", "baz" ] + end + end + + describe "empty?" do + it "should be emtpy if the run list has no members" do + @run_list.empty?.should == true + end + + it "should not be empty if the run list has members" do + @run_list << "chromeo" + @run_list.empty?.should == false + end + end + + describe "[]" do + it "should let you look up a member in the run list by position" do + @run_list << 'recipe[loulou]' + @run_list[0].should == 'recipe[loulou]' + end + end + + describe "[]=" do + it "should let you set a member of the run list by position" do + @run_list[0] = 'recipe[loulou]' + @run_list[0].should == 'recipe[loulou]' + end + + it "should properly expand a member of the run list given by position" do + @run_list[0] = 'loulou' + @run_list[0].should == 'recipe[loulou]' + end + end + + describe "each" do + it "should yield each member to your block" do + @run_list << "foo" + @run_list << "bar" + seen = Array.new + @run_list.each { |r| seen << r } + seen.should be_include("recipe[foo]") + seen.should be_include("recipe[bar]") + end + end + + describe "each_index" do + it "should yield each members index to your block" do + to_add = [ "recipe[foo]", "recipe[bar]", "recipe[baz]" ] + to_add.each { |i| @run_list << i } + @run_list.each_index { |i| @run_list[i].should == to_add[i] } + end + end + + describe "include?" do + it "should be true if the run list includes the item" do + @run_list << "foo" + @run_list.include?("foo") + end + end + + describe "reset" do + it "should reset the run_list based on the array you pass" do + @run_list << "chromeo" + list = %w{camp chairs snakes clowns} + @run_list.reset!(list) + list.each { |i| @run_list.should be_include(i) } + @run_list.include?("chromeo").should == false + end + end + + describe "when expanding the run list" do + before(:each) do + @role = Chef::Role.new + @role.name "stubby" + @role.run_list "one", "two" + @role.default_attributes :one => :two + @role.override_attributes :three => :four + + Chef::Role.stub!(:load).and_return(@role) + @rest = mock("Chef::REST", { :get_rest => @role, :url => "/" }) + Chef::REST.stub!(:new).and_return(@rest) + + @run_list << "role[stubby]" + @run_list << "kitty" + end + + describe "from disk" do + it "should load the role from disk" do + Chef::Role.should_receive(:from_disk).with("stubby") + @run_list.expand("_default", "disk") + end + + it "should log a helpful error if the role is not available" do + Chef::Role.stub!(:from_disk).and_raise(Chef::Exceptions::RoleNotFound) + Chef::Log.should_receive(:error).with("Role stubby (included by 'top level') is in the runlist but does not exist. Skipping expand.") + @run_list.expand("_default", "disk") + end + end + + describe "from the chef server" do + it "should load the role from the chef server" do + #@rest.should_receive(:get_rest).with("roles/stubby") + expansion = @run_list.expand("_default", "server") + expansion.recipes.should == ['one', 'two', 'kitty'] + end + + it "should default to expanding from the server" do + @rest.should_receive(:get_rest).with("roles/stubby") + @run_list.expand("_default") + end + + describe "with an environment set" do + before do + @role.env_run_list["production"] = Chef::RunList.new( "one", "two", "five") + end + + it "expands the run list using the environment specific run list" do + expansion = @run_list.expand("production", "server") + expansion.recipes.should == %w{one two five kitty} + end + + describe "and multiply nested roles" do + before do + @multiple_rest_requests = mock("Chef::REST") + + @role.env_run_list["production"] << "role[prod-base]" + + @role_prod_base = Chef::Role.new + @role_prod_base.name("prod-base") + @role_prod_base.env_run_list["production"] = Chef::RunList.new("role[nested-deeper]") + + + @role_nested_deeper = Chef::Role.new + @role_nested_deeper.name("nested-deeper") + @role_nested_deeper.env_run_list["production"] = Chef::RunList.new("recipe[prod-secret-sauce]") + end + + it "expands the run list using the specified environment for all nested roles" do + Chef::REST.stub!(:new).and_return(@multiple_rest_requests) + @multiple_rest_requests.should_receive(:get_rest).with("roles/stubby").and_return(@role) + @multiple_rest_requests.should_receive(:get_rest).with("roles/prod-base").and_return(@role_prod_base) + @multiple_rest_requests.should_receive(:get_rest).with("roles/nested-deeper").and_return(@role_nested_deeper) + + expansion = @run_list.expand("production", "server") + expansion.recipes.should == %w{one two five prod-secret-sauce kitty} + end + + end + + end + + end + + it "should return the list of expanded recipes" do + expansion = @run_list.expand("_default") + expansion.recipes[0].should == "one" + expansion.recipes[1].should == "two" + end + + it "should return the list of default attributes" do + expansion = @run_list.expand("_default") + expansion.default_attrs[:one].should == :two + end + + it "should return the list of override attributes" do + expansion = @run_list.expand("_default") + expansion.override_attrs[:three].should == :four + end + + it "should recurse into a child role" do + dog = Chef::Role.new + dog.name "dog" + dog.default_attributes :seven => :nine + dog.run_list "three" + @role.run_list << "role[dog]" + Chef::Role.stub!(:from_disk).with("stubby").and_return(@role) + Chef::Role.stub!(:from_disk).with("dog").and_return(dog) + + expansion = @run_list.expand("_default", 'disk') + expansion.recipes[2].should == "three" + expansion.default_attrs[:seven].should == :nine + end + + it "should not recurse infinitely" do + dog = Chef::Role.new + dog.name "dog" + dog.default_attributes :seven => :nine + dog.run_list "role[dog]", "three" + @role.run_list << "role[dog]" + Chef::Role.stub!(:from_disk).with("stubby").and_return(@role) + Chef::Role.should_receive(:from_disk).with("dog").once.and_return(dog) + + expansion = @run_list.expand("_default", 'disk') + expansion.recipes[2].should == "three" + expansion.recipes[3].should == "kitty" + expansion.default_attrs[:seven].should == :nine + end + end + + describe "when converting to an alternate representation" do + before do + @run_list << "recipe[nagios::client]" << "role[production]" << "recipe[apache2]" + end + + it "converts to an array of the string forms of its items" do + @run_list.to_a.should == ["recipe[nagios::client]", "role[production]", "recipe[apache2]"] + end + + it "converts to json by converting its array form" do + @run_list.to_json.should == ["recipe[nagios::client]", "role[production]", "recipe[apache2]"].to_json + end + + end + +end diff --git a/spec/unit/run_lock_spec.rb b/spec/unit/run_lock_spec.rb new file mode 100644 index 0000000000..4e62b110b9 --- /dev/null +++ b/spec/unit/run_lock_spec.rb @@ -0,0 +1,37 @@ +# +# Author:: Daniel DeLeo (<dan@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 File.expand_path('../../spec_helper', __FILE__) +require 'chef/client' + +describe Chef::RunLock do + + describe "when first created" do + it "locates the lockfile in the file cache path by default" do + run_lock = Chef::RunLock.new(:file_cache_path => "/var/chef/cache", :lockfile => nil) + run_lock.runlock_file.should == "/var/chef/cache/chef-client-running.pid" + end + + it "locates the lockfile in the user-configured path when set" do + run_lock = Chef::RunLock.new(:file_cache_path => "/var/chef/cache", :lockfile => "/tmp/chef-client-running.pid") + run_lock.runlock_file.should == "/tmp/chef-client-running.pid" + end + end + + # See also: spec/functional/run_lock_spec + +end diff --git a/spec/unit/run_status_spec.rb b/spec/unit/run_status_spec.rb new file mode 100644 index 0000000000..049b86cf54 --- /dev/null +++ b/spec/unit/run_status_spec.rb @@ -0,0 +1,145 @@ +# +# 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' + +describe Chef::RunStatus do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @run_status = Chef::RunStatus.new(@node, @events) + end + + describe "before the run context has been set" do + it "converts to a hash" do + @run_status.to_hash + end + end + + describe "when the run context has been set" do + before do + @run_status.run_context = @run_context + end + + it "has a run context" do + @run_status.run_context.should equal(@run_context) + end + + it "provides access to the run context's node" do + @run_status.node.should equal(@node) + end + + it "converts to a hash" do + @run_status.to_hash[:node].should equal(@node) + @run_status.to_hash[:success].should be_true + end + + describe "after it has recorded timing information" do + before do + @start_time = Time.new + @end_time = @start_time + 23 + Time.stub!(:now).and_return(@start_time, @end_time) + @run_status.start_clock + @run_status.stop_clock + end + + it "records the start time of the run" do + @run_status.start_time.should == @start_time + end + + it "records the end time of the run" do + @run_status.end_time.should == @end_time + end + + it "gives the elapsed time of the chef run" do + @run_status.elapsed_time.should == 23 + end + + it "includes timing information in its hash form" do + @run_status.to_hash[:start_time].should == @start_time + @run_status.to_hash[:end_time].should == @end_time + @run_status.to_hash[:elapsed_time].should == 23 + end + + end + + describe "with resources in the resource_collection" do + before do + @all_resources = [Chef::Resource::Cat.new("whiskers"), Chef::Resource::ZenMaster.new('dtz')] + @run_context.resource_collection.all_resources.replace(@all_resources) + end + + it "lists all resources" do + @run_status.all_resources.should == @all_resources + end + + it "has no updated resources" do + @run_status.updated_resources.should be_empty + end + + it "includes the list of all resources in its hash form" do + @run_status.to_hash[:all_resources].should == @all_resources + @run_status.to_hash[:updated_resources].should be_empty + end + + describe "and some have been updated" do + before do + @all_resources.first.updated = true + end + + it "lists the updated resources" do + @run_status.updated_resources.should == [@all_resources.first] + end + + it "includes the list of updated resources in its hash form" do + @run_status.to_hash[:updated_resources].should == [@all_resources.first] + end + end + end + + describe "when the run failed" do + before do + @exception = Exception.new("just testing") + @backtrace = caller + @exception.set_backtrace(@backtrace) + @run_status.exception = @exception + end + + it "stores the exception" do + @run_status.exception.should equal(@exception) + end + + it "stores the backtrace" do + @run_status.backtrace.should == @backtrace + end + + it "says the run was not successful" do + @run_status.success?.should be_false + @run_status.failed?.should be_true + end + + it "converts to a hash including the exception information" do + @run_status.to_hash[:success].should be_false + @run_status.to_hash[:exception].should == "Exception: just testing" + @run_status.to_hash[:backtrace].should == @backtrace + end + end + end +end diff --git a/spec/unit/runner_spec.rb b/spec/unit/runner_spec.rb new file mode 100644 index 0000000000..388596e350 --- /dev/null +++ b/spec/unit/runner_spec.rb @@ -0,0 +1,402 @@ + +# 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' + +class SnitchyProvider < Chef::Provider + def self.all_actions_called + @all_actions_called ||= [] + end + + def self.action_called(action) + all_actions_called << action + end + + def self.clear_action_record + @all_actions_called = nil + end + + def load_current_resource + true + end + + def action_first_action + @new_resource.updated_by_last_action(true) + self.class.action_called(:first) + end + + def action_second_action + @new_resource.updated_by_last_action(true) + self.class.action_called(:second) + end + + def action_third_action + @new_resource.updated_by_last_action(true) + self.class.action_called(:third) + end + +end + +class FailureResource < Chef::Resource + + attr_accessor :action + + def initialize(*args) + super + @action = :fail + end + + def provider + FailureProvider + end +end + +class FailureProvider < Chef::Provider + + class ChefClientFail < StandardError; end + + def load_current_resource + true + end + + def action_fail + raise ChefClientFail, "chef had an error of some sort" + end +end + +describe Chef::Runner do + + before(:each) do + @node = Chef::Node.new + @node.name "latte" + @node.automatic[:platform] = "mac_os_x" + @node.automatic[:platform_version] = "10.5.1" + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, Chef::CookbookCollection.new({}), @events) + @first_resource = Chef::Resource::Cat.new("loulou1", @run_context) + @run_context.resource_collection << @first_resource + Chef::Platform.set( + :resource => :cat, + :provider => Chef::Provider::SnakeOil + ) + @runner = Chef::Runner.new(@run_context) + end + + it "should pass each resource in the collection to a provider" do + @run_context.resource_collection.should_receive(:execute_each_resource).once + @runner.converge + end + + it "should use the provider specified by the resource (if it has one)" do + provider = Chef::Provider::Easy.new(@run_context.resource_collection[0], @run_context) + # Expect provider to be called twice, because will fall back to old provider lookup + @run_context.resource_collection[0].should_receive(:provider).twice.and_return(Chef::Provider::Easy) + Chef::Provider::Easy.should_receive(:new).once.and_return(provider) + @runner.converge + end + + it "should use the platform provider if it has one" do + Chef::Platform.should_receive(:find_provider_for_node).once.and_return(Chef::Provider::SnakeOil) + @runner.converge + end + + it "should run the action for each resource" do + Chef::Platform.should_receive(:find_provider_for_node).once.and_return(Chef::Provider::SnakeOil) + provider = Chef::Provider::SnakeOil.new(@run_context.resource_collection[0], @run_context) + provider.should_receive(:action_sell).once.and_return(true) + Chef::Provider::SnakeOil.should_receive(:new).once.and_return(provider) + @runner.converge + end + + it "should raise exceptions as thrown by a provider" do + provider = Chef::Provider::SnakeOil.new(@run_context.resource_collection[0], @run_context) + Chef::Provider::SnakeOil.stub!(:new).once.and_return(provider) + provider.stub!(:action_sell).once.and_raise(ArgumentError) + lambda { @runner.converge }.should raise_error(ArgumentError) + end + + it "should not raise exceptions thrown by providers if the resource has ignore_failure set to true" do + @run_context.resource_collection[0].stub!(:ignore_failure).and_return(true) + provider = Chef::Provider::SnakeOil.new(@run_context.resource_collection[0], @run_context) + Chef::Provider::SnakeOil.stub!(:new).once.and_return(provider) + provider.stub!(:action_sell).once.and_raise(ArgumentError) + lambda { @runner.converge }.should_not raise_error(ArgumentError) + end + + it "should retry with the specified delay if retries are specified" do + @first_resource.retries 3 + provider = Chef::Provider::SnakeOil.new(@run_context.resource_collection[0], @run_context) + Chef::Provider::SnakeOil.stub!(:new).once.and_return(provider) + provider.stub!(:action_sell).and_raise(ArgumentError) + @first_resource.should_receive(:sleep).with(2).exactly(3).times + lambda { @runner.converge }.should raise_error(ArgumentError) + end + + it "should execute immediate actions on changed resources" do + notifying_resource = Chef::Resource::Cat.new("peanut", @run_context) + notifying_resource.action = :purr # only action that will set updated on the resource + + @run_context.resource_collection << notifying_resource + @first_resource.action = :nothing # won't be updated unless notified by other resource + + notifying_resource.notifies(:purr, @first_resource, :immediately) + + @runner.converge + + @first_resource.should be_updated + end + + it "should follow a chain of actions" do + @first_resource.action = :nothing + + middle_resource = Chef::Resource::Cat.new("peanut", @run_context) + middle_resource.action = :nothing + @run_context.resource_collection << middle_resource + middle_resource.notifies(:purr, @first_resource, :immediately) + + last_resource = Chef::Resource::Cat.new("snuffles", @run_context) + last_resource.action = :purr + @run_context.resource_collection << last_resource + last_resource.notifies(:purr, middle_resource, :immediately) + + @runner.converge + + last_resource.should be_updated # by action(:purr) + middle_resource.should be_updated # by notification from last_resource + @first_resource.should be_updated # by notification from middle_resource + end + + it "should execute delayed actions on changed resources" do + @first_resource.action = :nothing + second_resource = Chef::Resource::Cat.new("peanut", @run_context) + second_resource.action = :purr + + @run_context.resource_collection << second_resource + second_resource.notifies(:purr, @first_resource, :delayed) + + @runner.converge + + @first_resource.should be_updated + end + + it "should execute delayed notifications when a failure occurs in the chef client run" do + @first_resource.action = :nothing + second_resource = Chef::Resource::Cat.new("peanut", @run_context) + second_resource.action = :purr + + @run_context.resource_collection << second_resource + second_resource.notifies(:purr, @first_resource, :delayed) + + third_resource = FailureResource.new("explode", @run_context) + @run_context.resource_collection << third_resource + + lambda {@runner.converge}.should raise_error(FailureProvider::ChefClientFail) + + @first_resource.should be_updated + end + + it "should execute delayed notifications when a failure occurs in a notification" do + @first_resource.action = :nothing + second_resource = Chef::Resource::Cat.new("peanut", @run_context) + second_resource.action = :purr + + @run_context.resource_collection << second_resource + + third_resource = FailureResource.new("explode", @run_context) + third_resource.action = :nothing + @run_context.resource_collection << third_resource + + second_resource.notifies(:fail, third_resource, :delayed) + second_resource.notifies(:purr, @first_resource, :delayed) + + lambda {@runner.converge}.should raise_error(FailureProvider::ChefClientFail) + + @first_resource.should be_updated + end + + it "should execute delayed notifications when a failure occurs in multiple notifications" do + @first_resource.action = :nothing + second_resource = Chef::Resource::Cat.new("peanut", @run_context) + second_resource.action = :purr + + @run_context.resource_collection << second_resource + + third_resource = FailureResource.new("explode", @run_context) + third_resource.action = :nothing + @run_context.resource_collection << third_resource + + fourth_resource = FailureResource.new("explode again", @run_context) + fourth_resource.action = :nothing + @run_context.resource_collection << fourth_resource + + second_resource.notifies(:fail, third_resource, :delayed) + second_resource.notifies(:fail, fourth_resource, :delayed) + second_resource.notifies(:purr, @first_resource, :delayed) + + exception = nil + begin + @runner.converge + rescue => e + exception = e + end + exception.should be_a(Chef::Exceptions::MultipleFailures) + + expected_message =<<-E +Multiple failures occurred: +* FailureProvider::ChefClientFail occurred in delayed notification: [explode] (dynamically defined) had an error: FailureProvider::ChefClientFail: chef had an error of some sort +* FailureProvider::ChefClientFail occurred in delayed notification: [explode again] (dynamically defined) had an error: FailureProvider::ChefClientFail: chef had an error of some sort +E + exception.message.should == expected_message + + @first_resource.should be_updated + end + + it "does not duplicate delayed notifications" do + SnitchyProvider.clear_action_record + + Chef::Platform.set( + :resource => :cat, + :provider => SnitchyProvider + ) + + @first_resource.action = :nothing + + second_resource = Chef::Resource::Cat.new("peanut", @run_context) + second_resource.action = :first_action + @run_context.resource_collection << second_resource + + third_resource = Chef::Resource::Cat.new("snickers", @run_context) + third_resource.action = :first_action + @run_context.resource_collection << third_resource + + second_resource.notifies(:second_action, @first_resource, :delayed) + second_resource.notifies(:third_action, @first_resource, :delayed) + + third_resource.notifies(:second_action, @first_resource, :delayed) + third_resource.notifies(:third_action, @first_resource, :delayed) + + @runner.converge + # resources 2 and 3 call :first_action in the course of normal resource + # execution, and schedule delayed actions :second and :third on the first + # resource. The duplicate actions should "collapse" to a single notification + # and order should be preserved. + SnitchyProvider.all_actions_called.should == [:first, :first, :second, :third] + end + + it "executes delayed notifications in the order they were declared" do + SnitchyProvider.clear_action_record + + Chef::Platform.set( + :resource => :cat, + :provider => SnitchyProvider + ) + + @first_resource.action = :nothing + + second_resource = Chef::Resource::Cat.new("peanut", @run_context) + second_resource.action = :first_action + @run_context.resource_collection << second_resource + + third_resource = Chef::Resource::Cat.new("snickers", @run_context) + third_resource.action = :first_action + @run_context.resource_collection << third_resource + + second_resource.notifies(:second_action, @first_resource, :delayed) + second_resource.notifies(:second_action, @first_resource, :delayed) + + third_resource.notifies(:third_action, @first_resource, :delayed) + third_resource.notifies(:third_action, @first_resource, :delayed) + + @runner.converge + SnitchyProvider.all_actions_called.should == [:first, :first, :second, :third] + end + + it "does not fire notifications if the resource was not updated by the last action executed" do + # REGRESSION TEST FOR CHEF-1452 + SnitchyProvider.clear_action_record + + Chef::Platform.set( + :resource => :cat, + :provider => SnitchyProvider + ) + + @first_resource.action = :first_action + + second_resource = Chef::Resource::Cat.new("peanut", @run_context) + second_resource.action = :nothing + @run_context.resource_collection << second_resource + + third_resource = Chef::Resource::Cat.new("snickers", @run_context) + third_resource.action = :nothing + @run_context.resource_collection << third_resource + + @first_resource.notifies(:second_action, second_resource, :immediately) + second_resource.notifies(:third_action, third_resource, :immediately) + + @runner.converge + + # All of the resources should only fire once: + SnitchyProvider.all_actions_called.should == [:first, :second, :third] + + # all of the resources should be marked as updated for reporting purposes + @first_resource.should be_updated + second_resource.should be_updated + third_resource.should be_updated + end + + it "should check a resource's only_if and not_if if notified by another resource" do + @first_resource.action = :nothing + + only_if_called_times = 0 + @first_resource.only_if {only_if_called_times += 1; true} + + not_if_called_times = 0 + @first_resource.not_if {not_if_called_times += 1; false} + + second_resource = Chef::Resource::Cat.new("carmel", @run_context) + @run_context.resource_collection << second_resource + second_resource.notifies(:purr, @first_resource, :delayed) + second_resource.action = :purr + + # hits only_if first time when the resource is run in order, second on notify + @runner.converge + + only_if_called_times.should == 2 + not_if_called_times.should == 2 + end + + it "should resolve resource references in notifications when resources are defined lazily" do + @first_resource.action = :nothing + + lazy_resources = lambda { + last_resource = Chef::Resource::Cat.new("peanut", @run_context) + @run_context.resource_collection << last_resource + last_resource.notifies(:purr, @first_resource.to_s, :delayed) + last_resource.action = :purr + } + second_resource = Chef::Resource::RubyBlock.new("myblock", @run_context) + @run_context.resource_collection << second_resource + second_resource.block { lazy_resources.call } + + @runner.converge + + @first_resource.should be_updated + end + +end + diff --git a/spec/unit/scan_access_control_spec.rb b/spec/unit/scan_access_control_spec.rb new file mode 100644 index 0000000000..00e2e2669a --- /dev/null +++ b/spec/unit/scan_access_control_spec.rb @@ -0,0 +1,182 @@ +# Author:: Daniel DeLeo (<dan@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 File.expand_path("../../spec_helper", __FILE__) +require 'chef/scan_access_control' + +describe Chef::ScanAccessControl do + + before do + @new_resource = Chef::Resource::File.new("/tmp/foo/bar/baz/qux") + @current_resource = Chef::Resource::File.new(@new_resource.path) + @scanner = Chef::ScanAccessControl.new(@new_resource, @current_resource) + end + + describe "when the fs entity does not exist" do + + before do + @new_resource.tap do |f| + f.owner("root") + f.group("root") + f.mode('0755') + end + @scanner.set_all! + end + + it "does not set any fields on the current resource" do + @current_resource.owner.should be_nil + @current_resource.group.should be_nil + @current_resource.mode.should be_nil + end + + end + + describe "when the fs entity exists" do + + before do + @stat = mock("File::Stat for #{@new_resource.path}", :uid => 0, :gid => 0, :mode => 00100644) + File.should_receive(:stat).with(@new_resource.path).and_return(@stat) + File.should_receive(:exist?).with(@new_resource.path).and_return(true) + end + + describe "when new_resource does not specify mode, user or group" do + # these tests are necessary for minitest-chef-handler to use as an API, see CHEF-3235 + before do + @scanner.set_all! + end + + it "sets the mode of the current resource to the current mode as a String" do + @current_resource.mode.should == "644" + end + + context "on unix", :unix_only do + it "sets the group of the current resource to the current group as a String" do + @current_resource.group.should == Etc.getgrgid(0).name + end + + it "sets the owner of the current resource to the current owner as a String" do + @current_resource.user.should == "root" + end + end + + context "on windows", :windows_only do + it "sets the group of the current resource to the current group as a String" do + @current_resource.group.should == 0 + end + + it "sets the owner of the current resource to the current owner as a String" do + @current_resource.user.should == 0 + end + end + end + + describe "when new_resource specifies the mode with a string" do + before do + @new_resource.mode("0755") + @scanner.set_all! + end + + it "sets the mode of the current resource to the file's current mode as a string" do + @current_resource.mode.should == "644" + end + end + + describe "when new_resource specified the mode with an integer" do + before do + @new_resource.mode(00755) + @scanner.set_all! + end + + it "sets the mode of the current resource to the current mode as an integer" do + @current_resource.mode.should == 00644 + end + + end + + describe "when new_resource specifies the user with a UID" do + + before do + @new_resource.user(0) + @scanner.set_all! + end + + it "sets the owner of current_resource to the UID of the current owner" do + @current_resource.user.should == 0 + end + end + + describe "when new_resource specifies the user with a username" do + + before do + @new_resource.user("root") + end + + it "sets the owner of current_resource to the username of the current owner" do + @root_passwd = mock("Struct::Passwd for uid 0", :name => "root") + Etc.should_receive(:getpwuid).with(0).and_return(@root_passwd) + @scanner.set_all! + + @current_resource.user.should == "root" + end + + describe "and there is no passwd entry for the user" do + it "sets the owner of the current_resource to the UID" do + Etc.should_receive(:getpwuid).with(0).and_raise(ArgumentError) + @scanner.set_all! + @current_resource.user.should == 0 + end + end + end + + describe "when new_resource specifies the group with a GID" do + + before do + @new_resource.group(0) + @scanner.set_all! + end + + it "sets the group of the current_resource to the gid of the current owner" do + @current_resource.group.should == 0 + end + + end + + describe "when new_resource specifies the group with a group name" do + before do + @new_resource.group("wheel") + end + + it "sets the group of the current resource to the group name" do + @group_entry = mock("Struct::Group for wheel", :name => "wheel") + Etc.should_receive(:getgrgid).with(0).and_return(@group_entry) + @scanner.set_all! + + @current_resource.group.should == "wheel" + end + + describe "and there is no group entry for the group" do + it "sets the current_resource's group to the GID" do + Etc.should_receive(:getgrgid).with(0).and_raise(ArgumentError) + @scanner.set_all! + @current_resource.group.should == 0 + end + end + + end + end +end + diff --git a/spec/unit/search/query_spec.rb b/spec/unit/search/query_spec.rb new file mode 100644 index 0000000000..e32bed697c --- /dev/null +++ b/spec/unit/search/query_spec.rb @@ -0,0 +1,99 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009,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 'chef/search/query' + +describe Chef::Search::Query do + before(:each) do + @rest = mock("Chef::REST") + Chef::REST.stub!(:new).and_return(@rest) + @query = Chef::Search::Query.new + end + + describe "search" do + before(:each) do + @response = { + "rows" => [ + { "id" => "for you" }, + { "id" => "hip hop" }, + { "id" => "thought was down by law for you" }, + { "id" => "kept it hard core for you" }, + ], + "start" => 0, + "total" => 4 + } + @rest.stub!(:get_rest).and_return(@response) + end + + it "should accept a type as the first argument" do + lambda { @query.search("foo") }.should_not raise_error(ArgumentError) + lambda { @query.search(:foo) }.should_not raise_error(ArgumentError) + lambda { @query.search(Hash.new) }.should raise_error(ArgumentError) + end + + it "should query for every object of a type by default" do + @rest.should_receive(:get_rest).with("search/foo?q=*:*&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000").and_return(@response) + @query = Chef::Search::Query.new + @query.search(:foo) + end + + it "should allow a custom query" do + @rest.should_receive(:get_rest).with("search/foo?q=gorilla:dundee&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000").and_return(@response) + @query = Chef::Search::Query.new + @query.search(:foo, "gorilla:dundee") + end + + it "should let you set a sort order" do + @rest.should_receive(:get_rest).with("search/foo?q=gorilla:dundee&sort=id%20desc&start=0&rows=1000").and_return(@response) + @query = Chef::Search::Query.new + @query.search(:foo, "gorilla:dundee", "id desc") + end + + it "should let you set a starting object" do + @rest.should_receive(:get_rest).with("search/foo?q=gorilla:dundee&sort=id%20desc&start=2&rows=1000").and_return(@response) + @query = Chef::Search::Query.new + @query.search(:foo, "gorilla:dundee", "id desc", 2) + end + + it "should let you set how many rows to return" do + @rest.should_receive(:get_rest).with("search/foo?q=gorilla:dundee&sort=id%20desc&start=2&rows=40").and_return(@response) + @query = Chef::Search::Query.new + @query.search(:foo, "gorilla:dundee", "id desc", 2, 40) + end + + it "should return the raw rows, start, and total if no block is passed" do + rows, start, total = @query.search(:foo) + rows.should equal(@response["rows"]) + start.should equal(@response["start"]) + total.should equal(@response["total"]) + end + + it "should call a block for each object in the response" do + @call_me = mock("blocky") + @response["rows"].each { |r| @call_me.should_receive(:do).with(r) } + @query.search(:foo) { |r| @call_me.do(r) } + end + + it "should page through the responses" do + @call_me = mock("blocky") + @response["rows"].each { |r| @call_me.should_receive(:do).with(r) } + @query.search(:foo, "*:*", nil, 0, 1) { |r| @call_me.do(r) } + end + end +end diff --git a/spec/unit/shell/model_wrapper_spec.rb b/spec/unit/shell/model_wrapper_spec.rb new file mode 100644 index 0000000000..35dc591edc --- /dev/null +++ b/spec/unit/shell/model_wrapper_spec.rb @@ -0,0 +1,97 @@ +# +# 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 Shell::ModelWrapper do + before do + @model = OpenStruct.new(:name=>"Chef::Node") + @wrapper = Shell::ModelWrapper.new(@model) + end + + describe "when created with an explicit model_symbol" do + before do + @model = OpenStruct.new(:name=>"Chef::ApiClient") + @wrapper = Shell::ModelWrapper.new(@model, :client) + end + + it "uses the explicit model symbol" do + @wrapper.model_symbol.should == :client + end + end + + it "determines the model symbol from the class name" do + @wrapper.model_symbol.should == :node + end + + describe "when listing objects" do + before do + @node_1 = Chef::Node.new + @node_1.name("sammich") + @node_2 = Chef::Node.new + @node_2.name("yummy") + @server_response = {:node_1 => @node_1, :node_2 => @node_2} + @wrapper = Shell::ModelWrapper.new(Chef::Node) + Chef::Node.stub(:list).and_return(@server_response) + end + + it "lists fully inflated objects without the resource IDs" do + @wrapper.all.should have(2).nodes + @wrapper.all.should include(@node_1, @node_2) + end + + it "maps the listed nodes when given a block" do + @wrapper.all {|n| n.name }.sort.reverse.should == %w{yummy sammich} + end + end + + describe "when searching for objects" do + before do + @node_1 = Chef::Node.new + @node_1.name("sammich") + @node_2 = Chef::Node.new + @node_2.name("yummy") + @server_response = {:node_1 => @node_1, :node_2 => @node_2} + @wrapper = Shell::ModelWrapper.new(Chef::Node) + + # Creating a Chef::Search::Query object tries to read the private key... + @searcher = mock("Chef::Search::Query #{__FILE__}:#{__LINE__}") + Chef::Search::Query.stub!(:new).and_return(@searcher) + end + + it "falls back to listing the objects when the 'query' is :all" do + Chef::Node.stub(:list).and_return(@server_response) + @wrapper.find(:all).should include(@node_1, @node_2) + end + + it "searches for objects using the given query string" do + @searcher.should_receive(:search).with(:node, 'name:app*').and_yield(@node_1).and_yield(@node_2) + @wrapper.find("name:app*").should include(@node_1, @node_2) + end + + it "creates a 'AND'-joined query string from a HASH" do + # Hash order woes + @searcher.should_receive(:search).with(:node, 'name:app* AND name:app*').and_yield(@node_1).and_yield(@node_2) + @wrapper.find(:name=>"app*",'name'=>"app*").should include(@node_1, @node_2) + end + + end + + +end diff --git a/spec/unit/shell/shell_ext_spec.rb b/spec/unit/shell/shell_ext_spec.rb new file mode 100644 index 0000000000..22e9ae674b --- /dev/null +++ b/spec/unit/shell/shell_ext_spec.rb @@ -0,0 +1,153 @@ +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2009 Daniel DeLeo +# 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 Shell::Extensions do + describe "extending object for top level methods" do + + before do + @shell_client = TestableShellSession.instance + Shell.stub!(:session).and_return(@shell_client) + @job_manager = TestJobManager.new + @root_context = Object.new + @root_context.instance_eval(&ObjectTestHarness) + Shell::Extensions.extend_context_object(@root_context) + @root_context.conf = mock("irbconf") + end + + it "finds a subsession in irb for an object" do + target_context_obj = Chef::Node.new + + irb_context = mock("context", :main => target_context_obj) + irb_session = mock("irb session", :context => irb_context) + @job_manager.jobs = [[:thread, irb_session]] + @root_context.stub!(:jobs).and_return(@job_manager) + @root_context.ensure_session_select_defined + @root_context.jobs.select_shell_session(target_context_obj).should == irb_session + @root_context.jobs.select_shell_session(:idontexist).should be_nil + end + + it "finds, then switches to a session" do + @job_manager.jobs = [] + @root_context.stub!(:ensure_session_select_defined) + @root_context.stub!(:jobs).and_return(@job_manager) + @job_manager.should_receive(:select_shell_session).and_return(:the_shell_session) + @job_manager.should_receive(:switch).with(:the_shell_session) + @root_context.find_or_create_session_for(:foo) + end + + it "creates a new session if an existing one isn't found" do + @job_manager.jobs = [] + @root_context.stub!(:jobs).and_return(@job_manager) + @job_manager.stub!(:select_shell_session).and_return(nil) + @root_context.should_receive(:irb).with(:foo) + @root_context.find_or_create_session_for(:foo) + end + + it "switches to recipe context" do + @root_context.should respond_to(:recipe_mode) + @shell_client.recipe = :monkeyTime + @root_context.should_receive(:find_or_create_session_for).with(:monkeyTime) + @root_context.recipe_mode + end + + it "switches to attribute context" do + @root_context.should respond_to(:attributes_mode) + @shell_client.node = "monkeyNodeTime" + @root_context.should_receive(:find_or_create_session_for).with("monkeyNodeTime") + @root_context.attributes_mode + end + + it "has a help command" do + @root_context.should respond_to(:help) + end + + it "turns irb tracing on and off" do + @root_context.should respond_to(:trace) + @root_context.conf.should_receive(:use_tracer=).with(true) + @root_context.stub!(:tracing?) + @root_context.tracing :on + end + + it "says if tracing is on or off" do + @root_context.conf.stub!(:use_tracer).and_return(true) + @root_context.should_receive(:puts).with("tracing is on") + @root_context.tracing? + end + + it "prints node attributes" do + node = mock("node", :attribute => {:foo => :bar}) + @shell_client.node = node + @root_context.should_receive(:pp).with({:foo => :bar}) + @root_context.ohai + @root_context.should_receive(:pp).with(:bar) + @root_context.ohai(:foo) + end + + it "resets the recipe and reloads ohai data" do + @shell_client.should_receive(:reset!) + @root_context.reset + end + + it "turns irb echo on and off" do + @root_context.conf.should_receive(:echo=).with(true) + @root_context.echo :on + end + + it "says if echo is on or off" do + @root_context.conf.stub!(:echo).and_return(true) + @root_context.should_receive(:puts).with("echo is on") + @root_context.echo? + end + + it "gives access to the stepable iterator" do + Shell::StandAloneSession.instance.stub!(:reset!) + Shell.session.stub!(:rebuild_context) + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(Chef::Node.new, {}, events) + run_context.resource_collection.instance_variable_set(:@iterator, :the_iterator) + Shell.session.run_context = run_context + @root_context.chef_run.should == :the_iterator + end + + it "lists directory contents" do + entries = %w{. .. someFile} + Dir.should_receive(:entries).with("/tmp").and_return(entries) + @root_context.ls "/tmp" + end + + end + + describe "extending the recipe object" do + + before do + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(Chef::Node.new, {}, @events) + @recipe_object = Chef::Recipe.new(nil, nil, @run_context) + Shell::Extensions.extend_context_recipe(@recipe_object) + end + + it "gives a list of the resources" do + resource = @recipe_object.file("foo") + @recipe_object.should_receive(:pp).with(["file[foo]"]) + @recipe_object.resources + end + + end +end diff --git a/spec/unit/shell/shell_session_spec.rb b/spec/unit/shell/shell_session_spec.rb new file mode 100644 index 0000000000..3d4081e583 --- /dev/null +++ b/spec/unit/shell/shell_session_spec.rb @@ -0,0 +1,141 @@ +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2009 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 "ostruct" + + +class TestableShellSession < Shell::ShellSession + + def rebuild_node + nil + end + + def rebuild_collection + nil + end + + def loading + nil + end + + def loading_complete + nil + end + +end + +describe Shell::ShellSession do + + it "is a singleton object" do + Shell::ShellSession.should include(Singleton) + end + +end + +describe Shell::StandAloneSession do + before do + @session = Shell::StandAloneSession.instance + @node = @session.node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = @session.run_context = Chef::RunContext.new(@node, {}, @events) + @recipe = @session.recipe = Chef::Recipe.new(nil, nil, @run_context) + Shell::Extensions.extend_context_recipe(@recipe) + end + + it "has a run_context" do + @session.run_context.should equal(@run_context) + end + + it "returns a collection based on it's standalone recipe file" do + @session.resource_collection.should == @recipe.run_context.resource_collection + end + + it "gives nil for the definitions (for now)" do + @session.definitions.should be_nil + end + + it "gives nil for the cookbook_loader" do + @session.cookbook_loader.should be_nil + end + + it "runs chef with the standalone recipe" do + @session.stub!(:node_built?).and_return(true) + Chef::Log.stub!(:level) + chef_runner = mock("Chef::Runner.new", :converge => :converged) + # pre-heat resource collection cache + @session.resource_collection + + Chef::Runner.should_receive(:new).with(@session.recipe.run_context).and_return(chef_runner) + @recipe.run_chef.should == :converged + end + +end + +describe Shell::SoloSession do + before do + Chef::Config[:shell_solo] = true + @session = Shell::SoloSession.instance + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = @session.run_context = Chef::RunContext.new(@node, {}, @events) + @session.node = @node + @recipe = @session.recipe = Chef::Recipe.new(nil, nil, @run_context) + Shell::Extensions.extend_context_recipe(@recipe) + end + + after do + Chef::Config[:shell_solo] = nil + end + + it "returns a collection based on it's compilation object and the extra recipe provided by chef-shell" do + @session.stub!(:node_built?).and_return(true) + kitteh = Chef::Resource::Cat.new("keyboard") + @recipe.run_context.resource_collection << kitteh + @session.resource_collection.should include(kitteh) + end + + it "returns definitions from it's compilation object" do + @session.definitions.should == @run_context.definitions + end + + it "keeps json attribs and passes them to the node for consumption" do + @session.node_attributes = {"besnard_lakes" => "are_the_dark_horse"} + @session.node.besnard_lakes.should == "are_the_dark_horse" + #pending "1) keep attribs in an ivar 2) pass them to the node 3) feed them to the node on reset" + end + + it "generates it's resource collection from the compiled cookbooks and the ad hoc recipe" do + @session.stub!(:node_built?).and_return(true) + kitteh_cat = Chef::Resource::Cat.new("kitteh") + @run_context.resource_collection << kitteh_cat + keyboard_cat = Chef::Resource::Cat.new("keyboard_cat") + @recipe.run_context.resource_collection << keyboard_cat + #@session.rebuild_collection + @session.resource_collection.should include(kitteh_cat, keyboard_cat) + end + + it "runs chef with a resource collection from the compiled cookbooks" do + @session.stub!(:node_built?).and_return(true) + Chef::Log.stub!(:level) + chef_runner = mock("Chef::Runner.new", :converge => :converged) + Chef::Runner.should_receive(:new).with(an_instance_of(Chef::RunContext)).and_return(chef_runner) + + @recipe.run_chef.should == :converged + end + +end diff --git a/spec/unit/shell_out_spec.rb b/spec/unit/shell_out_spec.rb new file mode 100644 index 0000000000..1330dd16de --- /dev/null +++ b/spec/unit/shell_out_spec.rb @@ -0,0 +1,18 @@ +require File.expand_path('../../spec_helper', __FILE__) + +describe "Chef::ShellOut deprecation notices" do + it "logs a warning when initializing a new Chef::ShellOut object" do + Chef::Log.should_receive(:warn).with("Chef::ShellOut is deprecated, please use Mixlib::ShellOut") + Chef::Log.should_receive(:warn).with(/Called from\:/) + Chef::ShellOut.new("pwd") + end +end + +describe "Chef::Exceptions::ShellCommandFailed deprecation notices" do + + it "logs a warning when referencing the constant Chef::Exceptions::ShellCommandFailed" do + Chef::Log.should_receive(:warn).with("Chef::Exceptions::ShellCommandFailed is deprecated, use Mixlib::ShellOut::ShellCommandFailed") + Chef::Log.should_receive(:warn).with(/Called from\:/) + Chef::Exceptions::ShellCommandFailed + end +end diff --git a/spec/unit/shell_spec.rb b/spec/unit/shell_spec.rb new file mode 100644 index 0000000000..fd4241732f --- /dev/null +++ b/spec/unit/shell_spec.rb @@ -0,0 +1,161 @@ +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2009 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 "ostruct" + +ObjectTestHarness = Proc.new do + extend Shell::Extensions::ObjectCoreExtensions + + def conf=(new_conf) + @conf = new_conf + end + + def conf + @conf + end + + desc "rspecin'" + def rspec_method + end +end + +class TestJobManager + attr_accessor :jobs +end + +describe Shell do + + before do + Shell.irb_conf = {} + Shell::ShellSession.instance.rspec_reset + Shell::ShellSession.instance.stub!(:reset!) + end + + describe "reporting its status" do + + it "alway says it is running" do + Shell.should be_running + end + + end + + describe "configuring IRB" do + it "configures irb history" do + Shell.configure_irb + Shell.irb_conf[:HISTORY_FILE].should == "~/.chef/chef_shell_history" + Shell.irb_conf[:SAVE_HISTORY].should == 1000 + end + + it "has a prompt like ``chef > '' in the default context" do + Shell.configure_irb + + conf = OpenStruct.new + conf.main = Object.new + conf.main.instance_eval(&ObjectTestHarness) + Shell.irb_conf[:IRB_RC].call(conf) + conf.prompt_c.should == "chef > " + conf.return_format.should == " => %s \n" + conf.prompt_i.should == "chef > " + conf.prompt_n.should == "chef ?> " + conf.prompt_s.should == "chef%l> " + + end + + it "has a prompt like ``chef:recipe > '' in recipe context" do + Shell.configure_irb + + conf = OpenStruct.new + events = Chef::EventDispatch::Dispatcher.new + conf.main = Chef::Recipe.new(nil,nil,Chef::RunContext.new(Chef::Node.new, {}, events)) + Shell.irb_conf[:IRB_RC].call(conf) + conf.prompt_c.should == "chef:recipe > " + conf.prompt_i.should == "chef:recipe > " + conf.prompt_n.should == "chef:recipe ?> " + conf.prompt_s.should == "chef:recipe%l> " + end + + it "has a prompt like ``chef:attributes > '' in attributes/node context" do + Shell.configure_irb + + conf = OpenStruct.new + conf.main = Chef::Node.new + Shell.irb_conf[:IRB_RC].call(conf) + conf.prompt_c.should == "chef:attributes > " + conf.prompt_i.should == "chef:attributes > " + conf.prompt_n.should == "chef:attributes ?> " + conf.prompt_s.should == "chef:attributes%l> " + end + + end + + describe "convenience macros for creating the chef object" do + + before do + @chef_object = Object.new + @chef_object.instance_eval(&ObjectTestHarness) + end + + it "creates help text for methods with descriptions" do + @chef_object.help_descriptions.should == [Shell::Extensions::Help.new("rspec_method", "rspecin'", nil)] + end + + it "adds help text when a new method is described then defined" do + describe_define =<<-EVAL + desc "foo2the Bar" + def baz + end + EVAL + @chef_object.instance_eval describe_define + @chef_object.help_descriptions.should == [Shell::Extensions::Help.new("rspec_method", "rspecin'"), + Shell::Extensions::Help.new("baz", "foo2the Bar")] + end + + it "adds help text for subcommands" do + describe_define =<<-EVAL + subcommands :baz_obj_command => "something you can do with baz.baz_obj_command" + def baz + end + EVAL + @chef_object.instance_eval describe_define + expected_help_text_fragments = [Shell::Extensions::Help.new("rspec_method", "rspecin'")] + expected_help_text_fragments << Shell::Extensions::Help.new("baz.baz_obj_command", "something you can do with baz.baz_obj_command") + @chef_object.help_descriptions.should == expected_help_text_fragments + end + + it "doesn't add previous subcommand help to commands defined afterward" do + describe_define =<<-EVAL + desc "swingFromTree" + def monkey_time + end + + def super_monkey_time + end + + EVAL + @chef_object.instance_eval describe_define + @chef_object.help_descriptions.should have(2).descriptions + @chef_object.help_descriptions.select {|h| h.cmd == "super_monkey_time" }.should be_empty + end + + it "creates a help banner with the command descriptions" do + @chef_object.help_banner.should match(/^\|\ Command[\s]+\|\ Description[\s]*$/) + @chef_object.help_banner.should match(/^\|\ rspec_method[\s]+\|\ rspecin\'[\s]*$/) + end + end + +end diff --git a/spec/unit/util/file_edit_spec.rb b/spec/unit/util/file_edit_spec.rb new file mode 100644 index 0000000000..3cf60172d9 --- /dev/null +++ b/spec/unit/util/file_edit_spec.rb @@ -0,0 +1,135 @@ +# +# Author:: Nuo Yan (<nuo@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::Util::FileEdit do + + before(:each) do + + @hosts_content=<<-HOSTS +127.0.0.1 localhost +255.255.255.255 broadcasthost +::1 localhost +fe80::1%lo0 localhost +HOSTS + + @tempfile = Tempfile.open('file_edit_spec') + @tempfile.write(@hosts_content) + @tempfile.close + @fedit = Chef::Util::FileEdit.new(@tempfile.path) + end + + after(:each) do + @tempfile && @tempfile.close! + end + + describe "initialiize" do + it "should create a new Chef::Util::FileEdit object" do + Chef::Util::FileEdit.new(@tempfile.path).should be_kind_of(Chef::Util::FileEdit) + end + + it "should throw an exception if the input file does not exist" do + lambda{Chef::Util::FileEdit.new("nonexistfile")}.should raise_error + end + + it "should throw an exception if the input file is blank" do + lambda do + Chef::Util::FileEdit.new(File.join(CHEF_SPEC_DATA, "filedit", "blank")) + end.should raise_error + end + end + + describe "search_file_replace" do + it "should accept regex passed in as a string (not Regexp object) and replace the match if there is one" do + @fedit.search_file_replace("localhost", "replacement") + @fedit.write_file + newfile = File.new(@tempfile.path).readlines + newfile[0].should match(/replacement/) + end + + it "should accept regex passed in as a Regexp object and replace the match if there is one" do + @fedit.search_file_replace(/localhost/, "replacement") + @fedit.write_file + newfile = File.new(@tempfile.path).readlines + newfile[0].should match(/replacement/) + end + + it "should do nothing if there isn't a match" do + @fedit.search_file_replace(/pattern/, "replacement") + @fedit.write_file + newfile = File.new(@tempfile.path).readlines + newfile[0].should_not match(/replacement/) + end + end + + describe "search_file_replace_line" do + it "should search for match and replace the whole line" do + @fedit.search_file_replace_line(/localhost/, "replacement line") + @fedit.write_file + newfile = File.new(@tempfile.path).readlines + newfile[0].should match(/replacement/) + newfile[0].should_not match(/127/) + end + end + + describe "search_file_delete" do + it "should search for match and delete the match" do + @fedit.search_file_delete(/localhost/) + @fedit.write_file + newfile = File.new(@tempfile.path).readlines + newfile[0].should_not match(/localhost/) + newfile[0].should match(/127/) + end + end + + describe "search_file_delete_line" do + it "should search for match and delete the matching line" do + @fedit.search_file_delete_line(/localhost/) + @fedit.write_file + newfile = File.new(@tempfile.path).readlines + newfile[0].should_not match(/localhost/) + newfile[0].should match(/broadcasthost/) + end + end + + describe "insert_line_after_match" do + it "should search for match and insert the given line after the matching line" do + @fedit.insert_line_after_match(/localhost/, "new line inserted") + @fedit.write_file + newfile = File.new(@tempfile.path).readlines + newfile[1].should match(/new/) + end + end + + describe "insert_line_if_no_match" do + it "should search for match and insert the given line if no line match" do + @fedit.insert_line_if_no_match(/pattern/, "new line inserted") + @fedit.write_file + newfile = File.new(@tempfile.path).readlines + newfile.last.should match(/new/) + end + + it "should do nothing if there is a match" do + @fedit.insert_line_if_no_match(/localhost/, "replacement") + @fedit.write_file + newfile = File.new(@tempfile.path).readlines + newfile[1].should_not match(/replacement/) + end + end +end diff --git a/spec/unit/version_class_spec.rb b/spec/unit/version_class_spec.rb new file mode 100644 index 0000000000..285588b031 --- /dev/null +++ b/spec/unit/version_class_spec.rb @@ -0,0 +1,172 @@ +# +# Author:: Seth Falcon (<seth@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 'chef/version_class' + +describe Chef::Version do + before do + @v0 = Chef::Version.new "0.0.0" + @v123 = Chef::Version.new "1.2.3" + end + + it "should turn itself into a string" do + @v0.to_s.should == "0.0.0" + @v123.to_s.should == "1.2.3" + end + + it "should make a round trip with its string representation" do + a = Chef::Version.new(@v123.to_s) + a.should == @v123 + end + + it "should transform 1.2 to 1.2.0" do + Chef::Version.new("1.2").to_s.should == "1.2.0" + end + + it "should transform 01.002.0003 to 1.2.3" do + a = Chef::Version.new "01.002.0003" + a.should == @v123 + end + + describe "when creating valid Versions" do + good_versions = %w(1.2 1.2.3 1000.80.50000 0.300.25 001.02.00003) + good_versions.each do |v| + it "should accept '#{v}'" do + Chef::Version.new v + end + end + end + + describe "when given bogus input" do + bad_versions = ["1.2.3.4", "1.2.a4", "1", "a", "1.2 3", "1.2 a", + "1 2 3", "1-2-3", "1_2_3", "1.2_3", "1.2-3"] + the_error = Chef::Exceptions::InvalidCookbookVersion + bad_versions.each do |v| + it "should raise #{the_error} when given '#{v}'" do + lambda { Chef::Version.new v }.should raise_error(the_error) + end + end + end + + describe "<=>" do + + it "should equate versions 1.2 and 1.2.0" do + Chef::Version.new("1.2").should == Chef::Version.new("1.2.0") + end + + it "should equate version 1.04 and 1.4" do + Chef::Version.new("1.04").should == Chef::Version.new("1.4") + end + + it "should treat versions as numbers in the right way" do + Chef::Version.new("2.0").should be < Chef::Version.new("11.0") + end + + it "should sort based on the version number" do + examples = [ + # smaller, larger + ["1.0", "2.0"], + ["1.2.3", "1.2.4"], + ["1.2.3", "1.3.0"], + ["1.2.3", "1.3"], + ["1.2.3", "2.1.1"], + ["1.2.3", "2.1"], + ["1.2", "1.2.4"], + ["1.2", "1.3.0"], + ["1.2", "1.3"], + ["1.2", "2.1.1"], + ["1.2", "2.1"] + ] + examples.each do |smaller, larger| + sm = Chef::Version.new(smaller) + lg = Chef::Version.new(larger) + sm.should be < lg + lg.should be > sm + sm.should_not == lg + end + end + + it "should sort an array of versions" do + a = %w{0.0.0 0.0.1 0.1.0 0.1.1 1.0.0 1.1.0 1.1.1}.map do |s| + Chef::Version.new(s) + end + got = a.sort.map {|v| v.to_s } + got.should == %w{0.0.0 0.0.1 0.1.0 0.1.1 1.0.0 1.1.0 1.1.1} + end + + it "should sort an array of versions, part 2" do + a = %w{9.8.7 1.0.0 1.2.3 4.4.6 4.5.6 0.8.6 4.5.5 5.9.8 3.5.7}.map do |s| + Chef::Version.new(s) + end + got = a.sort.map { |v| v.to_s } + got.should == %w{0.8.6 1.0.0 1.2.3 3.5.7 4.4.6 4.5.5 4.5.6 5.9.8 9.8.7} + end + + describe "comparison examples" do + [ + [ "0.0.0", :>, "0.0.0", false ], + [ "0.0.0", :>=, "0.0.0", true ], + [ "0.0.0", :==, "0.0.0", true ], + [ "0.0.0", :<=, "0.0.0", true ], + [ "0.0.0", :<, "0.0.0", false ], + [ "0.0.0", :>, "0.0.1", false ], + [ "0.0.0", :>=, "0.0.1", false ], + [ "0.0.0", :==, "0.0.1", false ], + [ "0.0.0", :<=, "0.0.1", true ], + [ "0.0.0", :<, "0.0.1", true ], + [ "0.0.1", :>, "0.0.1", false ], + [ "0.0.1", :>=, "0.0.1", true ], + [ "0.0.1", :==, "0.0.1", true ], + [ "0.0.1", :<=, "0.0.1", true ], + [ "0.0.1", :<, "0.0.1", false ], + [ "0.1.0", :>, "0.1.0", false ], + [ "0.1.0", :>=, "0.1.0", true ], + [ "0.1.0", :==, "0.1.0", true ], + [ "0.1.0", :<=, "0.1.0", true ], + [ "0.1.0", :<, "0.1.0", false ], + [ "0.1.1", :>, "0.1.1", false ], + [ "0.1.1", :>=, "0.1.1", true ], + [ "0.1.1", :==, "0.1.1", true ], + [ "0.1.1", :<=, "0.1.1", true ], + [ "0.1.1", :<, "0.1.1", false ], + [ "1.0.0", :>, "1.0.0", false ], + [ "1.0.0", :>=, "1.0.0", true ], + [ "1.0.0", :==, "1.0.0", true ], + [ "1.0.0", :<=, "1.0.0", true ], + [ "1.0.0", :<, "1.0.0", false ], + [ "1.0.0", :>, "0.0.1", true ], + [ "1.0.0", :>=, "1.9.2", false ], + [ "1.0.0", :==, "9.7.2", false ], + [ "1.0.0", :<=, "1.9.1", true ], + [ "1.0.0", :<, "1.9.0", true ], + [ "1.2.2", :>, "1.2.1", true ], + [ "1.2.2", :>=, "1.2.1", true ], + [ "1.2.2", :==, "1.2.1", false ], + [ "1.2.2", :<=, "1.2.1", false ], + [ "1.2.2", :<, "1.2.1", false ] + ].each do |spec| + it "(#{spec.first(3).join(' ')}) should be #{spec[3]}" do + got = Chef::Version.new(spec[0]).send(spec[1], + Chef::Version.new(spec[2])) + got.should == spec[3] + end + end + end + end +end + diff --git a/spec/unit/version_constraint_spec.rb b/spec/unit/version_constraint_spec.rb new file mode 100644 index 0000000000..aea7001f2b --- /dev/null +++ b/spec/unit/version_constraint_spec.rb @@ -0,0 +1,134 @@ +# +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright 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 'chef/version_constraint' + +describe Chef::VersionConstraint do + describe "validation" do + bad_version = ["> >", ">= 1.2.z", "> 1.2.3 < 5.0", "> 1.2.3, < 5.0"] + bad_op = ["<3.0.1", ">$ 1.2.3", "! 3.4"] + o_error = Chef::Exceptions::InvalidVersionConstraint + v_error = Chef::Exceptions::InvalidCookbookVersion + bad_version.each do |s| + it "should raise #{v_error} when given #{s}" do + lambda { Chef::VersionConstraint.new s }.should raise_error(v_error) + end + end + bad_op.each do |s| + it "should raise #{o_error} when given #{s}" do + lambda { Chef::VersionConstraint.new s }.should raise_error(o_error) + end + end + + it "should interpret a lone version number as implicit = OP" do + vc = Chef::VersionConstraint.new("1.2.3") + vc.to_s.should == "= 1.2.3" + end + + it "should allow initialization with [] for back compatibility" do + Chef::VersionConstraint.new([]) == Chef::VersionConstraint.new + end + + it "should allow initialization with ['1.2.3'] for back compatibility" do + Chef::VersionConstraint.new(["1.2"]) == Chef::VersionConstraint.new("1.2") + end + + end + + it "should default to >= 0.0.0" do + vc = Chef::VersionConstraint.new + vc.to_s.should == ">= 0.0.0" + end + + it "should default to >= 0.0.0 when initialized with nil" do + Chef::VersionConstraint.new(nil).to_s.should == ">= 0.0.0" + end + + describe "include?" do + describe "handles various input data types" do + before do + @vc = Chef::VersionConstraint.new "> 1.2.3" + end + it "String" do + @vc.should include "1.4" + end + it "Chef::Version" do + @vc.should include Chef::Version.new("1.4") + end + it "Chef::CookbookVersion" do + cv = Chef::CookbookVersion.new("alice") + cv.version = "1.4" + @vc.should include cv + end + end + + it "strictly less than" do + vc = Chef::VersionConstraint.new "< 1.2.3" + vc.should_not include "1.3.0" + vc.should_not include "1.2.3" + vc.should include "1.2.2" + end + + it "strictly greater than" do + vc = Chef::VersionConstraint.new "> 1.2.3" + vc.should include "1.3.0" + vc.should_not include "1.2.3" + vc.should_not include "1.2.2" + end + + it "less than or equal to" do + vc = Chef::VersionConstraint.new "<= 1.2.3" + vc.should_not include "1.3.0" + vc.should include "1.2.3" + vc.should include "1.2.2" + end + + it "greater than or equal to" do + vc = Chef::VersionConstraint.new ">= 1.2.3" + vc.should include "1.3.0" + vc.should include "1.2.3" + vc.should_not include "1.2.2" + end + + it "equal to" do + vc = Chef::VersionConstraint.new "= 1.2.3" + vc.should_not include "1.3.0" + vc.should include "1.2.3" + vc.should_not include "0.3.0" + end + + it "pessimistic ~> x.y.z" do + vc = Chef::VersionConstraint.new "~> 1.2.3" + vc.should include "1.2.3" + vc.should include "1.2.4" + + vc.should_not include "1.2.2" + vc.should_not include "1.3.0" + vc.should_not include "2.0.0" + end + + it "pessimistic ~> x.y" do + vc = Chef::VersionConstraint.new "~> 1.2" + vc.should include "1.3.3" + vc.should include "1.4" + + vc.should_not include "2.2" + vc.should_not include "0.3.0" + end + end +end |