spec/unit/policyfile_includes_spec.rb in chef-dk-3.0.36 vs spec/unit/policyfile_includes_spec.rb in chef-dk-3.1.0

- old
+ new

@@ -1,720 +1,720 @@ -# -*- coding: UTF-8 -*- -# -# Copyright:: Copyright (c) 2014-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" -require "chef-dk/policyfile_compiler" -require "chef-dk/exceptions" - -describe ChefDK::PolicyfileCompiler, "including upstream policy locks" do - - def expand_run_list(r) - r.map do |item| - "recipe[#{item}]" - end - end - - let(:run_list) { ["local::default"] } - let(:run_list_expanded) { expand_run_list(run_list) } - let(:named_run_list) { {} } - let(:named_run_list_expanded) do - named_run_list.inject({}) do |acc, (key, val)| - acc[key] = expand_run_list(val) - acc - end - end - let(:default_attributes) { {} } - let(:override_attributes) { {} } - - let(:default_source) { nil } - - let(:external_cookbook_universe) do - { - "cookbookA" => { - "1.0.0" => [ ], - "2.0.0" => [ ], - }, - "cookbookB" => { - "1.0.0" => [ ], - "2.0.0" => [ ], - }, - "cookbookC" => { - "1.0.0" => [ ], - "2.0.0" => [ ], - }, - "local" => { - "1.0.0" => [ ["cookbookC", "= 1.0.0" ] ], - }, - "local_easy" => { - "1.0.0" => [ ["cookbookC", "= 2.0.0" ] ], - }, - } - end - - let(:included_policy_default_attributes) { {} } - let(:included_policy_override_attributes) { {} } - let(:included_policy_expanded_named_runlist) { nil } - let(:included_policy_expanded_runlist) { ["recipe[cookbookA::default]"] } - let(:included_policy_cookbooks) do - [ - { - name: "cookbookA", - version: "2.0.0", - }, - ] - end - - let(:included_policy_lock_data) do - cookbook_locks = included_policy_cookbooks.inject({}) do |acc, cookbook_info| - acc[cookbook_info[:name]] = { - "version" => cookbook_info[:version], - "identifier" => "identifier", - "dotted_decimal_identifier" => "dotted_decimal_identifier", - "cache_key" => "#{cookbook_info[:name]}-#{cookbook_info[:version]}", - "origin" => "uri", - "source_options" => {}, - } - acc - end - - solution_dependencies_lock = included_policy_cookbooks.map do |cookbook_info| - [cookbook_info[:name], cookbook_info[:version]] - end - - solution_dependencies_cookbooks = included_policy_cookbooks.inject({}) do |acc, cookbook_info| - acc["#{cookbook_info[:name]} (#{cookbook_info[:version]})"] = external_cookbook_universe[cookbook_info[:name]][cookbook_info[:version]] - acc - end - - { - "name" => "included_policyfile", - "revision_id" => "myrevisionid", - "run_list" => included_policy_expanded_runlist, - "cookbook_locks" => cookbook_locks, - "default_attributes" => included_policy_default_attributes, - "override_attributes" => included_policy_override_attributes, - "solution_dependencies" => { - "Policyfile" => solution_dependencies_lock, - "dependencies" => solution_dependencies_cookbooks, - }, - }.tap do |core| - core["named_run_lists"] = included_policy_expanded_named_runlist if included_policy_expanded_named_runlist - end - end - - let(:included_policy_lock_name) { "included" } - let(:included_policy_fetcher) do - instance_double("ChefDK::Policyfile::LocalLockFetcher").tap do |double| - allow(double).to receive(:lock_data).and_return(included_policy_lock_data) - allow(double).to receive(:valid?).and_return(true) - allow(double).to receive(:errors).and_return([]) - end - end - - let(:lock_source_options) { { :path => "somelocation" } } - let(:included_policy_lock_spec) do - ChefDK::Policyfile::PolicyfileLocationSpecification.new(included_policy_lock_name, lock_source_options, nil).tap do |spec| - allow(spec).to receive(:valid?).and_return(true) - allow(spec).to receive(:fetcher).and_return(included_policy_fetcher) - allow(spec).to receive(:source_options_for_lock).and_return(lock_source_options) - end - end - - let(:included_policies) { [] } - - let(:policyfile) do - policyfile = ChefDK::PolicyfileCompiler.new.build do |p| - if default_source - p.default_source.replace([default_source]) - else - allow(p.default_source.first).to receive(:universe_graph).and_return(external_cookbook_universe) - allow(p.default_source.first).to receive(:null?).and_return(false) - end - p.run_list(*run_list) - - named_run_list.each do |name, run_list| - p.named_run_list(name, *run_list) - end - - default_attributes.each do |(name, value)| - p.default[name] = value - end - - override_attributes.each do |(name, value)| - p.override[name] = value - end - - allow(p).to receive(:included_policies).and_return(included_policies) - end - - policyfile - end - - let(:policyfile_lock) do - policyfile.lock - end - - context "when no policies are included" do - - it "does not emit included policies information in the lockfile" do - expect(policyfile_lock.to_lock["included_policies"]).to eq(nil) - end - - end - - context "when one policy is included" do - - let(:included_policies) { [included_policy_lock_spec] } - - # currently you must have a run list in a policyfile, but it should now - # become possible to make a combo-policy just by combining other policies - context "when the including policy does not have a run list" do - let(:run_list) { [] } - - it "emits a lockfile with an identical run list as the included policy" do - expect(policyfile_lock.to_lock["run_list"]).to eq(included_policy_expanded_runlist) - end - - end - - context "when the including policy has a run list" do - - it "appends run list items from the including policy to the included policy's run list, removing duplicates" do - expect(policyfile_lock.to_lock["run_list"]).to eq(included_policy_expanded_runlist + run_list_expanded) - end - - end - - context "when the policies have named run lists" do - - let(:included_policy_expanded_named_runlist) do - { - "shared" => ["recipe[cookbookA::included]"], - } - end - - context "and no named run lists are shared between the including and included policy" do - - let(:named_run_list) do - { - "local" => ["local::foo"], - } - end - - it "preserves the named run lists as given in both policies" do - expect(policyfile_lock.to_lock["named_run_lists"]).to include(included_policy_expanded_named_runlist, named_run_list_expanded) - end - - end - - context "and some named run lists are shared between the including and included policy" do - - let(:named_run_list) do - { - "shared" => ["local::foo"], - } - end - - it "appends run lists items from the including policy's run lists to the included policy's run lists" do - expect(policyfile_lock.to_lock["named_run_lists"]["shared"]).to eq(included_policy_expanded_named_runlist["shared"] + named_run_list_expanded["shared"]) - end - - end - - end - - context "when no cookbooks are shared as dependencies or transitive dependencies" do - let(:included_policy_expanded_runlist) { ["recipe[cookbookC::default]"] } - let(:run_list) { ["cookbookA::default"] } - - it "does not raise a have conflicting dependency requirements error" do - expect { policyfile_lock.to_lock }.not_to raise_error - end - - it "emits a lockfile where cookbooks pulled from the upstream are at identical versions" do - expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to( - have_key("cookbookC (2.0.0)")) - end - end - - context "when some cookbooks are shared as dependencies or transitive dependencies" do - let(:included_policy_expanded_runlist) { ["recipe[cookbookC::default]"] } - let(:included_policy_cookbooks) do - [ - { - name: "cookbookC", - version: "2.0.0", - }, - ] - end - - context "and the including policy does not specify any sources" do - let(:run_list) { [] } - it "it defaults to those provided in the included policy lock" do - expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to( - have_key("cookbookC (2.0.0)")) - end - end - - context "and the including policy specifies a source that is equivalent to the included policy" do - let(:run_list) { [] } - let(:default_source) { instance_double("ChefDK::Policyfile::NullCookbookSource") } - - before do - allow(default_source).to receive(:preferred_cookbooks).and_return(["cookbookC"]) - allow(default_source).to receive(:source_options_for).with("cookbookC", "2.0.0").and_return({}) - allow(default_source).to receive(:null?).and_return(false) - allow(default_source).to receive(:universe_graph).and_return(external_cookbook_universe) - allow(default_source).to receive(:desc).and_return("source double") - end - - it "it defaults to those provided in the included policy lock" do - expect { policyfile_lock.to_lock }.not_to raise_error - end - end - - context "and the including policy specifies a source that is not equivalent to the included policy" do - let(:run_list) { [] } - let(:default_source) { instance_double("ChefDK::Policyfile::NullCookbookSource") } - - before do - allow(default_source).to receive(:preferred_cookbooks).and_return(["cookbookC"]) - allow(default_source).to receive(:source_options_for).with("cookbookC", "2.0.0").and_return({ "foo" => "bar" }) - allow(default_source).to receive(:null?).and_return(false) - allow(default_source).to receive(:universe_graph).and_return(external_cookbook_universe) - allow(default_source).to receive(:desc).and_return("source double") - end - - it "it raises an error" do - expect { policyfile_lock.to_lock }.to raise_error(ChefDK::IncludePolicyCookbookSourceConflict) - end - end - - context "and the including policy's dependencies can be solved with the included policy's locks" do - let(:run_list) { ["local_easy::default"] } - - it "solves the dependencies added by the top-level policyfile and emits them in the lockfile" do - expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to( - have_key("cookbookC (2.0.0)")) - end - - end - - context "and the including policy's dependencies cannot be solved with the included policy's locks" do - let(:run_list) { ["local::default"] } - - it "raises an error describing the conflict" do - expect { policyfile_lock.to_lock }.to raise_error(Solve::Errors::NoSolutionError) - end - - it "includes the source of the conflicting dependency constraint from the including policy" do - expect { policyfile_lock.to_lock }.to raise_error(Solve::Errors::NoSolutionError) do |e| - expect(e.to_s).to match(/`cookbookC \(= 2.0.0\)`/) # This one comes from the included policy - expect(e.to_s).to match(/`cookbookC \(= 1.0.0\)` required by `local-1.0.0`/) # This one comes from the included policy - end - end - end - end - - context "when default attributes are specified" do - let(:default_attributes) do - { - "shared" => { - "foo" => "bar", - }, - } - end - - context "when the included policy does not have attributes that conflict with the including policy" do - let(:included_policy_default_attributes) do - { - "not_shared" => { - "foo" => "bar", - }, - } - end - - it "emits a lockfile with the attributes from both merged" do - expect(policyfile_lock.to_lock["default_attributes"]).to include(included_policy_default_attributes, default_attributes) - end - - end - - context "when the included policy has attributes that conflict with the including policy, but provide the same value" do - let(:included_policy_default_attributes) { default_attributes } - - it "emits a lockfile with the attributes from both merged" do - expect(policyfile_lock.to_lock["default_attributes"]).to eq(default_attributes) - end - - end - - context "when the included policy has attributes that conflict with the including policy's attributes" do - let(:included_policy_default_attributes) do - { - "shared" => { - "foo" => "not_bar", - }, - } - end - - it "raises an error describing all attribute conflicts" do - expect { policyfile_lock.to_lock }.to raise_error( - ChefDK::Policyfile::AttributeMergeChecker::ConflictError, - "Attribute '[shared][foo]' provided conflicting values by the following sources [\"user-specified\", \"included\"]") - end - end - end - - context "when override attributes are specified" do - let(:override_attributes) do - { - "shared" => { - "foo" => "bar", - }, - } - end - - context "when the included policy does not have attributes that conflict with the including policy" do - let(:included_policy_override_attributes) do - { - "not_shared" => { - "foo" => "bar", - }, - } - end - - it "emits a lockfile with the attributes from both merged" do - expect(policyfile_lock.to_lock["override_attributes"]).to include(included_policy_override_attributes, override_attributes) - end - - end - - context "when the included policy has attributes that conflict with the including policy, but provide the same value" do - let(:included_policy_override_attributes) { override_attributes } - - it "emits a lockfile with the attributes from both merged" do - expect(policyfile_lock.to_lock["override_attributes"]).to eq(override_attributes) - end - - end - - context "when the included policy has attributes that conflict with the including policy's attributes" do - let(:included_policy_override_attributes) do - { - "shared" => { - "foo" => "not_bar", - }, - } - end - - it "raises an error describing all attribute conflicts" do - expect { policyfile_lock.to_lock }.to raise_error( - ChefDK::Policyfile::AttributeMergeChecker::ConflictError, - "Attribute '[shared][foo]' provided conflicting values by the following sources [\"user-specified\", \"included\"]") - end - end - end - end - - context "when several policies are included" do - let(:included_policy_2_default_attributes) { {} } - let(:included_policy_2_override_attributes) { {} } - let(:included_policy_2_expanded_named_runlist) { nil } - let(:included_policy_2_expanded_runlist) { ["recipe[cookbookA::default]"] } - let(:included_policy_2_cookbooks) do - [ - { - name: "cookbookA", - version: "2.0.0", - }, - ] - end - - let(:included_policy_2_lock_data) do - cookbook_locks = included_policy_2_cookbooks.inject({}) do |acc, cookbook_info| - acc[cookbook_info[:name]] = { - "version" => cookbook_info[:version], - "identifier" => "identifier", - "dotted_decimal_identifier" => "dotted_decimal_identifier", - "cache_key" => "#{cookbook_info[:name]}-#{cookbook_info[:version]}", - "origin" => "uri", - "source_options" => {}, - } - acc - end - - solution_dependencies_lock = included_policy_2_cookbooks.map do |cookbook_info| - [cookbook_info[:name], cookbook_info[:version]] - end - - solution_dependencies_cookbooks = included_policy_2_cookbooks.inject({}) do |acc, cookbook_info| - acc["#{cookbook_info[:name]} (#{cookbook_info[:version]})"] = external_cookbook_universe[cookbook_info[:name]][cookbook_info[:version]] - acc - end - - { - "name" => "included_policy_2file", - "revision_id" => "myrevisionid", - "run_list" => included_policy_2_expanded_runlist, - "cookbook_locks" => cookbook_locks, - "default_attributes" => included_policy_2_default_attributes, - "override_attributes" => included_policy_2_override_attributes, - "solution_dependencies" => { - "Policyfile" => solution_dependencies_lock, - "dependencies" => solution_dependencies_cookbooks, - }, - }.tap do |core| - core["named_run_lists"] = included_policy_2_expanded_named_runlist if included_policy_2_expanded_named_runlist - end - end - - let(:included_policy_2_lock_name) { "included2" } - let(:included_policy_2_fetcher) do - instance_double("ChefDK::Policyfile::LocalLockFetcher").tap do |double| - allow(double).to receive(:lock_data).and_return(included_policy_2_lock_data) - allow(double).to receive(:valid?).and_return(true) - allow(double).to receive(:errors).and_return([]) - end - end - - let(:included_policy_2_lock_spec) do - ChefDK::Policyfile::PolicyfileLocationSpecification.new(included_policy_2_lock_name, lock_source_options, nil).tap do |spec| - allow(spec).to receive(:valid?).and_return(true) - allow(spec).to receive(:fetcher).and_return(included_policy_2_fetcher) - allow(spec).to receive(:source_options_for_lock).and_return(lock_source_options) - end - end - - let(:included_policies) { [included_policy_lock_spec, included_policy_2_lock_spec] } - - let(:run_list) { ["local::default"] } - - context "when no cookbooks are shared as dependencies or transitive dependencies by included policies" do - let(:included_policy_expanded_runlist) { ["recipe[cookbookA::default]"] } - let(:included_policy_cookbooks) do - [ - { - name: "cookbookA", - version: "2.0.0", - }, - ] - end - - let(:included_policy_2_expanded_runlist) { ["recipe[cookbookB::default]"] } - let(:included_policy_2_cookbooks) do - [ - { - name: "cookbookB", - version: "2.0.0", - }, - ] - end - - it "does not raise a have conflicting dependency requirements error" do - expect { policyfile_lock.to_lock }.not_to raise_error - end - - it "emits a lockfile with the correct dependencies" do - expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to eq({ - "cookbookA (2.0.0)" => [], - "cookbookB (2.0.0)" => [], - "cookbookC (1.0.0)" => [], - "local (1.0.0)" => [["cookbookC", "= 1.0.0"]], - }) - end - end - - context "when some cookbooks appear as dependencies or transitive dependencies of some included policies" do - let(:included_policy_expanded_runlist) { ["recipe[cookbookC::default]"] } - let(:included_policy_2_expanded_runlist) { ["recipe[cookbookC::default]"] } - - context "and the locked versions of the cookbooks match" do - let(:included_policy_cookbooks) do - [ - { - name: "cookbookC", - version: "1.0.0", - }, - ] - end - - let(:included_policy_2_cookbooks) do - [ - { - name: "cookbookC", - version: "1.0.0", - }, - ] - end - - it "solves the dependencies with the matching versions" do - expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to eq({ - "cookbookC (1.0.0)" => [], - "local (1.0.0)" => [["cookbookC", "= 1.0.0"]], - }) - end - end - - context "and the locked versions of the cookbooks do not match" do - let(:included_policy_cookbooks) do - [ - { - name: "cookbookC", - version: "1.0.0", - }, - ] - end - - let(:included_policy_2_cookbooks) do - [ - { - name: "cookbookC", - version: "2.0.0", - }, - ] - end - - it "raises an error describing the conflict" do - expect { policyfile_lock }.to raise_error( - ChefDK::Policyfile::IncludedPoliciesCookbookSource::ConflictingCookbookVersions, - /Multiple versions provided for cookbook cookbookC/ - ) - end - end - end - - context "when default attributes are specified" do - context "when the included policies do not have conflicting attributes" do - let(:included_policy_default_attributes) do - { - "not_conflict" => { - "foo" => "bar", - }, - } - end - let(:included_policy_2_default_attributes) do - { - "not_conflict" => { - "foo" => "bar", - }, - } - end - let(:default_attributes) do - { - "not_conflict" => { - "bar" => "baz", - }, - } - end - - it "emits a lockfile with the included policies' attributes merged" do - expect(policyfile_lock.to_lock["default_attributes"]).to eq({ - "not_conflict" => { - "foo" => "bar", - "bar" => "baz", - }, - }) - end - end - - context "when the included policies have conflicting attributes" do - let(:included_policy_default_attributes) do - { - "conflict" => { - "foo" => "bar", - }, - } - end - - let(:included_policy_2_default_attributes) do - { - "conflict" => { - "foo" => "baz", - }, - } - end - - it "raises an error describing the conflict" do - expect { policyfile_lock }.to raise_error( - ChefDK::Policyfile::AttributeMergeChecker::ConflictError, - "Attribute '[conflict][foo]' provided conflicting values by the following sources [\"included\", \"included2\"]") - end - end - end - - context "when override attributes are specified" do - context "when the included policies do not have conflicting attributes" do - let(:included_policy_override_attributes) do - { - "not_conflict" => { - "foo" => "bar", - }, - } - end - let(:included_policy_2_override_attributes) do - { - "not_conflict" => { - "foo" => "bar", - }, - } - end - let(:override_attributes) do - { - "not_conflict" => { - "bar" => "baz", - }, - } - end - - it "emits a lockfile with the included policies' attributes merged" do - expect(policyfile_lock.to_lock["override_attributes"]).to eq({ - "not_conflict" => { - "foo" => "bar", - "bar" => "baz", - }, - }) - end - end - - context "when the included policies have conflicting attributes" do - let(:included_policy_override_attributes) do - { - "conflict" => { - "foo" => "bar", - }, - } - end - - let(:included_policy_2_override_attributes) do - { - "conflict" => { - "foo" => "baz", - }, - } - end - - it "raises an error describing the conflict" do - expect { policyfile_lock }.to raise_error( - ChefDK::Policyfile::AttributeMergeChecker::ConflictError, - "Attribute '[conflict][foo]' provided conflicting values by the following sources [\"included\", \"included2\"]") - end - end - end - - end -end +# -*- coding: UTF-8 -*- +# +# Copyright:: Copyright (c) 2014-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" +require "chef-dk/policyfile_compiler" +require "chef-dk/exceptions" + +describe ChefDK::PolicyfileCompiler, "including upstream policy locks" do + + def expand_run_list(r) + r.map do |item| + "recipe[#{item}]" + end + end + + let(:run_list) { ["local::default"] } + let(:run_list_expanded) { expand_run_list(run_list) } + let(:named_run_list) { {} } + let(:named_run_list_expanded) do + named_run_list.inject({}) do |acc, (key, val)| + acc[key] = expand_run_list(val) + acc + end + end + let(:default_attributes) { {} } + let(:override_attributes) { {} } + + let(:default_source) { nil } + + let(:external_cookbook_universe) do + { + "cookbookA" => { + "1.0.0" => [ ], + "2.0.0" => [ ], + }, + "cookbookB" => { + "1.0.0" => [ ], + "2.0.0" => [ ], + }, + "cookbookC" => { + "1.0.0" => [ ], + "2.0.0" => [ ], + }, + "local" => { + "1.0.0" => [ ["cookbookC", "= 1.0.0" ] ], + }, + "local_easy" => { + "1.0.0" => [ ["cookbookC", "= 2.0.0" ] ], + }, + } + end + + let(:included_policy_default_attributes) { {} } + let(:included_policy_override_attributes) { {} } + let(:included_policy_expanded_named_runlist) { nil } + let(:included_policy_expanded_runlist) { ["recipe[cookbookA::default]"] } + let(:included_policy_cookbooks) do + [ + { + name: "cookbookA", + version: "2.0.0", + }, + ] + end + + let(:included_policy_lock_data) do + cookbook_locks = included_policy_cookbooks.inject({}) do |acc, cookbook_info| + acc[cookbook_info[:name]] = { + "version" => cookbook_info[:version], + "identifier" => "identifier", + "dotted_decimal_identifier" => "dotted_decimal_identifier", + "cache_key" => "#{cookbook_info[:name]}-#{cookbook_info[:version]}", + "origin" => "uri", + "source_options" => {}, + } + acc + end + + solution_dependencies_lock = included_policy_cookbooks.map do |cookbook_info| + [cookbook_info[:name], cookbook_info[:version]] + end + + solution_dependencies_cookbooks = included_policy_cookbooks.inject({}) do |acc, cookbook_info| + acc["#{cookbook_info[:name]} (#{cookbook_info[:version]})"] = external_cookbook_universe[cookbook_info[:name]][cookbook_info[:version]] + acc + end + + { + "name" => "included_policyfile", + "revision_id" => "myrevisionid", + "run_list" => included_policy_expanded_runlist, + "cookbook_locks" => cookbook_locks, + "default_attributes" => included_policy_default_attributes, + "override_attributes" => included_policy_override_attributes, + "solution_dependencies" => { + "Policyfile" => solution_dependencies_lock, + "dependencies" => solution_dependencies_cookbooks, + }, + }.tap do |core| + core["named_run_lists"] = included_policy_expanded_named_runlist if included_policy_expanded_named_runlist + end + end + + let(:included_policy_lock_name) { "included" } + let(:included_policy_fetcher) do + instance_double("ChefDK::Policyfile::LocalLockFetcher").tap do |double| + allow(double).to receive(:lock_data).and_return(included_policy_lock_data) + allow(double).to receive(:valid?).and_return(true) + allow(double).to receive(:errors).and_return([]) + end + end + + let(:lock_source_options) { { :path => "somelocation" } } + let(:included_policy_lock_spec) do + ChefDK::Policyfile::PolicyfileLocationSpecification.new(included_policy_lock_name, lock_source_options, nil).tap do |spec| + allow(spec).to receive(:valid?).and_return(true) + allow(spec).to receive(:fetcher).and_return(included_policy_fetcher) + allow(spec).to receive(:source_options_for_lock).and_return(lock_source_options) + end + end + + let(:included_policies) { [] } + + let(:policyfile) do + policyfile = ChefDK::PolicyfileCompiler.new.build do |p| + if default_source + p.default_source.replace([default_source]) + else + allow(p.default_source.first).to receive(:universe_graph).and_return(external_cookbook_universe) + allow(p.default_source.first).to receive(:null?).and_return(false) + end + p.run_list(*run_list) + + named_run_list.each do |name, run_list| + p.named_run_list(name, *run_list) + end + + default_attributes.each do |(name, value)| + p.default[name] = value + end + + override_attributes.each do |(name, value)| + p.override[name] = value + end + + allow(p).to receive(:included_policies).and_return(included_policies) + end + + policyfile + end + + let(:policyfile_lock) do + policyfile.lock + end + + context "when no policies are included" do + + it "does not emit included policies information in the lockfile" do + expect(policyfile_lock.to_lock["included_policies"]).to eq(nil) + end + + end + + context "when one policy is included" do + + let(:included_policies) { [included_policy_lock_spec] } + + # currently you must have a run list in a policyfile, but it should now + # become possible to make a combo-policy just by combining other policies + context "when the including policy does not have a run list" do + let(:run_list) { [] } + + it "emits a lockfile with an identical run list as the included policy" do + expect(policyfile_lock.to_lock["run_list"]).to eq(included_policy_expanded_runlist) + end + + end + + context "when the including policy has a run list" do + + it "appends run list items from the including policy to the included policy's run list, removing duplicates" do + expect(policyfile_lock.to_lock["run_list"]).to eq(included_policy_expanded_runlist + run_list_expanded) + end + + end + + context "when the policies have named run lists" do + + let(:included_policy_expanded_named_runlist) do + { + "shared" => ["recipe[cookbookA::included]"], + } + end + + context "and no named run lists are shared between the including and included policy" do + + let(:named_run_list) do + { + "local" => ["local::foo"], + } + end + + it "preserves the named run lists as given in both policies" do + expect(policyfile_lock.to_lock["named_run_lists"]).to include(included_policy_expanded_named_runlist, named_run_list_expanded) + end + + end + + context "and some named run lists are shared between the including and included policy" do + + let(:named_run_list) do + { + "shared" => ["local::foo"], + } + end + + it "appends run lists items from the including policy's run lists to the included policy's run lists" do + expect(policyfile_lock.to_lock["named_run_lists"]["shared"]).to eq(included_policy_expanded_named_runlist["shared"] + named_run_list_expanded["shared"]) + end + + end + + end + + context "when no cookbooks are shared as dependencies or transitive dependencies" do + let(:included_policy_expanded_runlist) { ["recipe[cookbookC::default]"] } + let(:run_list) { ["cookbookA::default"] } + + it "does not raise a have conflicting dependency requirements error" do + expect { policyfile_lock.to_lock }.not_to raise_error + end + + it "emits a lockfile where cookbooks pulled from the upstream are at identical versions" do + expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to( + have_key("cookbookC (2.0.0)")) + end + end + + context "when some cookbooks are shared as dependencies or transitive dependencies" do + let(:included_policy_expanded_runlist) { ["recipe[cookbookC::default]"] } + let(:included_policy_cookbooks) do + [ + { + name: "cookbookC", + version: "2.0.0", + }, + ] + end + + context "and the including policy does not specify any sources" do + let(:run_list) { [] } + it "it defaults to those provided in the included policy lock" do + expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to( + have_key("cookbookC (2.0.0)")) + end + end + + context "and the including policy specifies a source that is equivalent to the included policy" do + let(:run_list) { [] } + let(:default_source) { instance_double("ChefDK::Policyfile::NullCookbookSource") } + + before do + allow(default_source).to receive(:preferred_cookbooks).and_return(["cookbookC"]) + allow(default_source).to receive(:source_options_for).with("cookbookC", "2.0.0").and_return({}) + allow(default_source).to receive(:null?).and_return(false) + allow(default_source).to receive(:universe_graph).and_return(external_cookbook_universe) + allow(default_source).to receive(:desc).and_return("source double") + end + + it "it defaults to those provided in the included policy lock" do + expect { policyfile_lock.to_lock }.not_to raise_error + end + end + + context "and the including policy specifies a source that is not equivalent to the included policy" do + let(:run_list) { [] } + let(:default_source) { instance_double("ChefDK::Policyfile::NullCookbookSource") } + + before do + allow(default_source).to receive(:preferred_cookbooks).and_return(["cookbookC"]) + allow(default_source).to receive(:source_options_for).with("cookbookC", "2.0.0").and_return({ "foo" => "bar" }) + allow(default_source).to receive(:null?).and_return(false) + allow(default_source).to receive(:universe_graph).and_return(external_cookbook_universe) + allow(default_source).to receive(:desc).and_return("source double") + end + + it "it raises an error" do + expect { policyfile_lock.to_lock }.to raise_error(ChefDK::IncludePolicyCookbookSourceConflict) + end + end + + context "and the including policy's dependencies can be solved with the included policy's locks" do + let(:run_list) { ["local_easy::default"] } + + it "solves the dependencies added by the top-level policyfile and emits them in the lockfile" do + expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to( + have_key("cookbookC (2.0.0)")) + end + + end + + context "and the including policy's dependencies cannot be solved with the included policy's locks" do + let(:run_list) { ["local::default"] } + + it "raises an error describing the conflict" do + expect { policyfile_lock.to_lock }.to raise_error(Solve::Errors::NoSolutionError) + end + + it "includes the source of the conflicting dependency constraint from the including policy" do + expect { policyfile_lock.to_lock }.to raise_error(Solve::Errors::NoSolutionError) do |e| + expect(e.to_s).to match(/`cookbookC \(= 2.0.0\)`/) # This one comes from the included policy + expect(e.to_s).to match(/`cookbookC \(= 1.0.0\)` required by `local-1.0.0`/) # This one comes from the included policy + end + end + end + end + + context "when default attributes are specified" do + let(:default_attributes) do + { + "shared" => { + "foo" => "bar", + }, + } + end + + context "when the included policy does not have attributes that conflict with the including policy" do + let(:included_policy_default_attributes) do + { + "not_shared" => { + "foo" => "bar", + }, + } + end + + it "emits a lockfile with the attributes from both merged" do + expect(policyfile_lock.to_lock["default_attributes"]).to include(included_policy_default_attributes, default_attributes) + end + + end + + context "when the included policy has attributes that conflict with the including policy, but provide the same value" do + let(:included_policy_default_attributes) { default_attributes } + + it "emits a lockfile with the attributes from both merged" do + expect(policyfile_lock.to_lock["default_attributes"]).to eq(default_attributes) + end + + end + + context "when the included policy has attributes that conflict with the including policy's attributes" do + let(:included_policy_default_attributes) do + { + "shared" => { + "foo" => "not_bar", + }, + } + end + + it "raises an error describing all attribute conflicts" do + expect { policyfile_lock.to_lock }.to raise_error( + ChefDK::Policyfile::AttributeMergeChecker::ConflictError, + "Attribute '[shared][foo]' provided conflicting values by the following sources [\"user-specified\", \"included\"]") + end + end + end + + context "when override attributes are specified" do + let(:override_attributes) do + { + "shared" => { + "foo" => "bar", + }, + } + end + + context "when the included policy does not have attributes that conflict with the including policy" do + let(:included_policy_override_attributes) do + { + "not_shared" => { + "foo" => "bar", + }, + } + end + + it "emits a lockfile with the attributes from both merged" do + expect(policyfile_lock.to_lock["override_attributes"]).to include(included_policy_override_attributes, override_attributes) + end + + end + + context "when the included policy has attributes that conflict with the including policy, but provide the same value" do + let(:included_policy_override_attributes) { override_attributes } + + it "emits a lockfile with the attributes from both merged" do + expect(policyfile_lock.to_lock["override_attributes"]).to eq(override_attributes) + end + + end + + context "when the included policy has attributes that conflict with the including policy's attributes" do + let(:included_policy_override_attributes) do + { + "shared" => { + "foo" => "not_bar", + }, + } + end + + it "raises an error describing all attribute conflicts" do + expect { policyfile_lock.to_lock }.to raise_error( + ChefDK::Policyfile::AttributeMergeChecker::ConflictError, + "Attribute '[shared][foo]' provided conflicting values by the following sources [\"user-specified\", \"included\"]") + end + end + end + end + + context "when several policies are included" do + let(:included_policy_2_default_attributes) { {} } + let(:included_policy_2_override_attributes) { {} } + let(:included_policy_2_expanded_named_runlist) { nil } + let(:included_policy_2_expanded_runlist) { ["recipe[cookbookA::default]"] } + let(:included_policy_2_cookbooks) do + [ + { + name: "cookbookA", + version: "2.0.0", + }, + ] + end + + let(:included_policy_2_lock_data) do + cookbook_locks = included_policy_2_cookbooks.inject({}) do |acc, cookbook_info| + acc[cookbook_info[:name]] = { + "version" => cookbook_info[:version], + "identifier" => "identifier", + "dotted_decimal_identifier" => "dotted_decimal_identifier", + "cache_key" => "#{cookbook_info[:name]}-#{cookbook_info[:version]}", + "origin" => "uri", + "source_options" => {}, + } + acc + end + + solution_dependencies_lock = included_policy_2_cookbooks.map do |cookbook_info| + [cookbook_info[:name], cookbook_info[:version]] + end + + solution_dependencies_cookbooks = included_policy_2_cookbooks.inject({}) do |acc, cookbook_info| + acc["#{cookbook_info[:name]} (#{cookbook_info[:version]})"] = external_cookbook_universe[cookbook_info[:name]][cookbook_info[:version]] + acc + end + + { + "name" => "included_policy_2file", + "revision_id" => "myrevisionid", + "run_list" => included_policy_2_expanded_runlist, + "cookbook_locks" => cookbook_locks, + "default_attributes" => included_policy_2_default_attributes, + "override_attributes" => included_policy_2_override_attributes, + "solution_dependencies" => { + "Policyfile" => solution_dependencies_lock, + "dependencies" => solution_dependencies_cookbooks, + }, + }.tap do |core| + core["named_run_lists"] = included_policy_2_expanded_named_runlist if included_policy_2_expanded_named_runlist + end + end + + let(:included_policy_2_lock_name) { "included2" } + let(:included_policy_2_fetcher) do + instance_double("ChefDK::Policyfile::LocalLockFetcher").tap do |double| + allow(double).to receive(:lock_data).and_return(included_policy_2_lock_data) + allow(double).to receive(:valid?).and_return(true) + allow(double).to receive(:errors).and_return([]) + end + end + + let(:included_policy_2_lock_spec) do + ChefDK::Policyfile::PolicyfileLocationSpecification.new(included_policy_2_lock_name, lock_source_options, nil).tap do |spec| + allow(spec).to receive(:valid?).and_return(true) + allow(spec).to receive(:fetcher).and_return(included_policy_2_fetcher) + allow(spec).to receive(:source_options_for_lock).and_return(lock_source_options) + end + end + + let(:included_policies) { [included_policy_lock_spec, included_policy_2_lock_spec] } + + let(:run_list) { ["local::default"] } + + context "when no cookbooks are shared as dependencies or transitive dependencies by included policies" do + let(:included_policy_expanded_runlist) { ["recipe[cookbookA::default]"] } + let(:included_policy_cookbooks) do + [ + { + name: "cookbookA", + version: "2.0.0", + }, + ] + end + + let(:included_policy_2_expanded_runlist) { ["recipe[cookbookB::default]"] } + let(:included_policy_2_cookbooks) do + [ + { + name: "cookbookB", + version: "2.0.0", + }, + ] + end + + it "does not raise a have conflicting dependency requirements error" do + expect { policyfile_lock.to_lock }.not_to raise_error + end + + it "emits a lockfile with the correct dependencies" do + expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to eq({ + "cookbookA (2.0.0)" => [], + "cookbookB (2.0.0)" => [], + "cookbookC (1.0.0)" => [], + "local (1.0.0)" => [["cookbookC", "= 1.0.0"]], + }) + end + end + + context "when some cookbooks appear as dependencies or transitive dependencies of some included policies" do + let(:included_policy_expanded_runlist) { ["recipe[cookbookC::default]"] } + let(:included_policy_2_expanded_runlist) { ["recipe[cookbookC::default]"] } + + context "and the locked versions of the cookbooks match" do + let(:included_policy_cookbooks) do + [ + { + name: "cookbookC", + version: "1.0.0", + }, + ] + end + + let(:included_policy_2_cookbooks) do + [ + { + name: "cookbookC", + version: "1.0.0", + }, + ] + end + + it "solves the dependencies with the matching versions" do + expect(policyfile_lock.to_lock["solution_dependencies"]["dependencies"]).to eq({ + "cookbookC (1.0.0)" => [], + "local (1.0.0)" => [["cookbookC", "= 1.0.0"]], + }) + end + end + + context "and the locked versions of the cookbooks do not match" do + let(:included_policy_cookbooks) do + [ + { + name: "cookbookC", + version: "1.0.0", + }, + ] + end + + let(:included_policy_2_cookbooks) do + [ + { + name: "cookbookC", + version: "2.0.0", + }, + ] + end + + it "raises an error describing the conflict" do + expect { policyfile_lock }.to raise_error( + ChefDK::Policyfile::IncludedPoliciesCookbookSource::ConflictingCookbookVersions, + /Multiple versions provided for cookbook cookbookC/ + ) + end + end + end + + context "when default attributes are specified" do + context "when the included policies do not have conflicting attributes" do + let(:included_policy_default_attributes) do + { + "not_conflict" => { + "foo" => "bar", + }, + } + end + let(:included_policy_2_default_attributes) do + { + "not_conflict" => { + "foo" => "bar", + }, + } + end + let(:default_attributes) do + { + "not_conflict" => { + "bar" => "baz", + }, + } + end + + it "emits a lockfile with the included policies' attributes merged" do + expect(policyfile_lock.to_lock["default_attributes"]).to eq({ + "not_conflict" => { + "foo" => "bar", + "bar" => "baz", + }, + }) + end + end + + context "when the included policies have conflicting attributes" do + let(:included_policy_default_attributes) do + { + "conflict" => { + "foo" => "bar", + }, + } + end + + let(:included_policy_2_default_attributes) do + { + "conflict" => { + "foo" => "baz", + }, + } + end + + it "raises an error describing the conflict" do + expect { policyfile_lock }.to raise_error( + ChefDK::Policyfile::AttributeMergeChecker::ConflictError, + "Attribute '[conflict][foo]' provided conflicting values by the following sources [\"included\", \"included2\"]") + end + end + end + + context "when override attributes are specified" do + context "when the included policies do not have conflicting attributes" do + let(:included_policy_override_attributes) do + { + "not_conflict" => { + "foo" => "bar", + }, + } + end + let(:included_policy_2_override_attributes) do + { + "not_conflict" => { + "foo" => "bar", + }, + } + end + let(:override_attributes) do + { + "not_conflict" => { + "bar" => "baz", + }, + } + end + + it "emits a lockfile with the included policies' attributes merged" do + expect(policyfile_lock.to_lock["override_attributes"]).to eq({ + "not_conflict" => { + "foo" => "bar", + "bar" => "baz", + }, + }) + end + end + + context "when the included policies have conflicting attributes" do + let(:included_policy_override_attributes) do + { + "conflict" => { + "foo" => "bar", + }, + } + end + + let(:included_policy_2_override_attributes) do + { + "conflict" => { + "foo" => "baz", + }, + } + end + + it "raises an error describing the conflict" do + expect { policyfile_lock }.to raise_error( + ChefDK::Policyfile::AttributeMergeChecker::ConflictError, + "Attribute '[conflict][foo]' provided conflicting values by the following sources [\"included\", \"included2\"]") + end + end + end + + end +end