#!/usr/bin/env ruby

require 'spec_helper'

describe Puppet::Util do
  include PuppetSpec::Files

  if Puppet.features.microsoft_windows?
    def set_mode(mode, file)
      Puppet::Util::Windows::Security.set_mode(mode, file)
    end

    def get_mode(file)
      Puppet::Util::Windows::Security.get_mode(file) & 07777
    end
  else
    def set_mode(mode, file)
      File.chmod(mode, file)
    end

    def get_mode(file)
      Puppet::FileSystem.lstat(file).mode & 07777
    end
  end

  describe "#withenv" do
    before :each do
      @original_path = ENV["PATH"]
      @new_env = {:PATH => "/some/bogus/path"}
    end

    it "should change environment variables within the block then reset environment variables to their original values" do
      Puppet::Util.withenv @new_env do
        ENV["PATH"].should == "/some/bogus/path"
      end
      ENV["PATH"].should == @original_path
    end

    it "should reset environment variables to their original values even if the block fails" do
      begin
        Puppet::Util.withenv @new_env do
          ENV["PATH"].should == "/some/bogus/path"
          raise "This is a failure"
        end
      rescue
      end
      ENV["PATH"].should == @original_path
    end

    it "should reset environment variables even when they are set twice" do
      # Setting Path & Environment parameters in Exec type can cause weirdness
      @new_env["PATH"] = "/someother/bogus/path"
      Puppet::Util.withenv @new_env do
        # When assigning duplicate keys, can't guarantee order of evaluation
        ENV["PATH"].should =~ /\/some.*\/bogus\/path/
      end
      ENV["PATH"].should == @original_path
    end

    it "should remove any new environment variables after the block ends" do
      @new_env[:FOO] = "bar"
      ENV["FOO"] = nil
      Puppet::Util.withenv @new_env do
        ENV["FOO"].should == "bar"
      end
      ENV["FOO"].should == nil
    end

  end

  describe "#absolute_path?" do
    describe "on posix systems", :as_platform => :posix do
      it "should default to the platform of the local system" do
        Puppet::Util.should be_absolute_path('/foo')
        Puppet::Util.should_not be_absolute_path('C:/foo')
      end
    end

    describe "on windows", :as_platform => :windows do
      it "should default to the platform of the local system" do
        Puppet::Util.should be_absolute_path('C:/foo')
        Puppet::Util.should_not be_absolute_path('/foo')
      end
    end

    describe "when using platform :posix" do
      %w[/ /foo /foo/../bar //foo //Server/Foo/Bar //?/C:/foo/bar /\Server/Foo /foo//bar/baz].each do |path|
        it "should return true for #{path}" do
          Puppet::Util.should be_absolute_path(path, :posix)
        end
      end

      %w[. ./foo \foo C:/foo \\Server\Foo\Bar \\?\C:\foo\bar \/?/foo\bar \/Server/foo foo//bar/baz].each do |path|
        it "should return false for #{path}" do
          Puppet::Util.should_not be_absolute_path(path, :posix)
        end
      end
    end

    describe "when using platform :windows" do
      %w[C:/foo C:\foo \\\\Server\Foo\Bar \\\\?\C:\foo\bar //Server/Foo/Bar //?/C:/foo/bar /\?\C:/foo\bar \/Server\Foo/Bar c:/foo//bar//baz].each do |path|
        it "should return true for #{path}" do
          Puppet::Util.should be_absolute_path(path, :windows)
        end
      end

      %w[/ . ./foo \foo /foo /foo/../bar //foo C:foo/bar foo//bar/baz].each do |path|
        it "should return false for #{path}" do
          Puppet::Util.should_not be_absolute_path(path, :windows)
        end
      end
    end
  end

  describe "#path_to_uri" do
    %w[. .. foo foo/bar foo/../bar].each do |path|
      it "should reject relative path: #{path}" do
        lambda { Puppet::Util.path_to_uri(path) }.should raise_error(Puppet::Error)
      end
    end

    it "should perform URI escaping" do
      Puppet::Util.path_to_uri("/foo bar").path.should == "/foo%20bar"
    end

    describe "when using platform :posix" do
      before :each do
        Puppet.features.stubs(:posix).returns true
        Puppet.features.stubs(:microsoft_windows?).returns false
      end

      %w[/ /foo /foo/../bar].each do |path|
        it "should convert #{path} to URI" do
          Puppet::Util.path_to_uri(path).path.should == path
        end
      end
    end

    describe "when using platform :windows" do
      before :each do
        Puppet.features.stubs(:posix).returns false
        Puppet.features.stubs(:microsoft_windows?).returns true
      end

      it "should normalize backslashes" do
        Puppet::Util.path_to_uri('c:\\foo\\bar\\baz').path.should == '/' + 'c:/foo/bar/baz'
      end

      %w[C:/ C:/foo/bar].each do |path|
        it "should convert #{path} to absolute URI" do
          Puppet::Util.path_to_uri(path).path.should == '/' + path
        end
      end

      %w[share C$].each do |path|
        it "should convert UNC #{path} to absolute URI" do
          uri = Puppet::Util.path_to_uri("\\\\server\\#{path}")
          uri.host.should == 'server'
          uri.path.should == '/' + path
        end
      end
    end
  end

  describe ".uri_to_path" do
    require 'uri'

    it "should strip host component" do
      Puppet::Util.uri_to_path(URI.parse('http://foo/bar')).should == '/bar'
    end

    it "should accept puppet URLs" do
      Puppet::Util.uri_to_path(URI.parse('puppet:///modules/foo')).should == '/modules/foo'
    end

    it "should return unencoded path" do
      Puppet::Util.uri_to_path(URI.parse('http://foo/bar%20baz')).should == '/bar baz'
    end

    it "should be nil-safe" do
      Puppet::Util.uri_to_path(nil).should be_nil
    end

    describe "when using platform :posix",:if => Puppet.features.posix? do
      it "should accept root" do
        Puppet::Util.uri_to_path(URI.parse('file:/')).should == '/'
      end

      it "should accept single slash" do
        Puppet::Util.uri_to_path(URI.parse('file:/foo/bar')).should == '/foo/bar'
      end

      it "should accept triple slashes" do
        Puppet::Util.uri_to_path(URI.parse('file:///foo/bar')).should == '/foo/bar'
      end
    end

    describe "when using platform :windows", :if => Puppet.features.microsoft_windows? do
      it "should accept root" do
        Puppet::Util.uri_to_path(URI.parse('file:/C:/')).should == 'C:/'
      end

      it "should accept single slash" do
        Puppet::Util.uri_to_path(URI.parse('file:/C:/foo/bar')).should == 'C:/foo/bar'
      end

      it "should accept triple slashes" do
        Puppet::Util.uri_to_path(URI.parse('file:///C:/foo/bar')).should == 'C:/foo/bar'
      end

      it "should accept file scheme with double slashes as a UNC path" do
        Puppet::Util.uri_to_path(URI.parse('file://host/share/file')).should == '//host/share/file'
      end
    end
  end

  describe "safe_posix_fork" do
    let(:pid) { 5501 }

    before :each do
      # Most of the things this method does are bad to do during specs. :/
      Kernel.stubs(:fork).returns(pid).yields

      $stdin.stubs(:reopen)
      $stdout.stubs(:reopen)
      $stderr.stubs(:reopen)

      # ensure that we don't really close anything!
      (0..256).each {|n| IO.stubs(:new) }
    end

    it "should close all open file descriptors except stdin/stdout/stderr" do
      # This is ugly, but I can't really think of a better way to do it without
      # letting it actually close fds, which seems risky
      (0..2).each {|n| IO.expects(:new).with(n).never}
      (3..256).each {|n| IO.expects(:new).with(n).returns mock('io', :close) }

      Puppet::Util.safe_posix_fork
    end

    it "should fork a child process to execute the block" do
      Kernel.expects(:fork).returns(pid).yields

      Puppet::Util.safe_posix_fork do
        message = "Fork this!"
      end
    end

    it "should return the pid of the child process" do
      Puppet::Util.safe_posix_fork.should == pid
    end
  end

  describe "#which" do
    let(:base) { File.expand_path('/bin') }
    let(:path) { File.join(base, 'foo') }

    before :each do
      FileTest.stubs(:file?).returns false
      FileTest.stubs(:file?).with(path).returns true

      FileTest.stubs(:executable?).returns false
      FileTest.stubs(:executable?).with(path).returns true
    end

    it "should accept absolute paths" do
      Puppet::Util.which(path).should == path
    end

    it "should return nil if no executable found" do
      Puppet::Util.which('doesnotexist').should be_nil
    end

    it "should warn if the user's HOME is not set but their PATH contains a ~" do
      env_path = %w[~/bin /usr/bin /bin].join(File::PATH_SEPARATOR)

      env = {:HOME => nil, :PATH => env_path}
      env.merge!({:HOMEDRIVE => nil, :USERPROFILE => nil}) if Puppet.features.microsoft_windows?

      Puppet::Util.withenv(env) do
        Puppet::Util::Warnings.expects(:warnonce).once
        Puppet::Util.which('foo')
      end
    end

    it "should reject directories" do
      Puppet::Util.which(base).should be_nil
    end

    it "should ignore ~user directories if the user doesn't exist" do
      # Windows treats *any* user as a "user that doesn't exist", which means
      # that this will work correctly across all our platforms, and should
      # behave consistently.  If they ever implement it correctly (eg: to do
      # the lookup for real) it should just work transparently.
      baduser = 'if_this_user_exists_I_will_eat_my_hat'
      Puppet::Util.withenv("PATH" => "~#{baduser}#{File::PATH_SEPARATOR}#{base}") do
        Puppet::Util.which('foo').should == path
      end
    end

    describe "on POSIX systems" do
      before :each do
        Puppet.features.stubs(:posix?).returns true
        Puppet.features.stubs(:microsoft_windows?).returns false
      end

      it "should walk the search PATH returning the first executable" do
        ENV.stubs(:[]).with('PATH').returns(File.expand_path('/bin'))

        Puppet::Util.which('foo').should == path
      end
    end

    describe "on Windows systems" do
      let(:path) { File.expand_path(File.join(base, 'foo.CMD')) }

      before :each do
        Puppet.features.stubs(:posix?).returns false
        Puppet.features.stubs(:microsoft_windows?).returns true
      end

      describe "when a file extension is specified" do
        it "should walk each directory in PATH ignoring PATHEXT" do
          ENV.stubs(:[]).with('PATH').returns(%w[/bar /bin].map{|dir| File.expand_path(dir)}.join(File::PATH_SEPARATOR))

          FileTest.expects(:file?).with(File.join(File.expand_path('/bar'), 'foo.CMD')).returns false

          ENV.expects(:[]).with('PATHEXT').never
          Puppet::Util.which('foo.CMD').should == path
        end
      end

      describe "when a file extension is not specified" do
        it "should walk each extension in PATHEXT until an executable is found" do
          bar = File.expand_path('/bar')
          ENV.stubs(:[]).with('PATH').returns("#{bar}#{File::PATH_SEPARATOR}#{base}")
          ENV.stubs(:[]).with('PATHEXT').returns(".EXE#{File::PATH_SEPARATOR}.CMD")

          exts = sequence('extensions')
          FileTest.expects(:file?).in_sequence(exts).with(File.join(bar, 'foo.EXE')).returns false
          FileTest.expects(:file?).in_sequence(exts).with(File.join(bar, 'foo.CMD')).returns false
          FileTest.expects(:file?).in_sequence(exts).with(File.join(base, 'foo.EXE')).returns false
          FileTest.expects(:file?).in_sequence(exts).with(path).returns true

          Puppet::Util.which('foo').should == path
        end

        it "should walk the default extension path if the environment variable is not defined" do
          ENV.stubs(:[]).with('PATH').returns(base)
          ENV.stubs(:[]).with('PATHEXT').returns(nil)

          exts = sequence('extensions')
          %w[.COM .EXE .BAT].each do |ext|
            FileTest.expects(:file?).in_sequence(exts).with(File.join(base, "foo#{ext}")).returns false
          end
          FileTest.expects(:file?).in_sequence(exts).with(path).returns true

          Puppet::Util.which('foo').should == path
        end

        it "should fall back if no extension matches" do
          ENV.stubs(:[]).with('PATH').returns(base)
          ENV.stubs(:[]).with('PATHEXT').returns(".EXE")

          FileTest.stubs(:file?).with(File.join(base, 'foo.EXE')).returns false
          FileTest.stubs(:file?).with(File.join(base, 'foo')).returns true
          FileTest.stubs(:executable?).with(File.join(base, 'foo')).returns true

          Puppet::Util.which('foo').should == File.join(base, 'foo')
        end
      end
    end
  end

  describe "#binread" do
    let(:contents) { "foo\r\nbar" }

    it "should preserve line endings" do
      path = tmpfile('util_binread')
      File.open(path, 'wb') { |f| f.print contents }

      Puppet::Util.binread(path).should == contents
    end

    it "should raise an error if the file doesn't exist" do
      expect { Puppet::Util.binread('/path/does/not/exist') }.to raise_error(Errno::ENOENT)
    end
  end

  describe "hash symbolizing functions" do
    let (:myhash) { { "foo" => "bar", :baz => "bam" } }
    let (:resulthash) { { :foo => "bar", :baz => "bam" } }

    describe "#symbolizehash" do
      it "should return a symbolized hash" do
        newhash = Puppet::Util.symbolizehash(myhash)
        newhash.should == resulthash
      end
    end
  end

  context "#replace_file" do
    subject { Puppet::Util }
    it { should respond_to :replace_file }

    let :target do
      target = Tempfile.new("puppet-util-replace-file")
      target.puts("hello, world")
      target.flush              # make sure content is on disk.
      target.fsync rescue nil
      target.close
      target
    end

    it "should fail if no block is given" do
      expect { subject.replace_file(target.path, 0600) }.to raise_error /block/
    end

    it "should replace a file when invoked" do
      # Check that our file has the expected content.
      File.read(target.path).should == "hello, world\n"

      # Replace the file.
      subject.replace_file(target.path, 0600) do |fh|
        fh.puts "I am the passenger..."
      end

      # ...and check the replacement was complete.
      File.read(target.path).should == "I am the passenger...\n"
    end

    # When running with the same user and group sid, which is the default,
    # Windows collapses the owner and group modes into a single ACE, resulting
    # in set(0600) => get(0660) and so forth. --daniel 2012-03-30
    modes = [0555, 0660, 0770]
    modes += [0600, 0700] unless Puppet.features.microsoft_windows?
    modes.each do |mode|
      it "should copy 0#{mode.to_s(8)} permissions from the target file by default" do
        set_mode(mode, target.path)

        get_mode(target.path).should == mode

        subject.replace_file(target.path, 0000) {|fh| fh.puts "bazam" }

        get_mode(target.path).should == mode
        File.read(target.path).should == "bazam\n"
      end
    end

    it "should copy the permissions of the source file before yielding on Unix", :if => !Puppet.features.microsoft_windows? do
      set_mode(0555, target.path)
      inode = Puppet::FileSystem.stat(target.path).ino

      yielded = false
      subject.replace_file(target.path, 0600) do |fh|
        get_mode(fh.path).should == 0555
        yielded = true
      end
      yielded.should be_true

      Puppet::FileSystem.stat(target.path).ino.should_not == inode
      get_mode(target.path).should == 0555
    end

    it "should use the default permissions if the source file doesn't exist" do
      new_target = target.path + '.foo'
      Puppet::FileSystem.exist?(new_target).should be_false

      begin
        subject.replace_file(new_target, 0555) {|fh| fh.puts "foo" }
        get_mode(new_target).should == 0555
      ensure
        Puppet::FileSystem.unlink(new_target) if Puppet::FileSystem.exist?(new_target)
      end
    end

    it "should not replace the file if an exception is thrown in the block" do
      yielded = false
      threw   = false

      begin
        subject.replace_file(target.path, 0600) do |fh|
          yielded = true
          fh.puts "different content written, then..."
          raise "...throw some random failure"
        end
      rescue Exception => e
        if e.to_s =~ /some random failure/
          threw = true
        else
          raise
        end
      end

      yielded.should be_true
      threw.should be_true

      # ...and check the replacement was complete.
      File.read(target.path).should == "hello, world\n"
    end

    {:string => '664', :number => 0664, :symbolic => "ug=rw-,o=r--" }.each do |label,mode|
      it "should support #{label} format permissions" do
        new_target = target.path + "#{mode}.foo"
        Puppet::FileSystem.exist?(new_target).should be_false

        begin
          subject.replace_file(new_target, mode) {|fh| fh.puts "this is an interesting content" }

          get_mode(new_target).should == 0664
        ensure
          Puppet::FileSystem.unlink(new_target) if Puppet::FileSystem.exist?(new_target)
        end
      end
    end

  end

  describe "#pretty_backtrace" do
    it "should include lines that don't match the standard backtrace pattern" do
      line = "non-standard line\n"
      trace = caller[0..2] + [line] + caller[3..-1]
      Puppet::Util.pretty_backtrace(trace).should =~ /#{line}/
    end

    it "should include function names" do
      Puppet::Util.pretty_backtrace.should =~ /:in `\w+'/
    end

    it "should work with Windows paths" do
      Puppet::Util.pretty_backtrace(["C:/work/puppet/c.rb:12:in `foo'\n"]).
        should == "C:/work/puppet/c.rb:12:in `foo'"
    end
  end

  describe "#execute" do
    let(:command) { 'mycommand' }

    it "should pass arguments through" do
      arguments = 'myarg'
      Puppet::Util::Execution.expects(:execute).with(command, arguments)

      subject.execute(command, arguments)
    end

    it "should not supply default arguments" do
      Puppet::Util::Execution.expects(:execute).with(command)

      subject.execute(command)
    end
  end

  describe "#deterministic_rand" do

    it "should not fiddle with future rand calls" do
      Puppet::Util.deterministic_rand(123,20)
      rand_one = rand()
      Puppet::Util.deterministic_rand(123,20)
      rand().should_not eql(rand_one)
    end

    if defined?(Random) == 'constant' && Random.class == Class
      it "should not fiddle with the global seed" do
        srand(1234)
        Puppet::Util.deterministic_rand(123,20)
        srand().should eql(1234)
      end
    # ruby below 1.9.2 variant
    else
      it "should set a new global seed" do
        srand(1234)
        Puppet::Util.deterministic_rand(123,20)
        srand().should_not eql(1234)
      end
    end
  end
end
