# # Author:: Adam Jacob () # Author:: Christopher Walters () # Author:: Tim Hinderliter () # Author:: Seth Chisamore () # Copyright:: Copyright 2008-2018, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 do let(:cookbook_repo_path) { File.join(CHEF_SPEC_DATA, "cookbooks") } let(:cookbook_collection) { Chef::CookbookCollection.new(Chef::CookbookLoader.new(cookbook_repo_path)) } let(:node) { Chef::Node.new } let(:events) { Chef::EventDispatch::Dispatcher.new } let(:run_context) { Chef::RunContext.new(node, cookbook_collection, events) } let(:resource) { resource_class.new("funk", run_context) } let(:resource_class) { Chef::Resource } it "should mixin shell_out" do expect(resource.respond_to?(:shell_out)).to be true end it "should mixin shell_out!" do expect(resource.respond_to?(:shell_out!)).to be true end describe "when inherited" do it "adds an entry to a list of subclasses" do subclass = Class.new(Chef::Resource) expect(Chef::Resource.resource_classes).to include(subclass) end it "keeps track of subclasses of subclasses" do subclass = Class.new(Chef::Resource) subclass_of_subclass = Class.new(subclass) expect(Chef::Resource.resource_classes).to include(subclass_of_subclass) end end describe "when declaring the identity attribute" do it "has :name as identity attribute by default" do expect(Chef::Resource.identity_attr).to eq(:name) end it "sets an identity attribute" do resource_class = Class.new(Chef::Resource) resource_class.identity_attr(:path) expect(resource_class.identity_attr).to eq(: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) expect(resource_subclass.identity_attr).to eq(: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) expect(resource_subclass.identity_attr).to eq(:something_else) end end describe "when no identity attribute has been declared" do let(:resource_sans_id) { Chef::Resource.new("my-name") } # 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 expect(resource_sans_id.identity).to eq("my-name") end end describe "when an identity attribute has been declared" do let(:file_resource) 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" file_resource end it "gives the value of its identity attribute" do expect(file_resource.identity).to eq("/tmp/foo.txt") end end describe "when declaring state attributes" do it "has no state_attrs by default" do expect(Chef::Resource.state_attrs).to 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) expect(resource_class.state_attrs).to match_array(%i{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) expect(resource_subclass.state_attrs).to match_array(%i{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) expect(resource_subclass.state_attrs).to match_array(%i{checksum owner group mode}) end end describe "when a set of state attributes has been declared" do let(:file_resource) 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" file_resource end it "describes its state" do resource_state = file_resource.state_for_resource_reporter expect(resource_state.keys).to match_array(%i{checksum owner group mode}) expect(resource_state[:checksum]).to eq("abc123") expect(resource_state[:owner]).to eq("root") expect(resource_state[:group]).to eq("wheel") expect(resource_state[:mode]).to eq("0644") end end describe "#state_for_resource_reporter" do context "when a property is marked as sensitive" do it "suppresses the sensitive property's value" do resource_class = Class.new(Chef::Resource) { property :foo, String, sensitive: true } resource = resource_class.new("sensitive_property_tests") resource.foo = "some value" expect(resource.state_for_resource_reporter[:foo]).to eq("*sensitive value suppressed*") end end context "when a property is not marked as sensitive" do it "does not suppress the property's value" do resource_class = Class.new(Chef::Resource) { property :foo, String } resource = resource_class.new("sensitive_property_tests") resource.foo = "some value" expect(resource.state_for_resource_reporter[:foo]).to eq("some value") end end end describe "load_from" do let(:prior_resource) do prior_resource = Chef::Resource.new("funk") prior_resource.source_line prior_resource.allowed_actions << :funkytown prior_resource.action(:funkytown) prior_resource end before(:each) do resource.allowed_actions << :funkytown run_context.resource_collection << prior_resource end it "should load the attributes of a prior resource" do resource.load_from(prior_resource) end it "should not inherit the action from the prior resource" do resource.load_from(prior_resource) expect(resource.action).not_to eq(prior_resource.action) end end describe "name" do it "should have a name" do expect(resource.name).to eql("funk") end it "should let you set a new name" do resource.name "monkey" expect(resource.name).to eql("monkey") end it "coerces arrays to names" do expect(resource.name %w{a b}).to eql("a, b") end it "should coerce objects to a string" do expect(resource.name Object.new).to be_a(String) 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") expect(resource.delayed_notifications.detect { |e| e.resource.name == "coffee" && e.action == :reload }).not_to 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 expect(resource.immediate_notifications.detect { |e| e.resource.name == "coffee" && e.action == :reload }).not_to 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") expect do resource.notifies :reload, run_context.resource_collection.find(zen_master: "coffee"), :someday end.to 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") expect(resource.delayed_notifications.detect { |e| e.resource.name == "coffee" && e.action == :reload }).not_to be_nil run_context.resource_collection << Chef::Resource::ZenMaster.new("beans") resource.notifies :reload, run_context.resource_collection.find(zen_master: "beans") expect(resource.delayed_notifications.detect { |e| e.resource.name == "beans" && e.action == :reload }).not_to 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) expect(resource.delayed_notifications).to 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) expect(resource.immediate_notifications).to 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) expect(resource.delayed_notifications).to include(expected_notification) end it "notifies a resource with an array for its name via its prettified string name" do run_context.resource_collection << Chef::Resource::ZenMaster.new(%w{coffee tea}) resource.notifies :reload, run_context.resource_collection.find(zen_master: "coffee, tea") expect(resource.delayed_notifications.detect { |e| e.resource.name == "coffee, tea" && e.action == :reload }).not_to be_nil end it "notifies a resource without a name via a string name with brackets" do run_context.resource_collection << Chef::Resource::ZenMaster.new("") resource.notifies :reload, "zen_master[]" end it "notifies a resource without a name via a string name without brackets" do run_context.resource_collection << Chef::Resource::ZenMaster.new("") resource.notifies :reload, "zen_master" expect(resource.delayed_notifications.first.resource).to eql("zen_master") end it "notifies a resource without a name via a hash name with an empty string" do run_context.resource_collection << Chef::Resource::ZenMaster.new("") resource.notifies :reload, zen_master: "" expect(resource.delayed_notifications.first.resource).to eql(zen_master: "") 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 expect(zr.delayed_notifications.detect { |e| e.resource.name == "funk" && e.action == :reload }).not_to 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 expect(zr.delayed_notifications.detect { |e| e.resource.name == resource.name && e.action == :reload }).not_to be_nil run_context.resource_collection << Chef::Resource::ZenMaster.new("bean") zrb = run_context.resource_collection.find(zen_master: "bean") zrb.subscribes :reload, zr expect(zr.delayed_notifications.detect { |e| e.resource.name == resource.name && e.action == :reload }).not_to 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 expect(zr.immediate_notifications.detect { |e| e.resource.name == resource.name && e.action == :reload }).not_to be_nil end end describe "defined_at" do it "should correctly parse source_line on unix-like operating systems" do resource.source_line = "/some/path/to/file.rb:80:in `wombat_tears'" expect(resource.defined_at).to eq("/some/path/to/file.rb line 80") end it "should correctly parse source_line on Windows" do resource.source_line = "C:/some/path/to/file.rb:80 in 1`wombat_tears'" expect(resource.defined_at).to eq("C:/some/path/to/file.rb line 80") end it "should include the cookbook and recipe when it knows it" do resource.source_line = "/some/path/to/file.rb:80:in `wombat_tears'" resource.recipe_name = "wombats" resource.cookbook_name = "animals" expect(resource.defined_at).to eq("animals::wombats line 80") end it "should recognize dynamically defined resources" do expect(resource.defined_at).to eq("dynamically defined") end end describe "to_s" do it "should become a string like resource_name[name]" do zm = Chef::Resource::ZenMaster.new("coffee") expect(zm.to_s).to eql("zen_master[coffee]") end end describe "to_text" do it "prints nice message" do resource_class = Class.new(Chef::Resource) { property :foo, String } resource = resource_class.new("sensitive_property_tests") resource.foo = "some value" expect(resource.to_text).to match(/foo "some value"/) end context "when property is sensitive" do it "supresses that properties value" do resource_class = Class.new(Chef::Resource) { property :foo, String, sensitive: true } resource = resource_class.new("sensitive_property_tests") resource.foo = "some value" expect(resource.to_text).to match(/foo "\*sensitive value suppressed\*"/) end end context "when property is required" do it "does not propagate vailidation errors" do resource_class = Class.new(Chef::Resource) { property :foo, String, required: true } resource = resource_class.new("required_property_tests") expect { resource.to_text }.to_not raise_error Chef::Exceptions::ValidationFailed end end end context "Documentation of resources" do it "can have a description" do c = Class.new(Chef::Resource) do description "my description" end expect(c.description).to eq "my description" end it "can say when it was introduced" do c = Class.new(Chef::Resource) do introduced "14.0" end expect(c.introduced).to eq "14.0" end it "can have some examples" do c = Class.new(Chef::Resource) do examples <<~EOH resource "foo" do foo foo end EOH end expect(c.examples).to eq <<~EOH resource "foo" do foo foo end EOH end end describe "self.resource_name" do context "When resource_name is not set" do it "and there are no provides lines, resource_name is nil" do c = Class.new(Chef::Resource) do end r = c.new("hi") r.declared_type = :d expect(c.resource_name).to be_nil expect(r.resource_name).to be_nil expect(r.declared_type).to eq :d end it "and there are no provides lines, resource_name is used" do c = Class.new(Chef::Resource) do def initialize(*args, &block) @resource_name = :blah super end end r = c.new("hi") r.declared_type = :d expect(c.resource_name).to be_nil expect(r.resource_name).to eq :blah expect(r.declared_type).to eq :d end it "and the resource class gets a late-bound name, resource_name is nil" do c = Class.new(Chef::Resource) do def self.name "ResourceSpecNameTest" end end r = c.new("hi") r.declared_type = :d expect(c.resource_name).to be_nil expect(r.resource_name).to be_nil expect(r.declared_type).to eq :d end end it "resource_name without provides is honored" do c = Class.new(Chef::Resource) do resource_name "blah" end r = c.new("hi") r.declared_type = :d expect(c.resource_name).to eq :blah expect(r.resource_name).to eq :blah expect(r.declared_type).to eq :d end it "setting class.resource_name with 'resource_name = blah' overrides declared_type" do c = Class.new(Chef::Resource) do provides :self_resource_name_test_2 end c.resource_name = :blah r = c.new("hi") r.declared_type = :d expect(c.resource_name).to eq :blah expect(r.resource_name).to eq :blah expect(r.declared_type).to eq :d end it "setting class.resource_name with 'resource_name blah' overrides declared_type" do c = Class.new(Chef::Resource) do resource_name :blah provides :self_resource_name_test_3 end r = c.new("hi") r.declared_type = :d expect(c.resource_name).to eq :blah expect(r.resource_name).to eq :blah expect(r.declared_type).to eq :d end end describe "to_json" do it "should serialize to json" do json = resource.to_json expect(json).to match(/json_class/) expect(json).to match(/instance_vars/) end include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { resource } end end describe "to_hash" do context "when the resource has a property with a default" do let(:resource_class) { Class.new(Chef::Resource) { property :a, default: 1 } } it "should include the default in the hash" do expect(resource.to_hash.keys.sort).to eq(%i{a allowed_actions params provider updated updated_by_last_action before name source_line action elapsed_time default_guard_interpreter guard_interpreter}.sort) expect(resource.to_hash[:name]).to eq "funk" expect(resource.to_hash[:a]).to eq 1 end end it "should convert to a hash" do hash = resource.to_hash expected_keys = %i{allowed_actions params provider updated updated_by_last_action before name source_line action elapsed_time default_guard_interpreter guard_interpreter} expect(hash.keys - expected_keys).to eq([]) expect(expected_keys - hash.keys).to eq([]) expect(hash[:name]).to eql("funk") end end describe "self.json_create" do it "should deserialize itself from json" do json = Chef::JSONCompat.to_json(resource) serialized_node = Chef::Resource.from_json(json) expect(serialized_node).to be_a_kind_of(Chef::Resource) expect(serialized_node.name).to eql(resource.name) end end describe "ignore_failure" do it "should default to throwing an error if a provider fails for a resource" do expect(resource.ignore_failure).to eq(false) end it "should allow you to set whether a provider should throw exceptions with ignore_failure" do resource.ignore_failure(true) expect(resource.ignore_failure).to eq(true) end it "should allow you to set quiet ignore_failure as a symbol" do resource.ignore_failure(:quiet) expect(resource.ignore_failure).to eq(:quiet) end it "should allow you to set quiet ignore_failure as a string" do resource.ignore_failure("quiet") expect(resource.ignore_failure).to eq("quiet") end end describe "retries" do let(:retriable_resource) do retriable_resource = Chef::Resource::Cat.new("precious", run_context) retriable_resource.provider = Chef::Provider::SnakeOil retriable_resource.action = :purr retriable_resource end before do node.automatic_attrs[:platform] = "fubuntu" node.automatic_attrs[:platform_version] = "10.04" end it "should default to not retrying if a provider fails for a resource" do expect(retriable_resource.retries).to eq(0) end it "should allow you to set how many retries a provider should attempt after a failure" do retriable_resource.retries(2) expect(retriable_resource.retries).to eq(2) end it "should default to a retry delay of 2 seconds" do expect(retriable_resource.retry_delay).to eq(2) end it "should allow you to set the retry delay" do retriable_resource.retry_delay(10) expect(retriable_resource.retry_delay).to eq(10) end it "should keep given value of retries intact after the provider fails for a resource" do retriable_resource.retries(3) retriable_resource.retry_delay(0) # No need to wait. provider = Chef::Provider::SnakeOil.new(retriable_resource, run_context) allow(Chef::Provider::SnakeOil).to receive(:new).and_return(provider) allow(provider).to receive(:action_purr).and_raise expect(retriable_resource).to receive(:sleep).exactly(3).times expect { retriable_resource.run_action(:purr) }.to raise_error(RuntimeError) expect(retriable_resource.retries).to eq(3) end it "should not rescue from non-StandardError exceptions" do retriable_resource.retries(3) retriable_resource.retry_delay(0) # No need to wait. provider = Chef::Provider::SnakeOil.new(retriable_resource, run_context) allow(Chef::Provider::SnakeOil).to receive(:new).and_return(provider) allow(provider).to receive(:action_purr).and_raise(LoadError) expect(retriable_resource).not_to receive(:sleep) expect { retriable_resource.run_action(:purr) }.to raise_error(LoadError) end end it "runs an action by finding its provider, loading the current resource and then running the action" do skip end describe "when updated by a provider" do before do resource.updated_by_last_action(true) end it "records that it was updated" do expect(resource).to be_updated end it "records that the last action updated the resource" do expect(resource).to 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 expect(resource).to be_updated end it "reports that it was not updated by the last action" do expect(resource).not_to be_updated_by_last_action end end end describe "when invoking its action" do let(:resource) do resource = Chef::Resource.new("provided", run_context) resource.provider = Chef::Provider::SnakeOil resource end before do 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 expect_any_instance_of(Chef::Resource::Conditional).not_to receive(:evaluate) resource.only_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 } expect(resource.only_if.first.positivity).to eq(:only_if) # Chef::Mixin::Command.should_receive(:only_if).with(true, {}).and_return(false) resource.run_action(:purr) expect(snitch_variable).to be_truthy 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) expect(snitch_var1).to eq(1) expect(snitch_var2).to eq(2) end it "accepts command options for only_if conditionals" do expect_any_instance_of(Chef::Resource::Conditional).to receive(:evaluate_command).at_least(1).times resource.only_if("true", cwd: "/tmp") expect(resource.only_if.first.command_opts).to eq({ cwd: "/tmp" }) resource.run_action(:purr) end it "runs not_if as a command when it is a string" do expect_any_instance_of(Chef::Resource::Conditional).to 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 expect_any_instance_of(Chef::Resource::Conditional).to 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 expect_any_instance_of(Chef::Resource::Conditional).not_to receive(:evaluate) resource.not_if.clear resource.run_action(:purr) end it "accepts command options for not_if conditionals" do resource.not_if("pwd" , cwd: "/tmp") expect(resource.not_if.first.command_opts).to eq({ 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) expect(snitch_var1).to be_nil expect(snitch_var2).to be_falsey end it "reports 0 elapsed time if actual elapsed time is < 0" do expected = Time.now allow(Time).to receive(:now).and_return(expected, expected - 1) resource.run_action(:purr) expect(resource.elapsed_time).to eq(0) end describe "guard_interpreter attribute" do it "should be set to :default by default" do expect(resource.guard_interpreter).to eq(:default) end it "if set to :default should return :default when read" do resource.guard_interpreter(:default) expect(resource.guard_interpreter).to eq(:default) end it "should raise Chef::Exceptions::ValidationFailed on an attempt to set the guard_interpreter attribute to something other than a Symbol" do expect { resource.guard_interpreter("command_dot_com") }.to raise_error(Chef::Exceptions::ValidationFailed) end it "should not raise an exception when setting the guard interpreter attribute to a Symbol" do allow(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:new).and_return(nil) expect { resource.guard_interpreter(:command_dot_com) }.not_to raise_error end end end describe "should_skip?" do before do resource = Chef::Resource::Cat.new("sugar", run_context) end it "should return false by default" do expect(resource.should_skip?(:purr)).to be_falsey end it "should return false when only_if is met" do resource.only_if { true } expect(resource.should_skip?(:purr)).to be_falsey end it "should return true when only_if is not met" do resource.only_if { false } expect(resource.should_skip?(:purr)).to be_truthy end it "should return true when not_if is met" do resource.not_if { true } expect(resource.should_skip?(:purr)).to be_truthy end it "should return false when not_if is not met" do resource.not_if { false } expect(resource.should_skip?(:purr)).to be_falsey end it "should return true when only_if is met but also not_if is met" do resource.only_if { true } resource.not_if { true } expect(resource.should_skip?(:purr)).to be_truthy end it "should return false when only_if is met and also not_if is not met" do resource.only_if { true } resource.not_if { false } expect(resource.should_skip?(:purr)).to be_falsey end it "should return true when one of multiple only_if's is not met" do resource.only_if { true } resource.only_if { false } resource.only_if { true } expect(resource.should_skip?(:purr)).to be_truthy end it "should return true when one of multiple not_if's is met" do resource.not_if { false } resource.not_if { true } resource.not_if { false } expect(resource.should_skip?(:purr)).to be_truthy end it "should return false when all of multiple only_if's are met" do resource.only_if { true } resource.only_if { true } resource.only_if { true } expect(resource.should_skip?(:purr)).to be_falsey end it "should return false when all of multiple not_if's are not met" do resource.not_if { false } resource.not_if { false } resource.not_if { false } expect(resource.should_skip?(:purr)).to be_falsey end it "should return true when action is :nothing" do expect(resource.should_skip?(:nothing)).to be_truthy end it "should return true when action is :nothing ignoring only_if/not_if conditionals" do resource.only_if { true } resource.not_if { false } expect(resource.should_skip?(:nothing)).to be_truthy end it "should print \"skipped due to action :nothing\" message for doc formatter when action is :nothing" do fdoc = Chef::Formatters.new(:doc, STDOUT, STDERR) allow(run_context).to receive(:events).and_return(fdoc) expect(fdoc).to receive(:puts).with(" (skipped due to action :nothing)", anything) resource.should_skip?(:nothing) end end describe "when resource action is :nothing" do let(:resource1) do resource1 = Chef::Resource::Cat.new("sugar", run_context) resource1.action = :nothing resource1 end before do node.automatic_attrs[:platform] = "fubuntu" node.automatic_attrs[:platform_version] = "10.04" end it "should not run only_if/not_if conditionals (CHEF-972)" do snitch_var1 = 0 resource1.only_if { snitch_var1 = 1 } resource1.not_if { snitch_var1 = 2 } resource1.run_action(:nothing) expect(snitch_var1).to eq(0) end it "should run only_if/not_if conditionals when notified to run another action (CHEF-972)" do snitch_var1 = snitch_var2 = 0 runner = Chef::Runner.new(run_context) Chef::Provider::SnakeOil.provides :cat resource1.only_if { snitch_var1 = 1 } resource1.not_if { snitch_var2 = 2 } resource2 = Chef::Resource::Cat.new("coffee", run_context) resource2.notifies :purr, resource1 resource2.action = :purr run_context.resource_collection << resource1 run_context.resource_collection << resource2 runner.converge expect(snitch_var1).to eq(1) expect(snitch_var2).to eq(2) end end describe "building the platform map" do let(:klz) { Class.new(Chef::Resource) } before do Chef::Resource::Klz = klz end after do Chef::Resource.send(:remove_const, :Klz) end it "adds mappings for a single platform" do expect(Chef.resource_handler_map).to receive(:set).with( :dinobot, Chef::Resource::Klz, { platform: ["autobots"] } ) klz.provides :dinobot, platform: ["autobots"] end it "adds mappings for multiple platforms" do expect(Chef.resource_handler_map).to receive(:set).with( :energy, Chef::Resource::Klz, { platform: %w{autobots decepticons} } ) klz.provides :energy, platform: %w{autobots decepticons} end it "adds mappings for all platforms" do expect(Chef.resource_handler_map).to receive(:set).with( :tape_deck, Chef::Resource::Klz, {} ) klz.provides :tape_deck end end describe "resource_for_node" do describe "lookups from the platform map" do let(:klz1) { Class.new(Chef::Resource) } before(:each) do Chef::Resource::Klz1 = klz1 node = Chef::Node.new node.name("bumblebee") node.automatic[:platform] = "autobots" node.automatic[:platform_version] = "6.1" Object.const_set("Soundwave", klz1) klz1.provides :soundwave end after(:each) do Object.send(:remove_const, :Soundwave) Chef::Resource.send(:remove_const, :Klz1) end it "returns a resource by short_name if nothing else matches" do expect(Chef::Resource.resource_for_node(:soundwave, node)).to eql(klz1) end end describe "lookups from the platform map" do let(:klz2) { Class.new(Chef::Resource) } before(:each) do Chef::Resource::Klz2 = klz2 node.name("bumblebee") node.automatic[:platform] = "autobots" node.automatic[:platform_version] = "6.1" klz2.provides :dinobot, platform: ["autobots"] Object.const_set("Grimlock", klz2) klz2.provides :grimlock end after(:each) do Object.send(:remove_const, :Grimlock) Chef::Resource.send(:remove_const, :Klz2) end it "returns a resource by short_name and node" do expect(Chef::Resource.resource_for_node(:dinobot, node)).to eql(klz2) end end describe "chef_version constraints and the platform map" do let(:klz3) { Class.new(Chef::Resource) } it "doesn't wire up the provides when chef_version is < 1" do klz3.provides :bulbasaur, chef_version: "< 1.0" # this should be false expect { Chef::Resource.resource_for_node(:bulbasaur, node) }.to raise_error(Chef::Exceptions::NoSuchResourceType) end it "wires up the provides when chef_version is > 1" do klz3.provides :bulbasaur, chef_version: "> 1.0" # this should be true expect(Chef::Resource.resource_for_node(:bulbasaur, node)).to eql(klz3) end it "wires up the default when chef_version is < 1" do klz3.chef_version_for_provides("< 1.0") # this should be false klz3.provides :bulbasaur expect { Chef::Resource.resource_for_node(:bulbasaur, node) }.to raise_error(Chef::Exceptions::NoSuchResourceType) end it "wires up the default when chef_version is > 1" do klz3.chef_version_for_provides("> 1.0") # this should be true klz3.provides :bulbasaur expect(Chef::Resource.resource_for_node(:bulbasaur, node)).to eql(klz3) end end end describe "when creating notifications" do describe "with a string resource spec" do it "creates a delayed notification when timing is not specified" do resource.notifies(:run, "execute[foo]") expect(run_context.delayed_notification_collection.size).to eq(1) end it "creates a delayed notification when :delayed is not specified" do resource.notifies(:run, "execute[foo]", :delayed) expect(run_context.delayed_notification_collection.size).to eq(1) end it "creates an immediate notification when :immediate is specified" do resource.notifies(:run, "execute[foo]", :immediate) expect(run_context.immediate_notification_collection.size).to eq(1) end it "creates an immediate notification when :immediately is specified" do resource.notifies(:run, "execute[foo]", :immediately) expect(run_context.immediate_notification_collection.size).to eq(1) end describe "with a syntax error in the resource spec" do it "raises an exception immmediately" do expect do resource.notifies(:run, "typo[missing-closing-bracket") end.to raise_error(Chef::Exceptions::InvalidResourceSpecification) end end end describe "with a resource reference" do let(:notified_resource) { Chef::Resource.new("punk", run_context) } it "creates a delayed notification when timing is not specified" do resource.notifies(:run, notified_resource) expect(run_context.delayed_notification_collection.size).to eq(1) end it "creates a delayed notification when :delayed is not specified" do resource.notifies(:run, notified_resource, :delayed) expect(run_context.delayed_notification_collection.size).to eq(1) end it "creates an immediate notification when :immediate is specified" do resource.notifies(:run, notified_resource, :immediate) expect(run_context.immediate_notification_collection.size).to eq(1) end it "creates an immediate notification when :immediately is specified" do resource.notifies(:run, notified_resource, :immediately) expect(run_context.immediate_notification_collection.size).to eq(1) end end end describe "resource sensitive attribute" do let(:resource_file) { Chef::Resource::File.new("/nonexistent/CHEF-5098/file", run_context) } let(:action) { :create } def compiled_resource_data(resource, action, err) error_inspector = Chef::Formatters::ErrorInspectors::ResourceFailureInspector.new(resource, action, err) description = Chef::Formatters::ErrorDescription.new("test") error_inspector.add_explanation(description) Chef::Log.info("descrtiption: #{description.inspect},error_inspector: #{error_inspector}") description.sections[1]["Compiled Resource:"] end it "set to false by default" do expect(resource.sensitive).to be_falsey end it "when set to false should show compiled resource for failed resource" do expect { resource_file.run_action(action) }.to raise_error { |err| expect(compiled_resource_data(resource_file, action, err)).to match 'path "/nonexistent/CHEF-5098/file"' } end it "when set to true should show compiled resource for failed resource" do resource_file.sensitive true expect { resource_file.run_action(action) }.to raise_error { |err| expect(compiled_resource_data(resource_file, action, err)).to eql("suppressed sensitive resource output") } end end describe "#action" do let(:resource_class) do Class.new(described_class) do allowed_actions(%i{one two}) end end let(:resource) { resource_class.new("test", nil) } subject { resource.action } context "with a no action" do it { is_expected.to eq [:nothing] } end context "with a default action" do let(:resource_class) do Class.new(described_class) do default_action(:one) end end it { is_expected.to eq [:one] } end context "with a symbol action" do before { resource.action(:one) } it { is_expected.to eq [:one] } end context "with a string action" do before { resource.action("two") } it { is_expected.to eq [:two] } end context "with an array action" do before { resource.action(%i{two one}) } it { is_expected.to eq %i{two one} } end context "with an assignment" do before { resource.action = :one } it { is_expected.to eq [:one] } end context "with an array assignment" do before { resource.action = %i{two one} } it { is_expected.to eq %i{two one} } end context "with an invalid action" do it { expect { resource.action(:three) }.to raise_error Chef::Exceptions::ValidationFailed } end context "with an invalid assignment action" do it { expect { resource.action = :three }.to raise_error Chef::Exceptions::ValidationFailed } end end describe ".default_action" do let(:default_action) {} let(:resource_class) do actions = default_action Class.new(described_class) do default_action(actions) if actions end end subject { resource_class.default_action } context "with no default actions" do it { is_expected.to eq [:nothing] } end context "with a symbol default action" do let(:default_action) { :one } it { is_expected.to eq [:one] } end context "with a string default action" do let(:default_action) { "one" } it { is_expected.to eq [:one] } end context "with an array default action" do let(:default_action) { %i{two one} } it { is_expected.to eq %i{two one} } end end describe ".preview_resource" do let(:klass) { Class.new(Chef::Resource) } before do allow(Chef::DSL::Resources).to receive(:add_resource_dsl).with(:test_resource) end it "defaults to false" do expect(klass.preview_resource).to eq false end it "can be set to true" do klass.preview_resource(true) expect(klass.preview_resource).to eq true end it "does not affect provides by default" do expect(Chef.resource_handler_map).to receive(:set).with(:test_resource, klass, { canonical: true }) klass.resource_name(:test_resource) end end end