require 'spec_helper'

describe Puppet::Type.type(:exec).provider(:posix), :if => Puppet.features.posix? do
  include PuppetSpec::Files

  def make_exe
    cmdpath = tmpdir('cmdpath')
    exepath = tmpfile('my_command', cmdpath)
    FileUtils.touch(exepath)
    File.chmod(0755, exepath)
    exepath
  end

  let(:resource) { Puppet::Type.type(:exec).new(:title => '/foo', :provider => :posix) }
  let(:provider) { described_class.new(resource) }

  describe "#validatecmd" do
    it "should fail if no path is specified and the command is not fully qualified" do
      expect { provider.validatecmd("foo") }.to raise_error(
        Puppet::Error,
        "'foo' is not qualified and no path was specified. Please qualify the command or specify a path."
      )
    end

    it "should pass if a path is given" do
      provider.resource[:path] = ['/bogus/bin']
      provider.validatecmd("../foo")
    end

    it "should pass if command is fully qualifed" do
      provider.resource[:path] = ['/bogus/bin']
      provider.validatecmd("/bin/blah/foo")
    end
  end

  describe "#run" do
    describe "when the command is an absolute path" do
      let(:command) { tmpfile('foo') }

      it "should fail if the command doesn't exist" do
        expect { provider.run(command) }.to raise_error(ArgumentError, "Could not find command '#{command}'")
      end

      it "should fail if the command isn't a file" do
        FileUtils.mkdir(command)
        FileUtils.chmod(0755, command)

        expect { provider.run(command) }.to raise_error(ArgumentError, "'#{command}' is a directory, not a file")
      end

      it "should fail if the command isn't executable" do
        FileUtils.touch(command)
        allow(File).to receive(:executable?).with(command).and_return(false)

        expect { provider.run(command) }.to raise_error(ArgumentError, "'#{command}' is not executable")
      end
    end

    describe "when the command is a relative path" do
      it "should execute the command if it finds it in the path and is executable" do
        command = make_exe
        provider.resource[:path] = [File.dirname(command)]
        filename = File.basename(command)

        expect(Puppet::Util::Execution).to receive(:execute).with(filename, instance_of(Hash)).and_return(Puppet::Util::Execution::ProcessOutput.new('', 0))

        provider.run(filename)
      end

      it "should fail if the command isn't in the path" do
        resource[:path] = ["/fake/path"]

        expect { provider.run('foo') }.to raise_error(ArgumentError, "Could not find command 'foo'")
      end

      it "should fail if the command is in the path but not executable" do
        command = make_exe
        File.chmod(0644, command)
        allow(FileTest).to receive(:executable?).with(command).and_return(false)
        resource[:path] = [File.dirname(command)]
        filename = File.basename(command)

        expect { provider.run(filename) }.to raise_error(ArgumentError, "Could not find command '#{filename}'")
      end
    end

    it "should not be able to execute shell builtins" do
      provider.resource[:path] = ['/bogus/bin']
      expect { provider.run("cd ..") }.to raise_error(ArgumentError, "Could not find command 'cd'")
    end

    it "should execute the command if the command given includes arguments or subcommands" do
      provider.resource[:path] = ['/bogus/bin']
      command = make_exe

      expect(Puppet::Util::Execution).to receive(:execute).with("#{command} bar --sillyarg=true --blah", instance_of(Hash)).and_return(Puppet::Util::Execution::ProcessOutput.new('', 0))

      provider.run("#{command} bar --sillyarg=true --blah")
    end

    it "should fail if quoted command doesn't exist" do
      provider.resource[:path] = ['/bogus/bin']
      command = "/foo bar --sillyarg=true --blah"

      expect { provider.run(%Q["#{command}"]) }.to raise_error(ArgumentError, "Could not find command '#{command}'")
    end

    it "should warn if you're overriding something in environment" do
      provider.resource[:environment] = ['WHATEVER=/something/else', 'WHATEVER=/foo']
      command = make_exe

      expect(Puppet::Util::Execution).to receive(:execute).with(command, instance_of(Hash)).and_return(Puppet::Util::Execution::ProcessOutput.new('', 0))

      provider.run(command)

      expect(@logs.map {|l| "#{l.level}: #{l.message}" }).to eq(["warning: Overriding environment setting 'WHATEVER' with '/foo'"])
    end

    it "should warn when setting an empty environment variable" do
      provider.resource[:environment] = ['WHATEVER=']
      command = make_exe

      expect(Puppet::Util::Execution).to receive(:execute).with(command, instance_of(Hash)).and_return(Puppet::Util::Execution::ProcessOutput.new('', 0))

      provider.run(command)

      expect(@logs.map(&:to_s).join).to match(/Empty environment setting 'WHATEVER'\n\s+\(file & line not available\)/m)
    end

    it "should warn when setting an empty environment variable (within a manifest)" do
      provider.resource[:environment] = ['WHATEVER=']
      provider.resource.file = '/tmp/foobar'
      provider.resource.line = 42
      command = make_exe

      expect(Puppet::Util::Execution).to receive(:execute).with(command, instance_of(Hash)).and_return(Puppet::Util::Execution::ProcessOutput.new('', 0))

      provider.run(command)

      expect(@logs.map(&:to_s).join).to match(/Empty environment setting 'WHATEVER'\n\s+\(file: \/tmp\/foobar, line: 42\)/m)
    end

    it "should set umask before execution if umask parameter is in use" do
      provider.resource[:umask] = '0027'
      expect(Puppet::Util).to receive(:withumask).with(0027)
      provider.run(provider.resource[:command])
    end

    describe "posix locale settings", :unless => RUBY_PLATFORM == 'java' do
      # a sentinel value that we can use to emulate what locale environment variables might be set to on an international
      # system.
      lang_sentinel_value = "en_US.UTF-8"
      # a temporary hash that contains sentinel values for each of the locale environment variables that we override in
      # "exec"
      locale_sentinel_env = {}
      Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |var| locale_sentinel_env[var] = lang_sentinel_value }

      command = "/bin/echo $%s"

      it "should not override user's locale during execution" do
        # we'll do this once without any sentinel values, to give us a little more test coverage
        orig_env = {}
        Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |var| orig_env[var] = ENV[var] if ENV[var] }

        orig_env.keys.each do |var|
          output, _ = provider.run(command % var)
          expect(output.strip).to eq(orig_env[var])
        end

        # now, once more... but with our sentinel values
        Puppet::Util.withenv(locale_sentinel_env) do
          Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var|
            output, _ = provider.run(command % var)
            expect(output.strip).to eq(locale_sentinel_env[var])
          end
        end
      end

      it "should respect locale overrides in user's 'environment' configuration" do
        provider.resource[:environment] = ['LANG=C', 'LC_ALL=C']
        output, _ = provider.run(command % 'LANG')
        expect(output.strip).to eq('C')
        output, _ = provider.run(command % 'LC_ALL')
        expect(output.strip).to eq('C')
      end
    end

    describe "posix user-related environment vars", :unless => RUBY_PLATFORM == 'java' do
      # a temporary hash that contains sentinel values for each of the user-related environment variables that we
      # are expected to unset during an "exec"
      user_sentinel_env = {}
      Puppet::Util::POSIX::USER_ENV_VARS.each { |var| user_sentinel_env[var] = "Abracadabra" }

      command = "/bin/echo $%s"

      it "should unset user-related environment vars during execution" do
        # first we set up a temporary execution environment with sentinel values for the user-related environment vars
        # that we care about.
        Puppet::Util.withenv(user_sentinel_env) do
          # with this environment, we loop over the vars in question
          Puppet::Util::POSIX::USER_ENV_VARS.each do |var|
            # ensure that our temporary environment is set up as we expect
            expect(ENV[var]).to eq(user_sentinel_env[var])

            # run an "exec" via the provider and ensure that it unsets the vars
            output, _ = provider.run(command % var)
            expect(output.strip).to eq("")

            # ensure that after the exec, our temporary env is still intact
            expect(ENV[var]).to eq(user_sentinel_env[var])
          end

        end
      end

      it "should respect overrides to user-related environment vars in caller's 'environment' configuration" do
        sentinel_value = "Abracadabra"
        # set the "environment" property of the resource, populating it with a hash containing sentinel values for
        # each of the user-related posix environment variables
        provider.resource[:environment] = Puppet::Util::POSIX::USER_ENV_VARS.collect { |var| "#{var}=#{sentinel_value}"}

        # loop over the posix user-related environment variables
        Puppet::Util::POSIX::USER_ENV_VARS.each do |var|
          # run an 'exec' to get the value of each variable
          output, _ = provider.run(command % var)
          # ensure that it matches our expected sentinel value
          expect(output.strip).to eq(sentinel_value)
        end
      end
    end
  end
end
