# Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/provisioners/shell/provisioner") describe "Vagrant::Shell::Provisioner" do include_context "unit" let(:default_win_path) { "C:/tmp/vagrant-shell" } let(:env){ isolated_environment } let(:machine) { double(:machine, env: env, id: "ID").tap { |machine| allow(machine).to receive_message_chain(:config, :vm, :communicator).and_return(:not_winrm) allow(machine).to receive_message_chain(:config, :vm, :guest).and_return(:linux) allow(machine).to receive_message_chain(:communicate, :tap) {} } } before do allow(env).to receive(:tmp_path).and_return(Pathname.new("/dev/null")) end context "when reset is enabled" do let(:path) { nil } let(:inline) { "" } let(:communicator) { double("communicator") } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => false, :path => path, :inline => inline, :binary => false, :reset => true, :reboot => false, ) } let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } before { allow(machine).to receive(:communicate).and_return(communicator) allow(vsp).to receive(:provision_ssh) } it "should provision and then reset the connection" do expect(vsp).to receive(:provision_ssh) expect(communicator).to receive(:reset!) vsp.provision end context "when path and inline are not set" do let(:path) { nil } let(:inline) { nil } it "should reset the connection and not provision" do expect(vsp).not_to receive(:provision_ssh) expect(communicator).to receive(:reset!) vsp.provision end end end context "when reboot is enabled" do let(:path) { nil } let(:inline) { "" } let(:communicator) { double("communicator") } let(:guest) { double("guest") } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => false, :path => path, :inline => inline, :binary => false, :reset => false, :reboot => true ) } let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } before { allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:guest).and_return(guest) allow(vsp).to receive(:provision_ssh) } it "should provision and then reboot the guest" do expect(vsp).to receive(:provision_ssh) expect(guest).to receive(:capability).with(:reboot) vsp.provision end context "when path and inline are not set" do let(:path) { nil } let(:inline) { nil } it "should reboot the guest and not provision" do expect(vsp).not_to receive(:provision_ssh) expect(guest).to receive(:capability).with(:reboot) vsp.provision end end end context "with a script that contains invalid us-ascii byte sequences" do let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => false, :path => nil, :inline => script_that_is_incorrectly_us_ascii_encoded, :binary => false, :reset => false, :reboot => false ) } let(:script_that_is_incorrectly_us_ascii_encoded) { [207].pack("c*").force_encoding("US-ASCII") } it "does not raise an exception when normalizing newlines" do vsp = VagrantPlugins::Shell::Provisioner.new(machine, config) expect { vsp.provision }.not_to raise_error end end context "with a script that was set to freeze the string" do TEST_CONSTANT_VARIABLE = <<-TEST_CONSTANT_VARIABLE.freeze echo test TEST_CONSTANT_VARIABLE let(:script) { TEST_CONSTANT_VARIABLE } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => false, :path => nil, :inline => script, :binary => false, :reset => false, :reboot => false ) } it "does not raise an exception" do vsp = VagrantPlugins::Shell::Provisioner.new(machine, config) RSpec::Expectations.configuration.on_potential_false_positives = :nothing # This test should be fine, since we are specifically looking for the # string 'freeze' when RuntimeError is raised expect { vsp.provision }.not_to raise_error(RuntimeError) end end context "with remote script" do let(:filechecksum) { double("filechecksum", checksum: checksum_value) } let(:checksum_value) { double("checksum_value") } before do allow(FileChecksum).to receive(:new).and_return(filechecksum) allow_any_instance_of(Vagrant::Util::Downloader).to receive(:execute_curl).and_return(true) end context "that does not have matching sha1 checksum" do let(:checksum_value) { "INVALID_VALUE" } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => true, :path => "http://example.com/script.sh", :binary => false, :md5 => nil, :sha1 => 'EXPECTED_VALUE', :sha256 => nil, :sha384 => nil, :sha512 => nil, :reset => false, :reboot => false ) } it "should raise an exception" do vsp = VagrantPlugins::Shell::Provisioner.new(machine, config) expect{ vsp.provision }.to raise_error(Vagrant::Errors::DownloaderChecksumError) end end context "that does not have matching sha256 checksum" do let(:checksum_value) { "INVALID_VALUE" } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => true, :path => "http://example.com/script.sh", :binary => false, :md5 => nil, :sha1 => nil, :sha256 => 'EXPECTED_VALUE', :sha384 => nil, :sha512 => nil, :reset => false, :reboot => false ) } it "should raise an exception" do vsp = VagrantPlugins::Shell::Provisioner.new(machine, config) expect{ vsp.provision }.to raise_error(Vagrant::Errors::DownloaderChecksumError) end end context "that does not have matching sha384 checksum" do let(:checksum_value) { "INVALID_VALUE" } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => true, :path => "http://example.com/script.sh", :binary => false, :md5 => nil, :sha1 => nil, :sha256 => nil, :sha384 => 'EXPECTED_VALUE', :sha512 => nil, :reset => false, :reboot => false ) } it "should raise an exception" do vsp = VagrantPlugins::Shell::Provisioner.new(machine, config) expect{ vsp.provision }.to raise_error(Vagrant::Errors::DownloaderChecksumError) end end context "that does not have matching sha512 checksum" do let(:checksum_value) { "INVALID_VALUE" } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => true, :path => "http://example.com/script.sh", :binary => false, :md5 => nil, :sha1 => nil, :sha256 => nil, :sha384 => nil, :sha512 => 'EXPECTED_VALUE', :reset => false, :reboot => false ) } it "should raise an exception" do vsp = VagrantPlugins::Shell::Provisioner.new(machine, config) expect{ vsp.provision }.to raise_error(Vagrant::Errors::DownloaderChecksumError) end end context "that does not have matching md5 checksum" do let(:checksum_value) { "INVALID_VALUE" } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => true, :path => "http://example.com/script.sh", :binary => false, :md5 => 'EXPECTED_VALUE', :sha1 => nil, :sha256 => nil, :sha384 => nil, :sha512 => nil, :reset => false, :reboot => false ) } it "should raise an exception" do vsp = VagrantPlugins::Shell::Provisioner.new(machine, config) expect{ vsp.provision }.to raise_error(Vagrant::Errors::DownloaderChecksumError) end end end describe "#upload_path" do context "when upload path is not set" do let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => nil, :remote? => false, :path => "doesn't matter", :inline => "doesn't matter", :binary => false, :reset => true, :reboot => false, ) } it "should default to /tmp/vagrant-shell" do expect(vsp.upload_path).to eq("/tmp/vagrant-shell") end context "windows" do before do allow(machine).to receive_message_chain(:config, :vm, :guest).and_return(:windows) end it "should default to C:/tmp/vagrant-shell" do expect(vsp.upload_path).to eq("C:/tmp/vagrant-shell") end end end context "when upload_path is set" do let(:upload_path) { "arbitrary" } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => upload_path, :remote? => false, :path => "doesn't matter", :inline => "doesn't matter", :binary => false, :reset => true, :reboot => false, ) } let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } it "should use the value from from config" do expect(vsp.upload_path).to eq("arbitrary") end context "windows" do let(:upload_path) { "C:\\Windows\\Temp" } before do allow(machine).to receive_message_chain(:config, :vm, :guest).and_return(:windows) end it "should normalize the slashes" do expect(vsp.upload_path).to eq("C:/Windows/Temp") end end end context "with cached value" do let(:config) { double(:config) } let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } before do vsp.instance_variable_set(:@_upload_path, "anything") end it "should use cached value" do expect(vsp.upload_path).to eq("anything") end end end describe "#provision_winrm" do let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => false, :path => "script/info.ps1", :binary => false, :md5 => nil, :sha1 => 'EXPECTED_VALUE', :sha256 => nil, :sha384 => nil, :sha512 => nil, :reset => false, :reboot => false, :powershell_args => "", :name => nil, :privileged => false, :powershell_elevated_interactive => false, :keep_color => true, ) } let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } let(:communicator) { double("communicator") } let(:guest) { double("guest") } let(:ui) { Vagrant::UI::Silent.new } before { allow(guest).to receive(:capability?).with(:wait_for_reboot).and_return(false) allow(communicator).to receive(:sudo) allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:guest).and_return(guest) allow(machine).to receive(:ui).and_return(ui) allow(vsp).to receive(:with_script_file).and_yield(config.path) allow(communicator).to receive(:upload).with(config.path, /arbitrary.ps1$/) } it "should output all received output" do stdout = ["two lines\n", "from stdout\n"] stderr = ["one line\n", "and partial from stderr"] expect(communicator).to receive(:sudo). and_yield(:stdout, stdout.first). and_yield(:stderr, stderr.first). and_yield(:stderr, stderr.last). and_yield(:stdout, stdout.last) allow(ui).to receive(:detail) expect(ui).to receive(:detail).with("two lines", any_args) expect(ui).to receive(:detail).with("from stdout", any_args) expect(ui).to receive(:detail).with("one line", any_args) expect(ui).to receive(:detail).with("and partial from stderr", any_args) vsp.send(:provision_winrm, "") end it "ensures that files are uploaded with an extension" do allow(vsp).to receive(:with_script_file).and_yield(config.path) expect(communicator).to receive(:upload).with(config.path, /arbitrary.ps1$/) vsp.send(:provision_winrm, "") end context "bat file being uploaded" do before do allow(config).to receive(:path).and_return("script/info.bat") allow(vsp).to receive(:with_script_file).and_yield(config.path) end it "ensures that files are uploaded same extension as provided path.bat" do expect(communicator).to receive(:upload).with(config.path, /arbitrary/) expect(communicator).to receive(:sudo).with(/arbitrary.bat/, anything) vsp.send(:provision_winrm, "") end end context "inline option set" do let(:config) { double( :config, :args => "doesn't matter", :env => {}, :remote? => false, :inline => "some commands", :upload_path => nil, :path => nil, :binary => false, :md5 => nil, :sha1 => 'EXPECTED_VALUE', :sha256 => nil, :sha384 => nil, :sha512 => nil, :reset => false, :reboot => false, :powershell_args => "", :name => nil, :privileged => false, :powershell_elevated_interactive => false ) } it "creates an executable with an extension" do allow(machine).to receive_message_chain(:config, :winssh, :shell).and_return(nil) allow(vsp).to receive(:with_script_file).and_yield(default_win_path) allow(communicator).to receive(:upload).with(default_win_path, /vagrant-shell/) expect(communicator).to receive(:sudo).with(/vagrant-shell.ps1/, anything) vsp.send(:provision_winrm, "") end end end describe "#provision_winssh" do let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => false, :path => nil, :inline => "something", :binary => false, :md5 => nil, :sha1 => 'EXPECTED_VALUE', :sha256 => nil, :sha384 => nil, :sha512 => nil, :reset => false, :reboot => false, :powershell_args => "", :name => nil, :privileged => false, :powershell_elevated_interactive => false, :keep_color => true, ) } let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } let(:communicator) { double("communicator") } let(:guest) { double("guest") } let(:ui) { Vagrant::UI::Silent.new } before { allow(guest).to receive(:capability?).with(:wait_for_reboot).and_return(false) allow(communicator).to receive(:sudo) allow(communicator).to receive(:upload) allow(communicator).to receive_message_chain(:machine_config_ssh, :shell) allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:guest).and_return(guest) allow(machine).to receive(:ui).and_return(ui) allow(machine).to receive(:ssh_info).and_return(true) } context "ps1 file being uploaded" do before do allow(config).to receive(:path).and_return("script/info.ps1") allow(vsp).to receive(:with_script_file).and_yield(config.path) end it "ensures that files are uploaded same extension as provided path.ps1" do allow(machine).to receive_message_chain(:config, :winssh, :shell).and_return("cmd") expect(communicator).to receive(:upload).with(config.path, /arbitrary.ps1/) expect(communicator).to receive(:execute).with(/powershell.*arbitrary.ps1/, anything) vsp.send(:provision_winssh, "") end it "should output all received output" do stdout = ["two lines\n", "from stdout\n"] stderr = ["one line\n", "and partial from stderr"] expect(communicator).to receive(:execute). and_yield(:stdout, stdout.first). and_yield(:stderr, stderr.first). and_yield(:stderr, stderr.last). and_yield(:stdout, stdout.last) allow(ui).to receive(:detail) expect(ui).to receive(:detail).with("two lines", any_args) expect(ui).to receive(:detail).with("from stdout", any_args) expect(ui).to receive(:detail).with("one line", any_args) expect(ui).to receive(:detail).with("and partial from stderr", any_args) vsp.send(:provision_winssh, "") end end context "bat file being uploaded" do before do allow(config).to receive(:path).and_return("script/info.bat") allow(vsp).to receive(:with_script_file).and_yield(config.path) end it "ensures that files are uploaded same extension as provided path.bat" do allow(machine).to receive_message_chain(:config, :winssh, :shell).and_return("cmd") expect(communicator).to receive(:upload).with(config.path, /arbitrary.bat/) expect(communicator).to receive(:execute).with(/cmd.*arbitrary.bat/, anything) vsp.send(:provision_winssh, "") end end context "with inline script" do before do allow(vsp).to receive(:with_script_file).and_yield("/tmp/file/contents") end context "when upload path has a .ps1 extension" do before do allow(config).to receive(:upload_path).and_return("c:/tmp/vagrant-shell.ps1") end it "executes the remote script with powershell" do expect(communicator).to receive(:upload).with(anything, config.upload_path) expect(communicator).to receive(:execute).with(/powershell.*\.ps1/, anything) vsp.send(:provision_winssh, "") end end context "when upload path has a .bat extension" do before do allow(config).to receive(:upload_path).and_return("c:/tmp/vagrant-shell.bat") end it "executes the remote script with cmd" do expect(communicator).to receive(:upload).with(anything, config.upload_path) expect(communicator).to receive(:execute).with(/cmd.*\.bat/, anything) vsp.send(:provision_winssh, "") end end context "when upload path has no extension" do before do allow(config).to receive(:upload_path).and_return("c:/tmp/vagrant-shell") end context "when winssh shell is cmd" do before do allow(machine).to receive_message_chain(:config, :winssh, :shell).and_return("cmd") end it "adds an extension and executes the remote script with cmd" do expect(communicator).to receive(:upload).with(anything, /\.bat$/) expect(communicator).to receive(:execute).with(/cmd.*\.bat/, anything) vsp.send(:provision_winssh, "") end end context "when winssh shell is powershell" do before do allow(machine).to receive_message_chain(:config, :winssh, :shell).and_return("powershell") end it "adds an extension executes the remote script with powershell" do expect(communicator).to receive(:upload).with(anything, /\.ps1$/) expect(communicator).to receive(:execute).with(/powershell.*\.ps1/, anything) vsp.send(:provision_winssh, "") end end end end end describe "#handle_comm" do let(:ui) { Vagrant::UI::Silent.new } let(:keep_color) { false } let(:config) { double( :config, :keep_color => keep_color, ) } let(:env){ isolated_environment } let(:machine) { double(:machine, env: env, id: "ID") } let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } before do allow(machine).to receive(:ui).and_return(ui) end context "when type is stdout" do let(:type) { :stdout } let(:data) { "output data" } it "should output data through the ui" do expect(ui).to receive(:detail).and_call_original vsp.send(:handle_comm, type, data) end it "should color the output" do expect(ui).to receive(:detail).with(data, hash_including(color: :green)). and_call_original vsp.send(:handle_comm, type, data) end context "when configured to keep color" do let(:keep_color) { true } it "should not color the output" do expect(ui).to receive(:detail) do |msg, **opts| expect(msg).to eq(data) expect(opts).to be_empty end vsp.send(:handle_comm, type, data) end end end context "when type is stderr" do let(:type) { :stderr } let(:data) { "output data" } it "should output data through the ui" do expect(ui).to receive(:detail).and_call_original vsp.send(:handle_comm, type, data) end it "should color the output" do expect(ui).to receive(:detail).with(data, hash_including(color: :red)). and_call_original vsp.send(:handle_comm, type, data) end context "when configured to keep color" do let(:keep_color) { true } it "should not color the output" do expect(ui).to receive(:detail) do |msg, **opts| expect(msg).to eq(data) expect(opts).to be_empty end vsp.send(:handle_comm, type, data) end end end context "when type is not stdout or stderr" do let(:type) { :stdnull } let(:data) { "output data" } it "should not output data through the ui" do expect(ui).not_to receive(:detail) vsp.send(:handle_comm, type, data) end end end end