#! /usr/bin/env ruby
require 'spec_helper'
require 'uri'

source = Puppet::Type.type(:file).attrclass(:source)
describe Puppet::Type.type(:file).attrclass(:source) do
  include PuppetSpec::Files

  around :each do |example|
    Puppet.override(:environments => Puppet::Environments::Static.new) do
      example.run
    end
  end

  before do
    # Wow that's a messy interface to the resource.
    @environment = Puppet::Node::Environment.remote("myenv")
    @resource = stub 'resource', :[]= => nil, :property => nil, :catalog => Puppet::Resource::Catalog.new(nil, @environment), :line => 0, :file => ''
    @foobar = make_absolute("/foo/bar baz")
    @feebooz = make_absolute("/fee/booz baz")

    @foobar_uri  = URI.unescape(Puppet::Util.path_to_uri(@foobar).to_s)
    @feebooz_uri = URI.unescape(Puppet::Util.path_to_uri(@feebooz).to_s)
  end

  it "should be a subclass of Parameter" do
    source.superclass.must == Puppet::Parameter
  end

  describe "#validate" do
    let(:path) { tmpfile('file_source_validate') }
    let(:resource) { Puppet::Type.type(:file).new(:path => path) }

    it "should fail if the set values are not URLs" do
      URI.expects(:parse).with('foo').raises RuntimeError

      lambda { resource[:source] = %w{foo} }.must raise_error(Puppet::Error)
    end

    it "should fail if the URI is not a local file, file URI, or puppet URI" do
      lambda { resource[:source] = %w{http://foo/bar} }.must raise_error(Puppet::Error, /Cannot use URLs of type 'http' as source for fileserving/)
    end

    it "should strip trailing forward slashes", :unless => Puppet.features.microsoft_windows? do
      resource[:source] = "/foo/bar\\//"
      resource[:source].should == %w{file:/foo/bar\\}
    end

    it "should strip trailing forward and backslashes", :if => Puppet.features.microsoft_windows? do
      resource[:source] = "X:/foo/bar\\//"
      resource[:source].should == %w{file:/X:/foo/bar}
    end

    it "should accept an array of sources" do
      resource[:source] = %w{file:///foo/bar puppet://host:8140/foo/bar}
      resource[:source].should == %w{file:///foo/bar puppet://host:8140/foo/bar}
    end

    it "should accept file path characters that are not valid in URI" do
      resource[:source] = 'file:///foo bar'
    end

    it "should reject relative URI sources" do
      lambda { resource[:source] = 'foo/bar' }.must raise_error(Puppet::Error)
    end

    it "should reject opaque sources" do
      lambda { resource[:source] = 'mailto:foo@com' }.must raise_error(Puppet::Error)
    end

    it "should accept URI authority component" do
      resource[:source] = 'file://host/foo'
      resource[:source].should == %w{file://host/foo}
    end

    it "should accept when URI authority is absent" do
      resource[:source] = 'file:///foo/bar'
      resource[:source].should == %w{file:///foo/bar}
    end
  end

  describe "#munge" do
    let(:path) { tmpfile('file_source_munge') }
    let(:resource) { Puppet::Type.type(:file).new(:path => path) }

    it "should prefix file scheme to absolute paths" do
      resource[:source] = path
      resource[:source].should == [URI.unescape(Puppet::Util.path_to_uri(path).to_s)]
    end

    %w[file puppet].each do |scheme|
      it "should not prefix valid #{scheme} URIs" do
        resource[:source] = "#{scheme}:///foo bar"
        resource[:source].should == ["#{scheme}:///foo bar"]
      end
    end
  end

  describe "when returning the metadata" do
    before do
      @metadata = stub 'metadata', :source= => nil
      @resource.stubs(:[]).with(:links).returns :manage
      @resource.stubs(:[]).with(:source_permissions)
    end

    it "should return already-available metadata" do
      @source = source.new(:resource => @resource)
      @source.metadata = "foo"
      @source.metadata.should == "foo"
    end

    it "should return nil if no @should value is set and no metadata is available" do
      @source = source.new(:resource => @resource)
      @source.metadata.should be_nil
    end

    it "should collect its metadata using the Metadata class if it is not already set" do
      @source = source.new(:resource => @resource, :value => @foobar)
      Puppet::FileServing::Metadata.indirection.expects(:find).with do |uri, options|
        expect(uri).to eq @foobar_uri
        expect(options[:environment]).to eq @environment
        expect(options[:links]).to eq :manage
      end.returns @metadata

      @source.metadata
    end

    it "should use the metadata from the first found source" do
      metadata = stub 'metadata', :source= => nil
      @source = source.new(:resource => @resource, :value => [@foobar, @feebooz])
      options = {
        :environment => @environment,
        :links => :manage,
        :source_permissions => nil
      }
      Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar_uri, options).returns nil
      Puppet::FileServing::Metadata.indirection.expects(:find).with(@feebooz_uri, options).returns metadata
      @source.metadata.should equal(metadata)
    end

    it "should store the found source as the metadata's source" do
      metadata = mock 'metadata'
      @source = source.new(:resource => @resource, :value => @foobar)
      Puppet::FileServing::Metadata.indirection.expects(:find).with do |uri, options|
        expect(uri).to eq @foobar_uri
        expect(options[:environment]).to eq @environment
        expect(options[:links]).to eq :manage
      end.returns metadata

      metadata.expects(:source=).with(@foobar_uri)
      @source.metadata
    end

    it "should fail intelligently if an exception is encountered while querying for metadata" do
      @source = source.new(:resource => @resource, :value => @foobar)
      Puppet::FileServing::Metadata.indirection.expects(:find).with do |uri, options|
        expect(uri).to eq @foobar_uri
        expect(options[:environment]).to eq @environment
        expect(options[:links]).to eq :manage
      end.raises RuntimeError

      @source.expects(:fail).raises ArgumentError
      lambda { @source.metadata }.should raise_error(ArgumentError)
    end

    it "should fail if no specified sources can be found" do
      @source = source.new(:resource => @resource, :value => @foobar)
      Puppet::FileServing::Metadata.indirection.expects(:find).with  do |uri, options|
        expect(uri).to eq @foobar_uri
        expect(options[:environment]).to eq @environment
        expect(options[:links]).to eq :manage
      end.returns nil

      @source.expects(:fail).raises RuntimeError

      lambda { @source.metadata }.should raise_error(RuntimeError)
    end
  end

  it "should have a method for setting the desired values on the resource" do
    source.new(:resource => @resource).must respond_to(:copy_source_values)
  end

  describe "when copying the source values" do
    before :each do
      @resource = Puppet::Type.type(:file).new :path => @foobar

      @source = source.new(:resource => @resource)
      @metadata = stub 'metadata', :owner => 100, :group => 200, :mode => "173", :checksum => "{md5}asdfasdf", :ftype => "file", :source => @foobar
      @source.stubs(:metadata).returns @metadata

      Puppet.features.stubs(:root?).returns true
    end

    it "should not issue a deprecation warning if the source mode value is a Numeric" do
      @metadata.stubs(:mode).returns 0173
      if Puppet::Util::Platform.windows?
        Puppet.expects(:deprecation_warning).with(regexp_matches(/Copying owner\/mode\/group from the source file on Windows is deprecated/)).at_least_once
      else
        Puppet.expects(:deprecation_warning).never
      end

      @source.copy_source_values
    end

    it "should not issue a deprecation warning if the source mode value is a String" do
      @metadata.stubs(:mode).returns "173"
      if Puppet::Util::Platform.windows?
        Puppet.expects(:deprecation_warning).with(regexp_matches(/Copying owner\/mode\/group from the source file on Windows is deprecated/)).at_least_once
      else
        Puppet.expects(:deprecation_warning).never
      end

      @source.copy_source_values
    end

    it "should fail if there is no metadata" do
      @source.stubs(:metadata).returns nil
      @source.expects(:devfail).raises ArgumentError
      lambda { @source.copy_source_values }.should raise_error(ArgumentError)
    end

    it "should set :ensure to the file type" do
      @metadata.stubs(:ftype).returns "file"

      @source.copy_source_values
      @resource[:ensure].must == :file
    end

    it "should not set 'ensure' if it is already set to 'absent'" do
      @metadata.stubs(:ftype).returns "file"

      @resource[:ensure] = :absent
      @source.copy_source_values
      @resource[:ensure].must == :absent
    end

    describe "and the source is a file" do
      before do
        @metadata.stubs(:ftype).returns "file"
        Puppet.features.stubs(:microsoft_windows?).returns false
      end

      it "should copy the metadata's owner, group, checksum, and mode to the resource if they are not set on the resource" do
        @source.copy_source_values

        @resource[:owner].must == 100
        @resource[:group].must == 200
        @resource[:mode].must == "173"

        # Metadata calls it checksum, we call it content.
        @resource[:content].must == @metadata.checksum
      end

      it "should not copy the metadata's owner, group, checksum and mode to the resource if they are already set" do
        @resource[:owner] = 1
        @resource[:group] = 2
        @resource[:mode] = 3
        @resource[:content] = "foobar"

        @source.copy_source_values

        @resource[:owner].must == 1
        @resource[:group].must == 2
        @resource[:mode].must == "3"
        @resource[:content].should_not == @metadata.checksum
      end

      describe "and puppet is not running as root" do
        before do
          Puppet.features.stubs(:root?).returns false
        end

        it "should not try to set the owner" do
          @source.copy_source_values
          @resource[:owner].should be_nil
        end

        it "should not try to set the group" do
          @source.copy_source_values
          @resource[:group].should be_nil
        end
      end

      context "when source_permissions is `use_when_creating`" do
        before :each do
          @resource[:source_permissions] = "use_when_creating"
          Puppet.features.expects(:root?).returns true
          @source.stubs(:local?).returns(false)
        end

        context "when managing a new file" do
          it "should copy owner and group from local sources" do
            @source.stubs(:local?).returns true

            @source.copy_source_values

            @resource[:owner].must == 100
            @resource[:group].must == 200
            @resource[:mode].must == "173"
          end

          it "copies the remote owner" do
            @source.copy_source_values

            @resource[:owner].must == 100
          end

          it "copies the remote group" do
            @source.copy_source_values

            @resource[:group].must == 200
          end

          it "copies the remote mode" do
            @source.copy_source_values

            @resource[:mode].must == "173"
          end
        end

        context "when managing an existing file" do
          before :each do
            Puppet::FileSystem.stubs(:exist?).with(@resource[:path]).returns(true)
          end

          it "should not copy owner, group or mode from local sources" do
            @source.stubs(:local?).returns true

            @source.copy_source_values

            @resource[:owner].must be_nil
            @resource[:group].must be_nil
            @resource[:mode].must be_nil
          end

          it "preserves the local owner" do
            @source.copy_source_values

            @resource[:owner].must be_nil
          end

          it "preserves the local group" do
            @source.copy_source_values

            @resource[:group].must be_nil
          end

          it "preserves the local mode" do
            @source.copy_source_values

            @resource[:mode].must be_nil
          end
        end
      end

      context "when source_permissions is `ignore`" do
        before :each do
          @resource[:source_permissions] = "ignore"
          @source.stubs(:local?).returns(false)
          Puppet.features.expects(:root?).returns true
        end

        it "should not copy owner, group or mode from local sources" do
          @source.stubs(:local?).returns true

          @source.copy_source_values

          @resource[:owner].must be_nil
          @resource[:group].must be_nil
          @resource[:mode].must be_nil
        end

        it "preserves the local owner" do
          @source.copy_source_values

          @resource[:owner].must be_nil
        end

        it "preserves the local group" do
          @source.copy_source_values

          @resource[:group].must be_nil
        end

        it "preserves the local mode" do
          @source.copy_source_values

          @resource[:mode].must be_nil
        end
      end

      describe "on Windows" do
        before :each do
          Puppet.features.stubs(:microsoft_windows?).returns true
        end
        let(:deprecation_message) { "Copying owner/mode/group from the" <<
              " source file on Windows is deprecated;" <<
              " use source_permissions => ignore." }

        it "should copy only mode from remote sources" do
          @source.stubs(:local?).returns false

          @source.copy_source_values

          @resource[:owner].must be_nil
          @resource[:group].must be_nil
          @resource[:mode].must == "173"
        end

        it "should copy mode from remote sources" do
          @source.stubs(:local?).returns false

          @source.copy_source_values

          @resource[:mode].must == "173"
        end

        it "should copy owner and group from local sources" do
          @source.stubs(:local?).returns true

          @source.copy_source_values

          @resource[:owner].must == 100
          @resource[:group].must == 200
          @resource[:mode].must == "173"
        end

        it "should issue deprecation warning when copying metadata from remote sources when group, owner, and mode are unspecified" do
          @source.stubs(:local?).returns false
          Puppet.expects(:deprecation_warning).with(deprecation_message).at_least_once

          @source.copy_source_values
        end

        it "should issue deprecation warning when copying metadata from remote sources if only user is unspecified" do
          @source.stubs(:local?).returns false
          Puppet.expects(:deprecation_warning).with(deprecation_message).at_least_once
          @resource[:group] = 2
          @resource[:mode] = "0003"

          @source.copy_source_values
        end

        it "should issue deprecation warning when copying metadata from remote sources if only group is unspecified" do
          @source.stubs(:local?).returns false
          Puppet.expects(:deprecation_warning).with(deprecation_message).at_least_once
          @resource[:owner] = 1
          @resource[:mode] = "0003"

          @source.copy_source_values
        end

        it "should issue deprecation warning when copying metadata from remote sources if only mode is unspecified" do
          @source.stubs(:local?).returns false
          Puppet.expects(:deprecation_warning).with(deprecation_message).at_least_once
          @resource[:owner] = 1
          @resource[:group] = 2

          @source.copy_source_values
        end

        it "should not issue deprecation warning when copying metadata from remote sources if group, owner, and mode are all specified" do
          @source.stubs(:local?).returns false
          Puppet.expects(:deprecation_warning).with(deprecation_message).never
          @resource[:owner] = 1
          @resource[:group] = 2
          @resource[:mode] = "0003"

          @source.copy_source_values
        end
      end
    end

    describe "and the source is a link" do
      it "should set the target to the link destination" do
        @metadata.stubs(:ftype).returns "link"
        @metadata.stubs(:links).returns "manage"
        @resource.stubs(:[])
        @resource.stubs(:[]=)

        @metadata.expects(:destination).returns "/path/to/symlink"

        @resource.expects(:[]=).with(:target, "/path/to/symlink")
        @source.copy_source_values
      end
    end
  end

  it "should have a local? method" do
    source.new(:resource => @resource).must be_respond_to(:local?)
  end

  context "when accessing source properties" do
    let(:catalog) { Puppet::Resource::Catalog.new }
    let(:path) { tmpfile('file_resource') }
    let(:resource) { Puppet::Type.type(:file).new(:path => path, :catalog => catalog) }
    let(:sourcepath) { tmpfile('file_source') }

    describe "for local sources" do
      before :each do
        FileUtils.touch(sourcepath)
      end

      describe "on POSIX systems", :if => Puppet.features.posix? do
        ['', "file:", "file://"].each do |prefix|
          it "with prefix '#{prefix}' should be local" do
            resource[:source] = "#{prefix}#{sourcepath}"
            resource.parameter(:source).must be_local
          end

          it "should be able to return the metadata source full path" do
            resource[:source] = "#{prefix}#{sourcepath}"
            resource.parameter(:source).full_path.should == sourcepath
          end
        end
      end

      describe "on Windows systems", :if => Puppet.features.microsoft_windows? do
        ['', "file:/", "file:///"].each do |prefix|
          it "should be local with prefix '#{prefix}'" do
            resource[:source] = "#{prefix}#{sourcepath}"
            resource.parameter(:source).must be_local
          end

          it "should be able to return the metadata source full path" do
            resource[:source] = "#{prefix}#{sourcepath}"
            resource.parameter(:source).full_path.should == sourcepath
          end

          it "should convert backslashes to forward slashes" do
            resource[:source] = "#{prefix}#{sourcepath.gsub(/\\/, '/')}"
          end
        end

        it "should be UNC with two slashes"
      end
    end

    describe "for remote sources" do
      let(:sourcepath) { "/path/to/source" }
      let(:uri) { URI::Generic.build(:scheme => 'puppet', :host => 'server', :port => 8192, :path => sourcepath).to_s }

      before(:each) do
        metadata = Puppet::FileServing::Metadata.new(path, :source => uri, 'type' => 'file')
        #metadata = stub('remote', :ftype => "file", :source => uri)
        Puppet::FileServing::Metadata.indirection.stubs(:find).
          with(uri,all_of(has_key(:environment), has_key(:links))).returns metadata
        resource[:source] = uri
      end

      it "should not be local" do
        resource.parameter(:source).should_not be_local
      end

      it "should be able to return the metadata source full path" do
        resource.parameter(:source).full_path.should == "/path/to/source"
      end

      it "should be able to return the source server" do
        resource.parameter(:source).server.should == "server"
      end

      it "should be able to return the source port" do
        resource.parameter(:source).port.should == 8192
      end

      describe "which don't specify server or port" do
        let(:uri) { "puppet:///path/to/source" }

        it "should return the default source server" do
          Puppet[:server] = "myserver"
          resource.parameter(:source).server.should == "myserver"
        end

        it "should return the default source port" do
          Puppet[:masterport] = 1234
          resource.parameter(:source).port.should == 1234
        end
      end
    end
  end
end
