summaryrefslogtreecommitdiff
path: root/spec/unit
diff options
context:
space:
mode:
Diffstat (limited to 'spec/unit')
-rw-r--r--spec/unit/api_client_spec.rb163
-rw-r--r--spec/unit/application/agent_spec.rb0
-rw-r--r--spec/unit/application/client_spec.rb136
-rw-r--r--spec/unit/application/knife_spec.rb152
-rw-r--r--spec/unit/application/server_spec.rb0
-rw-r--r--spec/unit/application/solo_spec.rb167
-rw-r--r--spec/unit/application_spec.rb239
-rw-r--r--spec/unit/checksum/storage/filesystem_spec.rb69
-rw-r--r--spec/unit/checksum_cache_spec.rb209
-rw-r--r--spec/unit/chef_fs/diff_spec.rb328
-rw-r--r--spec/unit/chef_fs/file_pattern_spec.rb526
-rw-r--r--spec/unit/chef_fs/file_system/chef_server_root_dir_spec.rb237
-rw-r--r--spec/unit/chef_fs/file_system/cookbooks_dir_spec.rb568
-rw-r--r--spec/unit/chef_fs/file_system/data_bags_dir_spec.rb220
-rw-r--r--spec/unit/chef_fs/file_system_spec.rb136
-rw-r--r--spec/unit/chef_spec.rb25
-rw-r--r--spec/unit/client_spec.rb290
-rw-r--r--spec/unit/config_spec.rb215
-rw-r--r--spec/unit/cookbook/chefignore_spec.rb38
-rw-r--r--spec/unit/cookbook/metadata_spec.rb627
-rw-r--r--spec/unit/cookbook/synchronizer_spec.rb258
-rw-r--r--spec/unit/cookbook/syntax_check_spec.rb211
-rw-r--r--spec/unit/cookbook_loader_spec.rb207
-rw-r--r--spec/unit/cookbook_manifest_spec.rb554
-rw-r--r--spec/unit/cookbook_spec.rb84
-rw-r--r--spec/unit/cookbook_version_spec.rb307
-rw-r--r--spec/unit/daemon_spec.rb281
-rw-r--r--spec/unit/data_bag_item_spec.rb280
-rw-r--r--spec/unit/data_bag_spec.rb169
-rw-r--r--spec/unit/dsl/data_query_spec.rb66
-rw-r--r--spec/unit/dsl/platfrom_introspection_spec.rb272
-rw-r--r--spec/unit/encrypted_data_bag_item_spec.rb122
-rw-r--r--spec/unit/environment_spec.rb362
-rw-r--r--spec/unit/exceptions_spec.rb73
-rw-r--r--spec/unit/file_access_control_spec.rb282
-rw-r--r--spec/unit/file_cache_spec.rb114
-rw-r--r--spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb202
-rw-r--r--spec/unit/formatters/error_inspectors/cookbook_resolve_error_inspector_spec.rb93
-rw-r--r--spec/unit/formatters/error_inspectors/cookbook_sync_error_inspector_spec.rb43
-rw-r--r--spec/unit/formatters/error_inspectors/node_load_error_inspector_spec.rb27
-rw-r--r--spec/unit/formatters/error_inspectors/registration_error_inspector_spec.rb27
-rw-r--r--spec/unit/formatters/error_inspectors/resource_failure_inspector_spec.rb162
-rw-r--r--spec/unit/formatters/error_inspectors/run_list_expansion_error_inspector_spec.rb93
-rw-r--r--spec/unit/handler/json_file_spec.rb64
-rw-r--r--spec/unit/handler_spec.rb216
-rw-r--r--spec/unit/json_compat_spect.rb53
-rw-r--r--spec/unit/knife/bootstrap_spec.rb214
-rw-r--r--spec/unit/knife/client_bulk_delete_spec.rb78
-rw-r--r--spec/unit/knife/client_create_spec.rb74
-rw-r--r--spec/unit/knife/client_delete_spec.rb40
-rw-r--r--spec/unit/knife/client_edit_spec.rb40
-rw-r--r--spec/unit/knife/client_list_spec.rb34
-rw-r--r--spec/unit/knife/client_reregister_spec.rb61
-rw-r--r--spec/unit/knife/client_show_spec.rb42
-rw-r--r--spec/unit/knife/config_file_selection_spec.rb118
-rw-r--r--spec/unit/knife/configure_client_spec.rb83
-rw-r--r--spec/unit/knife/configure_spec.rb229
-rw-r--r--spec/unit/knife/cookbook_bulk_delete_spec.rb87
-rw-r--r--spec/unit/knife/cookbook_create_spec.rb271
-rw-r--r--spec/unit/knife/cookbook_delete_spec.rb239
-rw-r--r--spec/unit/knife/cookbook_download_spec.rb217
-rw-r--r--spec/unit/knife/cookbook_list_spec.rb88
-rw-r--r--spec/unit/knife/cookbook_metadata_from_file_spec.rb65
-rw-r--r--spec/unit/knife/cookbook_metadata_spec.rb179
-rw-r--r--spec/unit/knife/cookbook_show_spec.rb223
-rw-r--r--spec/unit/knife/cookbook_site_download_spec.rb151
-rw-r--r--spec/unit/knife/cookbook_site_install_spec.rb138
-rw-r--r--spec/unit/knife/cookbook_site_share_spec.rb146
-rw-r--r--spec/unit/knife/cookbook_site_unshare_spec.rb77
-rw-r--r--spec/unit/knife/cookbook_test_spec.rb84
-rw-r--r--spec/unit/knife/cookbook_upload_spec.rb183
-rw-r--r--spec/unit/knife/core/bootstrap_context_spec.rb128
-rw-r--r--spec/unit/knife/core/cookbook_scm_repo_spec.rb187
-rw-r--r--spec/unit/knife/core/object_loader_spec.rb81
-rw-r--r--spec/unit/knife/core/subcommand_loader_spec.rb54
-rw-r--r--spec/unit/knife/core/ui_spec.rb309
-rw-r--r--spec/unit/knife/data_bag_create_spec.rb105
-rw-r--r--spec/unit/knife/data_bag_edit_spec.rb89
-rw-r--r--spec/unit/knife/data_bag_from_file_spec.rb191
-rw-r--r--spec/unit/knife/data_bag_show_spec.rb112
-rw-r--r--spec/unit/knife/environment_create_spec.rb91
-rw-r--r--spec/unit/knife/environment_delete_spec.rb71
-rw-r--r--spec/unit/knife/environment_edit_spec.rb79
-rw-r--r--spec/unit/knife/environment_from_file_spec.rb89
-rw-r--r--spec/unit/knife/environment_list_spec.rb54
-rw-r--r--spec/unit/knife/environment_show_spec.rb52
-rw-r--r--spec/unit/knife/index_rebuild_spec.rb65
-rw-r--r--spec/unit/knife/knife_help.rb92
-rw-r--r--spec/unit/knife/node_bulk_delete_spec.rb97
-rw-r--r--spec/unit/knife/node_delete_spec.rb68
-rw-r--r--spec/unit/knife/node_edit_spec.rb88
-rw-r--r--spec/unit/knife/node_from_file_spec.rb59
-rw-r--r--spec/unit/knife/node_list_spec.rb63
-rw-r--r--spec/unit/knife/node_run_list_add_spec.rb125
-rw-r--r--spec/unit/knife/node_run_list_remove_spec.rb74
-rw-r--r--spec/unit/knife/node_show_spec.rb48
-rw-r--r--spec/unit/knife/role_bulk_delete_spec.rb80
-rw-r--r--spec/unit/knife/role_create_spec.rb80
-rw-r--r--spec/unit/knife/role_delete_spec.rb67
-rw-r--r--spec/unit/knife/role_edit_spec.rb79
-rw-r--r--spec/unit/knife/role_from_file_spec.rb69
-rw-r--r--spec/unit/knife/role_list_spec.rb56
-rw-r--r--spec/unit/knife/ssh_spec.rb182
-rw-r--r--spec/unit/knife/status_spec.rb43
-rw-r--r--spec/unit/knife/tag_create_spec.rb23
-rw-r--r--spec/unit/knife/tag_delete_spec.rb25
-rw-r--r--spec/unit/knife/tag_list_spec.rb23
-rw-r--r--spec/unit/knife_spec.rb295
-rw-r--r--spec/unit/log_spec.rb24
-rw-r--r--spec/unit/lwrp_spec.rb231
-rw-r--r--spec/unit/mash_spec.rb51
-rw-r--r--spec/unit/mixin/checksum_spec.rb41
-rw-r--r--spec/unit/mixin/command_spec.rb105
-rw-r--r--spec/unit/mixin/convert_to_class_name_spec.rb50
-rw-r--r--spec/unit/mixin/deep_merge_spec.rb314
-rw-r--r--spec/unit/mixin/deprecation_spec.rb34
-rw-r--r--spec/unit/mixin/enforce_ownership_and_permissions_spec.rb93
-rw-r--r--spec/unit/mixin/params_validate_spec.rb372
-rw-r--r--spec/unit/mixin/path_sanity_spec.rb80
-rw-r--r--spec/unit/mixin/securable_spec.rb254
-rw-r--r--spec/unit/mixin/shell_out_spec.rb109
-rw-r--r--spec/unit/mixin/template_spec.rb104
-rw-r--r--spec/unit/mixin/xml_escape_spec.rb54
-rw-r--r--spec/unit/monkey_patches/string_spec.rb37
-rw-r--r--spec/unit/node/attribute_spec.rb1194
-rw-r--r--spec/unit/node/immutable_collections_spec.rb141
-rw-r--r--spec/unit/node_spec.rb684
-rw-r--r--spec/unit/platform_spec.rb240
-rw-r--r--spec/unit/provider/breakpoint_spec.rb54
-rw-r--r--spec/unit/provider/cookbook_file_spec.rb220
-rw-r--r--spec/unit/provider/cron/solaris_spec.rb121
-rw-r--r--spec/unit/provider/cron_spec.rb812
-rw-r--r--spec/unit/provider/deploy/revision_spec.rb109
-rw-r--r--spec/unit/provider/deploy/timestamped_spec.rb40
-rw-r--r--spec/unit/provider/deploy_spec.rb654
-rw-r--r--spec/unit/provider/directory_spec.rb147
-rw-r--r--spec/unit/provider/env_spec.rb232
-rw-r--r--spec/unit/provider/erl_call_spec.rb88
-rw-r--r--spec/unit/provider/execute_spec.rb63
-rw-r--r--spec/unit/provider/file_spec.rb498
-rw-r--r--spec/unit/provider/git_spec.rb352
-rw-r--r--spec/unit/provider/group/dscl_spec.rb294
-rw-r--r--spec/unit/provider/group/gpasswd_spec.rb108
-rw-r--r--spec/unit/provider/group/groupadd_spec.rb161
-rw-r--r--spec/unit/provider/group/groupmod_spec.rb134
-rw-r--r--spec/unit/provider/group/pw_spec.rb140
-rw-r--r--spec/unit/provider/group/usermod_spec.rb95
-rw-r--r--spec/unit/provider/group/windows_spec.rb94
-rw-r--r--spec/unit/provider/group_spec.rb259
-rw-r--r--spec/unit/provider/http_request_spec.rb178
-rw-r--r--spec/unit/provider/ifconfig_spec.rb213
-rw-r--r--spec/unit/provider/link_spec.rb252
-rw-r--r--spec/unit/provider/log_spec.rb81
-rw-r--r--spec/unit/provider/mdadm_spec.rb128
-rw-r--r--spec/unit/provider/mount/mount_spec.rb398
-rw-r--r--spec/unit/provider/mount/windows_spec.rb134
-rw-r--r--spec/unit/provider/mount_spec.rb160
-rw-r--r--spec/unit/provider/ohai_spec.rb85
-rw-r--r--spec/unit/provider/package/apt_spec.rb351
-rw-r--r--spec/unit/provider/package/dpkg_spec.rb216
-rw-r--r--spec/unit/provider/package/easy_install_spec.rb112
-rw-r--r--spec/unit/provider/package/freebsd_spec.rb259
-rw-r--r--spec/unit/provider/package/ips_spec.rb209
-rw-r--r--spec/unit/provider/package/macports_spec.rb203
-rw-r--r--spec/unit/provider/package/pacman_spec.rb206
-rw-r--r--spec/unit/provider/package/portage_spec.rb276
-rw-r--r--spec/unit/provider/package/rpm_spec.rb152
-rw-r--r--spec/unit/provider/package/rubygems_spec.rb614
-rw-r--r--spec/unit/provider/package/smartos_spec.rb83
-rw-r--r--spec/unit/provider/package/solaris_spec.rb181
-rw-r--r--spec/unit/provider/package/yum_spec.rb1795
-rw-r--r--spec/unit/provider/package/zypper_spec.rb159
-rw-r--r--spec/unit/provider/package_spec.rb429
-rw-r--r--spec/unit/provider/remote_directory_spec.rb204
-rw-r--r--spec/unit/provider/remote_file_spec.rb324
-rw-r--r--spec/unit/provider/route_spec.rb230
-rw-r--r--spec/unit/provider/ruby_block_spec.rb46
-rw-r--r--spec/unit/provider/script_spec.rb96
-rw-r--r--spec/unit/provider/service/arch_service_spec.rb330
-rw-r--r--spec/unit/provider/service/debian_service_spec.rb254
-rw-r--r--spec/unit/provider/service/freebsd_service_spec.rb379
-rw-r--r--spec/unit/provider/service/gentoo_service_spec.rb144
-rw-r--r--spec/unit/provider/service/init_service_spec.rb212
-rw-r--r--spec/unit/provider/service/insserv_service_spec.rb76
-rw-r--r--spec/unit/provider/service/invokercd_service_spec.rb212
-rw-r--r--spec/unit/provider/service/macosx_spec.rb229
-rw-r--r--spec/unit/provider/service/redhat_spec.rb156
-rw-r--r--spec/unit/provider/service/simple_service_spec.rb171
-rw-r--r--spec/unit/provider/service/solaris_smf_service_spec.rb140
-rw-r--r--spec/unit/provider/service/systemd_service_spec.rb239
-rw-r--r--spec/unit/provider/service/upstart_service_spec.rb314
-rw-r--r--spec/unit/provider/service/windows_spec.rb235
-rw-r--r--spec/unit/provider/service_spec.rb169
-rw-r--r--spec/unit/provider/subversion_spec.rb281
-rw-r--r--spec/unit/provider/template_spec.rb198
-rw-r--r--spec/unit/provider/user/dscl_spec.rb480
-rw-r--r--spec/unit/provider/user/pw_spec.rb235
-rw-r--r--spec/unit/provider/user/useradd_spec.rb386
-rw-r--r--spec/unit/provider/user/windows_spec.rb178
-rw-r--r--spec/unit/provider/user_spec.rb477
-rw-r--r--spec/unit/provider_spec.rb168
-rw-r--r--spec/unit/recipe_spec.rb270
-rw-r--r--spec/unit/resource/apt_package_spec.rb43
-rw-r--r--spec/unit/resource/bash_spec.rb40
-rw-r--r--spec/unit/resource/breakpoint_spec.rb43
-rw-r--r--spec/unit/resource/chef_gem_spec.rb49
-rw-r--r--spec/unit/resource/conditional_spec.rb147
-rw-r--r--spec/unit/resource/cookbook_file_spec.rb89
-rw-r--r--spec/unit/resource/cron_spec.rb181
-rw-r--r--spec/unit/resource/csh_spec.rb40
-rw-r--r--spec/unit/resource/deploy_revision_spec.rb47
-rw-r--r--spec/unit/resource/deploy_spec.rb259
-rw-r--r--spec/unit/resource/directory_spec.rb82
-rw-r--r--spec/unit/resource/dpkg_package_spec.rb38
-rw-r--r--spec/unit/resource/easy_install_package_spec.rb48
-rw-r--r--spec/unit/resource/env_spec.rb85
-rw-r--r--spec/unit/resource/erl_call_spec.rb81
-rw-r--r--spec/unit/resource/execute_spec.rb124
-rw-r--r--spec/unit/resource/file_spec.rb121
-rw-r--r--spec/unit/resource/freebsd_package_spec.rb39
-rw-r--r--spec/unit/resource/gem_package_spec.rb49
-rw-r--r--spec/unit/resource/git_spec.rb46
-rw-r--r--spec/unit/resource/group_spec.rb148
-rw-r--r--spec/unit/resource/http_request_spec.rb59
-rw-r--r--spec/unit/resource/ifconfig_spec.rb46
-rw-r--r--spec/unit/resource/ips_package_spec.rb43
-rw-r--r--spec/unit/resource/link_spec.rb118
-rw-r--r--spec/unit/resource/log_spec.rb61
-rw-r--r--spec/unit/resource/macports_package_spec.rb37
-rw-r--r--spec/unit/resource/mdadm_spec.rb102
-rw-r--r--spec/unit/resource/mount_spec.rb158
-rw-r--r--spec/unit/resource/ohai_spec.rb62
-rw-r--r--spec/unit/resource/package_spec.rb80
-rw-r--r--spec/unit/resource/pacman_package_spec.rb38
-rw-r--r--spec/unit/resource/perl_spec.rb40
-rw-r--r--spec/unit/resource/portage_package_spec.rb38
-rw-r--r--spec/unit/resource/python_spec.rb40
-rw-r--r--spec/unit/resource/remote_directory_spec.rb97
-rw-r--r--spec/unit/resource/remote_file_spec.rb123
-rw-r--r--spec/unit/resource/route_spec.rb107
-rw-r--r--spec/unit/resource/rpm_package_spec.rb38
-rw-r--r--spec/unit/resource/ruby_block_spec.rb61
-rw-r--r--spec/unit/resource/ruby_spec.rb40
-rw-r--r--spec/unit/resource/scm_spec.rb159
-rw-r--r--spec/unit/resource/script_spec.rb69
-rw-r--r--spec/unit/resource/service_spec.rb165
-rw-r--r--spec/unit/resource/smartos_package_spec.rb38
-rw-r--r--spec/unit/resource/subversion_spec.rb58
-rw-r--r--spec/unit/resource/template_spec.rb108
-rw-r--r--spec/unit/resource/timestamped_deploy_spec.rb28
-rw-r--r--spec/unit/resource/user_spec.rb122
-rw-r--r--spec/unit/resource/yum_package_spec.rb85
-rw-r--r--spec/unit/resource_collection/stepable_iterator_spec.rb144
-rw-r--r--spec/unit/resource_collection_spec.rb257
-rw-r--r--spec/unit/resource_definition_spec.rb119
-rw-r--r--spec/unit/resource_platform_map_spec.rb164
-rw-r--r--spec/unit/resource_reporter_spec.rb580
-rw-r--r--spec/unit/resource_spec.rb706
-rw-r--r--spec/unit/rest/auth_credentials_spec.rb419
-rw-r--r--spec/unit/rest_spec.rb661
-rw-r--r--spec/unit/role_spec.rb275
-rw-r--r--spec/unit/run_context_spec.rb78
-rw-r--r--spec/unit/run_list/run_list_expansion_spec.rb129
-rw-r--r--spec/unit/run_list/run_list_item_spec.rb117
-rw-r--r--spec/unit/run_list/versioned_recipe_list_spec.rb123
-rw-r--r--spec/unit/run_list_spec.rb312
-rw-r--r--spec/unit/run_lock_spec.rb37
-rw-r--r--spec/unit/run_status_spec.rb145
-rw-r--r--spec/unit/runner_spec.rb402
-rw-r--r--spec/unit/scan_access_control_spec.rb182
-rw-r--r--spec/unit/search/query_spec.rb99
-rw-r--r--spec/unit/shell/model_wrapper_spec.rb97
-rw-r--r--spec/unit/shell/shell_ext_spec.rb153
-rw-r--r--spec/unit/shell/shell_session_spec.rb141
-rw-r--r--spec/unit/shell_out_spec.rb18
-rw-r--r--spec/unit/shell_spec.rb161
-rw-r--r--spec/unit/util/file_edit_spec.rb135
-rw-r--r--spec/unit/version_class_spec.rb172
-rw-r--r--spec/unit/version_constraint_spec.rb134
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 '&amp;'" do
+ @escaper.xml_escape("&").should == "&amp;"
+ end
+
+ it "escapes angle brackets to &lt; or &gt;" do
+ @escaper.xml_escape("<").should == "&lt;"
+ @escaper.xml_escape(">").should == "&gt;"
+ 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 == '&#169;'
+ end
+
+ it "converts win 1252 characters correctly" do
+ @escaper.xml_escape("\x80").should == '&#8364;'
+ 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