# # Copyright:: Copyright (c) 2014-2020 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 "shared/custom_generator_cookbook" require "shared/setup_git_committer_config" require "chef-cli/command/generator_commands/cookbook" describe ChefCLI::Command::GeneratorCommands::Cookbook do include_context("setup_git_committer_config") let(:argv) { %w{new_cookbook} } let(:stdout_io) { StringIO.new } let(:stderr_io) { StringIO.new } let(:expected_cookbook_file_relpaths) do %w{ .gitignore kitchen.yml test test/integration test/integration/default/default_test.rb Policyfile.rb chefignore LICENSE metadata.rb README.md CHANGELOG.md recipes recipes/default.rb } end let(:expected_cookbook_file_relpaths_specs) do %w{ .gitignore kitchen.yml test test/integration test/integration/default/default_test.rb Policyfile.rb chefignore LICENSE metadata.rb README.md CHANGELOG.md recipes recipes/default.rb spec spec/spec_helper.rb spec/unit spec/unit/recipes spec/unit/recipes/default_spec.rb } end let(:expected_cookbook_files) do expected_cookbook_file_relpaths.map do |relpath| File.join(tempdir, "new_cookbook", relpath) end end let(:expected_cookbook_files_specs) do expected_cookbook_file_relpaths_specs.map do |relpath| File.join(tempdir, "new_cookbook", relpath) end end let(:non_delivery_breadcrumb) do <<~EOF Your cookbook is ready. Type `cd new_cookbook` to enter it. There are several commands you can run to get started locally developing and testing your cookbook. Type `delivery local --help` to see a full list of local testing commands. Why not start by writing an InSpec test? Tests for the default recipe are stored at: test/integration/default/default_test.rb If you'd prefer to dive right in, the default recipe can be found at: recipes/default.rb EOF end subject(:cookbook_generator) do g = described_class.new(argv) allow(g).to receive(:cookbook_path_in_git_repo?).and_return(false) allow(g).to receive(:stdout).and_return(stdout_io) g end def generator_context ChefCLI::Generator.context end before do ChefCLI::Generator.reset end include_examples "custom generator cookbook" do let(:generator_arg) { "new_cookbook" } let(:generator_name) { "cookbook" } end it "configures the chef runner" do expect(cookbook_generator.chef_runner).to be_a(ChefCLI::ChefRunner) expect(cookbook_generator.chef_runner.cookbook_path).to eq(File.expand_path("lib/chef-cli/skeletons", project_root)) end context "when given invalid/incomplete arguments" do let(:expected_help_message) do "Usage: chef generate cookbook NAME [options]\n" end def with_argv(argv) generator = described_class.new(argv) allow(generator).to receive(:stdout).and_return(stdout_io) allow(generator).to receive(:stderr).and_return(stderr_io) generator end it "prints usage when args are empty" do with_argv([]).run expect(stderr_io.string).to include(expected_help_message) end it "errors if both berks and policyfiles are requested" do expect(with_argv(%w{my_cookbook --berks --policy}).run).to eq(1) message = "Berkshelf and Policyfiles are mutually exclusive. Please specify only one." expect(stderr_io.string).to include(message) end it "warns if a hyphenated cookbook name is passed" do expect(with_argv(%w{my-cookbook}).run).to eq(0) message = "Hyphens are discouraged in cookbook names as they may cause problems with custom resources. See https://docs.chef.io/workstation/ctl_chef/#chef-generate-cookbook for more information." expect(stdout_io.string).to include(message) end end context "when given the name of the cookbook to generate" do let(:argv) { %w{new_cookbook} } before do reset_tempdir end it "configures the generator context" do cookbook_generator.read_and_validate_params cookbook_generator.setup_context expect(generator_context.cookbook_root).to eq(Dir.pwd) expect(generator_context.cookbook_name).to eq("new_cookbook") expect(generator_context.recipe_name).to eq("default") expect(generator_context.verbose).to be(false) expect(generator_context.specs).to be(false) end it "creates a new cookbook" do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end generated_files = Dir.glob("#{tempdir}/new_cookbook/**/*", File::FNM_DOTMATCH) expected_cookbook_files.each do |expected_file| expect(generated_files).to include(expected_file) end end context "when given the specs flag" do let(:argv) { %w{ new_cookbook --specs } } it "configures the generator context with specs mode enabled" do cookbook_generator.read_and_validate_params cookbook_generator.setup_context expect(generator_context.specs).to be(true) end it "creates a new cookbook" do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end generated_files = Dir.glob("#{tempdir}/new_cookbook/**/*", File::FNM_DOTMATCH) expected_cookbook_files_specs.each do |expected_file| expect(generated_files).to include(expected_file) end end end context "when given the verbose flag" do let(:argv) { %w{ new_cookbook --verbose } } it "configures the generator context with verbose mode enabled" do cookbook_generator.read_and_validate_params cookbook_generator.setup_context expect(generator_context.verbose).to be(true) end it "emits verbose output" do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end # The normal chef formatter puts a heading for each recipe like this. # Full output is large and subject to change with minor changes in the # generator cookbook, so we just look for this line expected_line = "Recipe: code_generator::cookbook" actual = stdout_io.string expect(actual).to include(expected_line) end end shared_examples_for "a generated file" do |context_var| before do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end end it "should contain #{context_var} from the generator context" do expect(File.read(file)).to match line end end describe "README.md" do let(:file) { File.join(tempdir, "new_cookbook", "README.md") } include_examples "a generated file", :cookbook_name do let(:line) { "# new_cookbook" } end end describe "CHANGELOG.md" do let(:file) { File.join(tempdir, "new_cookbook", "CHANGELOG.md") } include_examples "a generated file", :cookbook_name do let(:line) { "# new_cookbook" } end end # This shared example group requires a let binding for # `expected_kitchen_yml_content` shared_examples_for "kitchen_yml_and_integration_tests" do describe "Generating Test Kitchen and integration testing files" do describe "generating kitchen config" do before do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end end let(:file) { File.join(tempdir, "new_cookbook", "kitchen.yml") } it "creates a kitchen.yml with the expected content" do expect(IO.read(file)).to eq(expected_kitchen_yml_content) end end describe "test/integration/default/default_test.rb" do let(:file) { File.join(tempdir, "new_cookbook", "test", "integration", "default", "default_test.rb") } include_examples "a generated file", :cookbook_name do let(:line) { "describe port" } end end end end # This shared example group requires you to define a let binding for # `expected_chefspec_spec_helper_content` shared_examples_for "chefspec_spec_helper_file" do describe "Generating ChefSpec files" do before do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end end let(:file) { File.join(tempdir, "new_cookbook", "spec", "spec_helper.rb") } it "creates a spec/spec_helper.rb for ChefSpec with the expected content" do expect(IO.read(file)).to eq(expected_chefspec_spec_helper_content) end end end context "when configured for Policyfiles" do let(:argv) { %w{new_cookbook --policy} } describe "Policyfile.rb" do let(:file) { File.join(tempdir, "new_cookbook", "Policyfile.rb") } let(:expected_content) do <<~POLICYFILE_RB # Policyfile.rb - Describe how you want Chef Infra Client to build your system. # # For more information on the Policyfile feature, visit # https://docs.chef.io/policyfile/ # A name that describes what the system you're building with Chef does. name 'new_cookbook' # Where to find external cookbooks: default_source :supermarket # run_list: chef-client will run these recipes in the order specified. run_list 'new_cookbook::default' # Specify a custom source for a single cookbook: cookbook 'new_cookbook', path: '.' POLICYFILE_RB end before do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end end it "has a run_list and cookbook path that will work out of the box" do expect(IO.read(file)).to eq(expected_content) end end include_examples "kitchen_yml_and_integration_tests" do let(:expected_kitchen_yml_content) do <<~KITCHEN_YML --- driver: name: vagrant ## The forwarded_port port feature lets you connect to ports on the VM guest ## via localhost on the host. ## see also: https://www.vagrantup.com/docs/networking/forwarded_ports # network: # - ["forwarded_port", {guest: 80, host: 8080}] provisioner: name: chef_zero ## product_name and product_version specifies a specific Chef product and version to install. ## see the Chef documentation for more details: https://docs.chef.io/workstation/config_yml_kitchen/ # product_name: chef # product_version: 17 verifier: name: inspec platforms: - name: ubuntu-20.04 - name: centos-8 suites: - name: default verifier: inspec_tests: - test/integration/default KITCHEN_YML end end include_examples "chefspec_spec_helper_file" do let(:argv) { %w{ new_cookbook --policy --specs } } let(:expected_chefspec_spec_helper_content) do <<~SPEC_HELPER require 'chefspec' require 'chefspec/policyfile' SPEC_HELPER end end end context "when YAML recipe flag is passed" do let(:argv) { %w{new_cookbook --yaml} } describe "recipes/default.yml" do let(:file) { File.join(tempdir, "new_cookbook", "recipes", "default.yml") } let(:expected_content_header) do <<~DEFAULT_YML_HEADER # # Cookbook:: new_cookbook # Recipe:: default # DEFAULT_YML_HEADER end let(:expected_content) do <<~DEFAULT_YML_CONTENT --- resources: # Example Syntax # Additional snippets are available using the Chef Infra Extension for Visual Studio Code # - type: file # name: '/path/to/file' # content: 'content' # owner: 'root' # group: 'root' # mode: '0755' # action: # - create DEFAULT_YML_CONTENT end before do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end end it "has a default.yml file with template contents" do expect(IO.read(file)).to match(expected_content_header) expect(IO.read(file)).to match(expected_content) end end end context "when configured for Berkshelf" do let(:argv) { %w{new_cookbook --berks} } describe "Berksfile" do let(:file) { File.join(tempdir, "new_cookbook", "Berksfile") } let(:expected_content) do <<~POLICYFILE_RB source 'https://supermarket.chef.io' metadata POLICYFILE_RB end before do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end end it "pulls deps from metadata" do expect(IO.read(file)).to eq(expected_content) end end include_examples "kitchen_yml_and_integration_tests" do let(:expected_kitchen_yml_content) do <<~KITCHEN_YML --- driver: name: vagrant ## The forwarded_port port feature lets you connect to ports on the VM guest via ## localhost on the host. ## see also: https://www.vagrantup.com/docs/networking/forwarded_ports # network: # - ["forwarded_port", {guest: 80, host: 8080}] provisioner: name: chef_zero # You may wish to disable always updating cookbooks in CI or other testing environments. # For example: # always_update_cookbooks: <%= !ENV['CI'] %> always_update_cookbooks: true ## product_name and product_version specifies a specific Chef product and version to install. ## see the Chef documentation for more details: https://docs.chef.io/workstation/config_yml_kitchen/ # product_name: chef # product_version: 17 verifier: name: inspec platforms: - name: ubuntu-20.04 - name: centos-8 suites: - name: default run_list: - recipe[new_cookbook::default] verifier: inspec_tests: - test/integration/default attributes: KITCHEN_YML end end include_examples "chefspec_spec_helper_file" do let(:argv) { %w{ new_cookbook --berks --specs } } let(:expected_chefspec_spec_helper_content) do <<~SPEC_HELPER require 'chefspec' require 'chefspec/berkshelf' SPEC_HELPER end end end describe "metadata.rb" do let(:file) { File.join(tempdir, "new_cookbook", "metadata.rb") } include_examples "a generated file", :cookbook_name do let(:line) { /name\s+'new_cookbook'.+# issues_url.+# source_url/m } end end describe "recipes/default.rb" do let(:file) { File.join(tempdir, "new_cookbook", "recipes", "default.rb") } include_examples "a generated file", :cookbook_name do let(:line) { "# Cookbook:: new_cookbook" } end end describe "spec/unit/recipes/default_spec.rb" do let(:argv) { %w{ new_cookbook --specs } } let(:file) { File.join(tempdir, "new_cookbook", "spec", "unit", "recipes", "default_spec.rb") } include_examples "a generated file", :cookbook_name do let(:line) { "describe 'new_cookbook::default' do" } end end end context "when given the path to the cookbook to generate" do let(:argv) { [ File.join(tempdir, "a_new_cookbook") ] } before do reset_tempdir end it "configures the generator context" do cookbook_generator.read_and_validate_params cookbook_generator.setup_context expect(generator_context.cookbook_root).to eq(tempdir) expect(generator_context.cookbook_name).to eq("a_new_cookbook") end end context "when given generic arguments to populate the generator context" do let(:argv) { [ "new_cookbook", "--generator-arg", "key1=value1", "-a", "key2=value2", "-a", " key3 = value3 " ] } before do reset_tempdir end it "configures the generator context for long form option key1" do cookbook_generator.read_and_validate_params cookbook_generator.setup_context expect(generator_context.key1).to eq("value1") end it "configures the generator context for short form option key2" do cookbook_generator.read_and_validate_params cookbook_generator.setup_context expect(generator_context.key2).to eq("value2") end it "configures the generator context for key3 containing additional spaces" do cookbook_generator.read_and_validate_params cookbook_generator.setup_context expect(generator_context.key3).to eq("value3") end end end