# # Author:: Adam Jacob () # Author:: AJ Christensen () # 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 expect(@attributes).to be_a_kind_of(Chef::Node::Attribute) end it "should take an Automatioc, Normal, Default and Override hash" do expect { Chef::Node::Attribute.new({}, {}, {}, {}) }.not_to 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 }) expect(na.send(accessor)).to eq({ accessor.to_s => true }) end end it "should be enumerable" do expect(@attributes).to be_is_a(Enumerable) end end describe "when printing attribute components" do it "does not cause a type error" do # See CHEF-3799; IO#puts implicitly calls #to_ary on its argument. This # is expected to raise a NoMethodError or return an Array. `to_ary` is # the "strict" conversion method that should only be implemented by # things that are truly Array-like, so NoMethodError is the right choice. # (cf. there is no Hash#to_ary). expect { @attributes.default.to_ary }.to raise_error(NoMethodError) end end describe "when debugging attributes" do before do @attributes.default[:foo][:bar] = "default" @attributes.env_default[:foo][:bar] = "env_default" @attributes.role_default[:foo][:bar] = "role_default" @attributes.force_default[:foo][:bar] = "force_default" @attributes.normal[:foo][:bar] = "normal" @attributes.override[:foo][:bar] = "override" @attributes.role_override[:foo][:bar] = "role_override" @attributes.env_override[:foo][:bar] = "env_override" @attributes.force_override[:foo][:bar] = "force_override" @attributes.automatic[:foo][:bar] = "automatic" end it "gives the value at each level of precedence for a path spec" do expected = [["set_unless_enabled?", false], ["default", "default"], ["env_default", "env_default"], ["role_default", "role_default"], ["force_default", "force_default"], ["normal", "normal"], ["override", "override"], ["role_override", "role_override"], ["env_override", "env_override"], ["force_override", "force_override"], ["automatic", "automatic"] ] expect(@attributes.debug_value(:foo, :bar)).to eq(expected) end end describe "when fetching values based on precedence" do before do @attributes.default["default"] = "cookbook default" @attributes.override["override"] = "cookbook override" end it "prefers 'forced default' over any other default" do @attributes.force_default["default"] = "force default" @attributes.role_default["default"] = "role default" @attributes.env_default["default"] = "environment default" expect(@attributes["default"]).to eq("force default") end it "prefers role_default over environment or cookbook default" do @attributes.role_default["default"] = "role default" @attributes.env_default["default"] = "environment default" expect(@attributes["default"]).to eq("role default") end it "prefers environment default over cookbook default" do @attributes.env_default["default"] = "environment default" expect(@attributes["default"]).to eq("environment default") end it "returns the cookbook default when no other default values are present" do expect(@attributes["default"]).to eq("cookbook default") end it "prefers 'forced overrides' over role or cookbook overrides" do @attributes.force_override["override"] = "force override" @attributes.env_override["override"] = "environment override" @attributes.role_override["override"] = "role override" expect(@attributes["override"]).to eq("force override") end it "prefers environment overrides over role or cookbook overrides" do @attributes.env_override["override"] = "environment override" @attributes.role_override["override"] = "role override" expect(@attributes["override"]).to eq("environment override") end it "prefers role overrides over cookbook overrides" do @attributes.role_override["override"] = "role override" expect(@attributes["override"]).to eq("role override") end it "returns cookbook overrides when no other overrides are present" do expect(@attributes["override"]).to eq("cookbook override") end it "merges arrays within the default precedence" do @attributes.role_default["array"] = %w{role} @attributes.env_default["array"] = %w{env} expect(@attributes["array"]).to eq(%w{env role}) end it "merges arrays within the override precedence" do @attributes.role_override["array"] = %w{role} @attributes.env_override["array"] = %w{env} expect(@attributes["array"]).to eq(%w{role env}) end it "does not merge arrays between default and normal" do @attributes.role_default["array"] = %w{role} @attributes.normal["array"] = %w{normal} expect(@attributes["array"]).to eq(%w{normal}) end it "does not merge arrays between normal and override" do @attributes.normal["array"] = %w{normal} @attributes.role_override["array"] = %w{role} expect(@attributes["array"]).to eq(%w{role}) end it "merges nested hashes between precedence levels" do @attributes = Chef::Node::Attribute.new({}, {}, {}, {}) @attributes.env_default = {"a" => {"b" => {"default" => "default"}}} @attributes.normal = {"a" => {"b" => {"normal" => "normal"}}} @attributes.override = {"a" => {"override" => "role"}} @attributes.automatic = {"a" => {"automatic" => "auto"}} expect(@attributes["a"]).to eq({"b"=>{"default"=>"default", "normal"=>"normal"}, "override"=>"role", "automatic"=>"auto"}) end end describe "when reading combined default or override values" do before do @attributes.default["cd"] = "cookbook default" @attributes.role_default["rd"] = "role default" @attributes.env_default["ed"] = "env default" @attributes.default!["fd"] = "force default" @attributes.override["co"] = "cookbook override" @attributes.role_override["ro"] = "role override" @attributes.env_override["eo"] = "env override" @attributes.override!["fo"] = "force override" end it "merges all types of overrides into a combined override" do expect(@attributes.combined_override["co"]).to eq("cookbook override") expect(@attributes.combined_override["ro"]).to eq("role override") expect(@attributes.combined_override["eo"]).to eq("env override") expect(@attributes.combined_override["fo"]).to eq("force override") end it "merges all types of defaults into a combined default" do expect(@attributes.combined_default["cd"]).to eq("cookbook default") expect(@attributes.combined_default["rd"]).to eq("role default") expect(@attributes.combined_default["ed"]).to eq("env default") expect(@attributes.combined_default["fd"]).to eq("force default") end end describe "[]" do it "should return override data if it exists" do expect(@attributes["macaddress"]).to eq("00:00:00:00:00:00") end it "should return attribute data if it is not overridden" do expect(@attributes["platform"]).to eq("mac_os_x") end it "should return data that doesn't have corresponding keys in every hash" do expect(@attributes["command"]["ps"]).to eq("ps -ef") end it "should return default data if it is not overriden or in attribute data" do expect(@attributes["music"]["mastodon"]).to eq("rocks") end it "should prefer the override data over an available default" do expect(@attributes["music"]["mars_volta"]).to eq("cicatriz") end it "should prefer the attribute data over an available default" do expect(@attributes["music"]["jimmy_eat_world"]).to eq("nice") end it "should prefer override data over default data if there is no attribute data" do expect(@attributes["hot"]["day"]).to eq("sunday") end it "should return the merged hash if all three have values" do result = @attributes["music"] expect(result["mars_volta"]).to eq("cicatriz") expect(result["jimmy_eat_world"]).to eq("nice") expect(result["mastodon"]).to eq("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"] = { } expect { @attributes["the_ghost"]["exterminate"] = false }.to 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" } expect { @attributes.normal["the_ghost"]["eviscerate"]["tomorrow"] = false }.not_to raise_error end it "should set the attribute value" do @attributes.normal["longboard"] = "surfing" expect(@attributes.normal["longboard"]).to eq("surfing") expect(@attributes.normal["longboard"]).to eq("surfing") end it "should set deeply nested attribute values when a precedence level is specified" do @attributes.normal["deftones"]["hunters"]["nap"] = "surfing" expect(@attributes.normal["deftones"]["hunters"]["nap"]).to eq("surfing") end it "should die if you try and do nested attributes that do not exist without read vivification" do expect { @attributes["foo"]["bar"] = :baz }.to raise_error end it "should let you set attributes manually without vivification" do @attributes.normal["foo"] = Mash.new @attributes.normal["foo"]["bar"] = :baz expect(@attributes.normal["foo"]["bar"]).to eq(:baz) end it "should optionally skip setting the value if one already exists" do @attributes.set_unless_value_present = true @attributes.normal["hostname"] = "bar" expect(@attributes["hostname"]).to eq("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" expect(@attributes.normal["foo"]["bar"]).to eq({}) end end describe "to_hash" do it "should convert to a hash" do expect(@attributes.to_hash.class).to eq(Hash) end it "should convert to a hash based on current state" do hash = @attributes["hot"].to_hash expect(hash.class).to eq(Hash) expect(hash["day"]).to eq("sunday") end it "should create a deep copy of the node attribute" do @attributes.default['foo']['bar']['baz'] = 'fizz' hash = @attributes['foo'].to_hash expect(hash).to eql({"bar"=>{"baz"=>"fizz"}}) hash['bar']['baz'] = 'buzz' expect(hash).to eql({"bar"=>{"baz"=>"buzz"}}) expect(@attributes.default['foo']).to eql({"bar"=>{"baz"=>"fizz"}}) end it "should create a deep copy of arrays in the node attribute" do @attributes.default['foo']['bar'] = ['fizz'] hash = @attributes['foo'].to_hash expect(hash).to eql({"bar"=>[ 'fizz' ]}) hash['bar'].push('buzz') expect(hash).to eql({"bar"=>[ 'fizz', 'buzz' ]}) expect(@attributes.default['foo']).to eql({"bar"=>[ 'fizz' ]}) end it "mutating strings should not mutate the attributes" do pending "this is a bug that should be fixed" @attributes.default['foo']['bar']['baz'] = 'fizz' hash = @attributes['foo'].to_hash expect(hash).to eql({"bar"=>{"baz"=>"fizz"}}) hash['bar']['baz'] << 'buzz' expect(hash).to eql({"bar"=>{"baz"=>"fizzbuzz"}}) expect(@attributes.default['foo']).to eql({"bar"=>{"baz"=>"fizz"}}) end end describe "dup" do it "array can be duped even if some elements can't" do @attributes.default[:foo] = %w[foo bar baz] + Array(1..3) + [nil, true, false, [ "el", 0, nil ] ] @attributes.default[:foo].dup end end describe "has_key?" do it "should return true if an attribute exists" do expect(@attributes.has_key?("music")).to eq(true) end it "should return false if an attribute does not exist" do expect(@attributes.has_key?("ninja")).to eq(false) end it "should return false if an attribute does not exist using dot notation" do expect(@attributes.has_key?("does_not_exist_at_all")).to eq(false) end it "should return true if an attribute exists but is set to nil using dot notation" do expect(@attributes.music.deeper.has_key?("gates_of_ishtar")).to eq(true) end it "should return true if an attribute exists but is set to false" do @attributes.has_key?("music") expect(@attributes["music"].has_key?("apophis")).to eq(true) end it "does not find keys above the current nesting level" do expect(@attributes["music"]["this"]["apparatus"]).not_to have_key("this") end it "does not find keys below the current nesting level" do expect(@attributes["music"]["this"]).not_to have_key("must") end [:include?, :key?, :member?].each do |method| it "should alias the method #{method} to itself" do expect(@attributes).to respond_to(method) end it "#{method} should behave like has_key?" do expect(@attributes.send(method, "music")).to eq(true) end end end describe "attribute?" do it "should return true if an attribute exists" do expect(@attributes.attribute?("music")).to eq(true) end it "should return false if an attribute does not exist" do expect(@attributes.attribute?("ninja")).to eq(false) end end describe "method_missing" do it "should behave like a [] lookup" do expect(@attributes.music.mastodon).to eq("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" ] expect(@attributes.normal.music.mastodon).to eq([ "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 expect(collect.include?("one")).to eq(true) expect(collect.include?("hut")).to eq(true) expect(collect.include?("snakes")).to eq(true) expect(collect.include?("snack")).to eq(true) expect(collect.include?("place")).to eq(true) expect(collect.length).to eq(5) end it "should yield lower if we go deeper" do collect = Array.new @attributes.one.keys.each do |k| collect << k end expect(collect.include?("two")).to eq(true) expect(collect.include?("four")).to eq(true) expect(collect.include?("six")).to eq(true) expect(collect.length).to eq(3) end it "should not raise an exception if one of the hashes has a nil value on a deep lookup" do expect { @attributes.place.keys { |k| } }.not_to raise_error 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 expect(collect["one"]).to eq("six") expect(collect["hut"]).to eq("three") expect(collect["snakes"]).to eq("on a plane") expect(collect["snack"]).to eq("cookies") end it "should yield as a two-element array" do @attributes.each do |a| expect(a).to 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 expect(@attributes).to 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 expect(collect).to include("one") expect(collect).to include("snack") expect(collect).to include("hut") expect(collect).to 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 expect(@attributes).to 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 expect(collect["one"]).to eq("six") expect(collect["hut"]).to eq("three") expect(collect["snakes"]).to eq("on a plane") expect(collect["snack"]).to eq("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 expect(@attributes).to 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 expect(collect).to include("cookies") expect(collect).to include("three") expect(collect).to include("on a plane") end it "should yield four elements" do collect = Array.new @attributes.each_value do |v| collect << v end expect(collect.length).to eq(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 expect(@attributes).to respond_to(:empty?) end it "should return true when there are no keys" do expect(@empty.empty?).to eq(true) end it "should return false when there are keys" do expect(@attributes.empty?).to eq(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 expect(@attributes).to 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| expect(@attributes.fetch(k)).to eq(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 expect { @attributes.fetch("lololol") }.to raise_error(IndexError) end end describe "and a default arg is passed" do it "should return the value of the default arg" do expect(@attributes.fetch("lol", "blah")).to eq("blah") end end describe "and a block is passed" do it "should run the block and return its value" do expect(@attributes.fetch("lol") { |x| "#{x}, blah" }).to eq("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 expect(@attributes).to respond_to(:has_value?) end it "should return true if any key has the value supplied" do expect(@attributes.has_value?("cookies")).to eq(true) end it "should return false no key has the value supplied" do expect(@attributes.has_value?("lololol")).to eq(false) end it "should alias value?" do expect(@attributes).to respond_to(:value?) end end describe "index" do # Hash#index is deprecated and triggers warnings. def silence old_verbose = $VERBOSE $VERBOSE = nil yield ensure $VERBOSE = old_verbose end 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 expect(@attributes).to respond_to(:index) end describe "when the value is indexed" do it "should return the index" do silence do expect(@attributes.index("six")).to eq("one") end end end describe "when the value is not indexed" do it "should return nil" do silence do expect(@attributes.index("lolol")).to eq(nil) end 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 expect(@attributes).to respond_to(:values) end it "should return an array of values" do expect(@attributes.values.length).to eq(4) end it "should match the values output from each" do expect(@attributes.values).to include("six") expect(@attributes.values).to include("cookies") expect(@attributes.values).to include("three") expect(@attributes.values).to 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 expect(@attributes).to respond_to(:select) end if RUBY_VERSION >= "1.8.7" it "should not raise a LocalJumpError if no block is given" do expect { @attributes.select }.not_to raise_error end else it "should raise a LocalJumpError if no block is given" do expect{ @attributes.select }.to raise_error(LocalJumpError) end end it "should return an empty hash/array (ruby-version-dependent) for a block containing nil" do expect(@attributes.select { nil }).to eq({}.select { nil }) end # sorted for spec clarity it "should return a new array of k,v pairs for which the block returns true" do expect(@attributes.select { true }.sort).to eq( [ ["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 expect(@attributes).to respond_to(:size) end it "should alias length to size" do expect(@attributes).to respond_to(:length) end it "should return 0 for an empty attribute" do expect(@empty.size).to eq(0) end it "should return the number of pairs" do expect(@attributes.size).to eq(4) end end describe "kind_of?" do it "should falsely inform you that it is a Hash" do expect(@attributes).to be_a_kind_of(Hash) end it "should falsely inform you that it is a Mash" do expect(@attributes).to be_a_kind_of(Mash) end it "should inform you that it is a Chef::Node::Attribute" do expect(@attributes).to be_a_kind_of(Chef::Node::Attribute) end it "should inform you that it is anything else" do expect(@attributes).not_to be_a_kind_of(Chef::Node) end end describe "to_s" do it "should output simple attributes" do attributes = Chef::Node::Attribute.new(nil, nil, nil, nil) expect(attributes.to_s).to eq("{}") end it "should output merged attributes" do default_hash = { "a" => 1, "b" => 2 } override_hash = { "b" => 3, "c" => 4 } attributes = Chef::Node::Attribute.new(nil, default_hash, override_hash, nil) expect(attributes.to_s).to eq('{"a"=>1, "b"=>3, "c"=>4}') 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... expect(@attributes.inspect).to match(/@automatic=\{.*\}/) expect(@attributes.inspect).to match(/@normal=\{.*\}/) end end describe "when not mutated" do it "does not reset the cache when dup'd [CHEF-3680]" do @attributes.default[:foo][:bar] = "set on original" subtree = @attributes[:foo] @attributes.default[:foo].dup[:bar] = "set on dup" expect(subtree[:bar]).to eq("set on original") 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" expect(@attributes.merged_attributes[:foo]).to eq("bar") end it "converts the input in to a VividMash tree (normal)" do @attributes.normal = {} @attributes.normal.foo = "bar" expect(@attributes.merged_attributes[:foo]).to eq("bar") end it "converts the input in to a VividMash tree (override)" do @attributes.override = {} @attributes.override.foo = "bar" expect(@attributes.merged_attributes[:foo]).to eq("bar") end it "converts the input in to a VividMash tree (automatic)" do @attributes.automatic = {} @attributes.automatic.foo = "bar" expect(@attributes.merged_attributes[:foo]).to eq("bar") end end describe "when deep-merging between precedence levels" do it "correctly deep merges hashes and preserves the original contents" do @attributes.default = { "arglebargle" => { "foo" => "bar" } } @attributes.override = { "arglebargle" => { "fizz" => "buzz" } } expect(@attributes.merged_attributes[:arglebargle]).to eq({ "foo" => "bar", "fizz" => "buzz" }) expect(@attributes.default[:arglebargle]).to eq({ "foo" => "bar" }) expect(@attributes.override[:arglebargle]).to eq({ "fizz" => "buzz" }) end it "does not deep merge arrays, and preserves the original contents" do @attributes.default = { "arglebargle" => [ 1, 2, 3 ] } @attributes.override = { "arglebargle" => [ 4, 5, 6 ] } expect(@attributes.merged_attributes[:arglebargle]).to eq([ 4, 5, 6 ]) expect(@attributes.default[:arglebargle]).to eq([ 1, 2, 3 ]) expect(@attributes.override[:arglebargle]).to eq([ 4, 5, 6 ]) end it "correctly deep merges hashes and preserves the original contents when merging default and role_default" do @attributes.default = { "arglebargle" => { "foo" => "bar" } } @attributes.role_default = { "arglebargle" => { "fizz" => "buzz" } } expect(@attributes.merged_attributes[:arglebargle]).to eq({ "foo" => "bar", "fizz" => "buzz" }) expect(@attributes.default[:arglebargle]).to eq({ "foo" => "bar" }) expect(@attributes.role_default[:arglebargle]).to eq({ "fizz" => "buzz" }) end it "correctly deep merges arrays, and preserves the original contents when merging default and role_default" do @attributes.default = { "arglebargle" => [ 1, 2, 3 ] } @attributes.role_default = { "arglebargle" => [ 4, 5, 6 ] } expect(@attributes.merged_attributes[:arglebargle]).to eq([ 1, 2, 3, 4, 5, 6 ]) expect(@attributes.default[:arglebargle]).to eq([ 1, 2, 3 ]) expect(@attributes.role_default[:arglebargle]).to eq([ 4, 5, 6 ]) end end describe "when attemping to write without specifying precedence" do it "raises an error when using []=" do expect { @attributes[:new_key] = "new value" }.to raise_error(Chef::Exceptions::ImmutableAttributeModification) end it "raises an error when using `attr=value`" do expect { @attributes.new_key = "new value" }.to raise_error(Chef::Exceptions::ImmutableAttributeModification) end end end