require 'spec_helper'
require 'puppet_spec/compiler'
require 'matchers/resource'

require 'puppet/indirector/catalog/compiler'

def set_facts(fact_hash)
  fact_hash.each do |key, value|
    allow(Facter).to receive(:value).with(key).and_return(value)
  end
end

describe Puppet::Resource::Catalog::Compiler do
  include Matchers::Resource
  include PuppetSpec::Files

  let(:compiler) { described_class.new }
  let(:node_name) { "foo" }
  let(:node) { Puppet::Node.new(node_name)}

  describe "when initializing" do
    before do
      allow(Puppet).to receive(:version).and_return(1)
    end

    it "should cache the server metadata and reuse it" do
      Puppet[:node_terminus] = :memory
      Puppet::Node.indirection.save(Puppet::Node.new("node1"))
      Puppet::Node.indirection.save(Puppet::Node.new("node2"))

      allow(compiler).to receive(:compile)

      compiler.find(Puppet::Indirector::Request.new(:catalog, :find, 'node1', nil, :node => 'node1'))
      compiler.find(Puppet::Indirector::Request.new(:catalog, :find, 'node2', nil, :node => 'node2'))
    end
  end

  describe "when finding catalogs" do
    before do
      allow(node).to receive(:merge)
      allow(Puppet::Node.indirection).to receive(:find).and_return(node)
      @request = Puppet::Indirector::Request.new(:catalog, :find, node_name, nil, :node => node_name)
    end

    it "should directly use provided nodes for a local request" do
      expect(Puppet::Node.indirection).not_to receive(:find)
      expect(compiler).to receive(:compile).with(node, anything)
      allow(@request).to receive(:options).and_return(:use_node => node)
      allow(@request).to receive(:remote?).and_return(false)
      compiler.find(@request)
    end

    it "rejects a provided node if the request is remote" do
      allow(@request).to receive(:options).and_return(:use_node => node)
      allow(@request).to receive(:remote?).and_return(true)
      expect {
        compiler.find(@request)
      }.to raise_error Puppet::Error, /invalid option use_node/i
    end

    it "should use the authenticated node name if no request key is provided" do
      allow(@request).to receive(:key).and_return(nil)
      expect(Puppet::Node.indirection).to receive(:find).with(node_name, anything).and_return(node)
      expect(compiler).to receive(:compile).with(node, anything)
      compiler.find(@request)
    end

    it "should use the provided node name by default" do
      expect(@request).to receive(:key).and_return("my_node")

      expect(Puppet::Node.indirection).to receive(:find).with("my_node", anything).and_return(node)
      expect(compiler).to receive(:compile).with(node, anything)
      compiler.find(@request)
    end

    it "should fail if no node is passed and none can be found" do
      allow(Puppet::Node.indirection).to receive(:find).with(node_name, anything).and_return(nil)
      expect { compiler.find(@request) }.to raise_error(ArgumentError)
    end

    it "should fail intelligently when searching for a node raises an exception" do
      allow(Puppet::Node.indirection).to receive(:find).with(node_name, anything).and_raise("eh")
      expect { compiler.find(@request) }.to raise_error(Puppet::Error)
    end

    it "should pass the found node to the compiler for compiling" do
      expect(Puppet::Node.indirection).to receive(:find).with(node_name, anything).and_return(node)
      expect(Puppet::Parser::Compiler).to receive(:compile).with(node, anything)
      compiler.find(@request)
    end

    it "should pass node containing percent character to the compiler" do
      node_with_percent_character = Puppet::Node.new "%6de"
      allow(Puppet::Node.indirection).to receive(:find).and_return(node_with_percent_character)
      expect(Puppet::Parser::Compiler).to receive(:compile).with(node_with_percent_character, anything)
      compiler.find(@request)
    end

    it "should extract any facts from the request" do
      expect(Puppet::Node.indirection).to receive(:find).with(node_name, anything).and_return(node)
      expect(compiler).to receive(:extract_facts_from_request).with(@request)
      allow(Puppet::Parser::Compiler).to receive(:compile)
      compiler.find(@request)
    end

    it "requires `facts_format` option if facts are passed in" do
      facts = Puppet::Node::Facts.new("mynode", :afact => "avalue")
      request = Puppet::Indirector::Request.new(:catalog, :find, "mynode", nil, :facts => facts)
      expect {
        compiler.find(request)
      }.to raise_error ArgumentError, /no fact format provided for mynode/
    end

    it "rejects facts in the request from a different node" do
      facts = Puppet::Node::Facts.new("differentnode", :afact => "avalue")
      request = Puppet::Indirector::Request.new(
        :catalog, :find, "mynode", nil, :facts => facts, :facts_format => "unused"
      )
      expect {
        compiler.find(request)
      }.to raise_error Puppet::Error, /fact definition for the wrong node/i
    end

    it "should return the results of compiling as the catalog" do
      allow(Puppet::Node.indirection).to receive(:find).and_return(node)
      catalog = Puppet::Resource::Catalog.new(node.name)
      allow(Puppet::Parser::Compiler).to receive(:compile).and_return(catalog)

      expect(compiler.find(@request)).to equal(catalog)
    end

    it "passes the code_id from the request to the compiler" do
      allow(Puppet::Node.indirection).to receive(:find).and_return(node)
      code_id = 'b59e5df0578ef411f773ee6c33d8073c50e7b8fe'
      @request.options[:code_id] = code_id

      expect(Puppet::Parser::Compiler).to receive(:compile).with(anything, code_id)

      compiler.find(@request)
    end

    it "returns a catalog with the code_id from the request" do
      allow(Puppet::Node.indirection).to receive(:find).and_return(node)
      code_id = 'b59e5df0578ef411f773ee6c33d8073c50e7b8fe'
      @request.options[:code_id] = code_id

      catalog = Puppet::Resource::Catalog.new(node.name, node.environment, code_id)
      allow(Puppet::Parser::Compiler).to receive(:compile).and_return(catalog)

      expect(compiler.find(@request).code_id).to eq(code_id)
    end

    it "does not inline metadata when the static_catalog option is false" do
      allow(Puppet::Node.indirection).to receive(:find).and_return(node)
      @request.options[:static_catalog] = false
      @request.options[:code_id] = 'some_code_id'
      allow(node.environment).to receive(:static_catalogs?).and_return(true)

      catalog = Puppet::Resource::Catalog.new(node.name, node.environment)
      allow(Puppet::Parser::Compiler).to receive(:compile).and_return(catalog)

      expect(compiler).not_to receive(:inline_metadata)
      compiler.find(@request)
    end

    it "does not inline metadata when static_catalogs are disabled" do
      allow(Puppet::Node.indirection).to receive(:find).and_return(node)
      @request.options[:static_catalog] = true
      @request.options[:checksum_type] = 'md5'
      @request.options[:code_id] = 'some_code_id'
      allow(node.environment).to receive(:static_catalogs?).and_return(false)

      catalog = Puppet::Resource::Catalog.new(node.name, node.environment)
      allow(Puppet::Parser::Compiler).to receive(:compile).and_return(catalog)

      expect(compiler).not_to receive(:inline_metadata)
      compiler.find(@request)
    end

    it "does not inline metadata when code_id is not specified" do
      allow(Puppet::Node.indirection).to receive(:find).and_return(node)
      @request.options[:static_catalog] = true
      @request.options[:checksum_type] = 'md5'
      allow(node.environment).to receive(:static_catalogs?).and_return(true)

      catalog = Puppet::Resource::Catalog.new(node.name, node.environment)
      allow(Puppet::Parser::Compiler).to receive(:compile).and_return(catalog)

      expect(compiler).not_to receive(:inline_metadata)
      expect(compiler.find(@request)).to eq(catalog)
    end

    it "inlines metadata when the static_catalog option is true, static_catalogs are enabled, and a code_id is provided" do
      allow(Puppet::Node.indirection).to receive(:find).and_return(node)
      @request.options[:static_catalog] = true
      @request.options[:checksum_type] = 'sha256'
      @request.options[:code_id] = 'some_code_id'
      allow(node.environment).to receive(:static_catalogs?).and_return(true)

      catalog = Puppet::Resource::Catalog.new(node.name, node.environment)
      allow(Puppet::Parser::Compiler).to receive(:compile).and_return(catalog)

      expect(compiler).to receive(:inline_metadata).with(catalog, :sha256).and_return(catalog)
      compiler.find(@request)
    end

    it "inlines metadata with the first common checksum type" do
      allow(Puppet::Node.indirection).to receive(:find).and_return(node)
      @request.options[:static_catalog] = true
      @request.options[:checksum_type] = 'atime.md5.sha256.mtime'
      @request.options[:code_id] = 'some_code_id'
      allow(node.environment).to receive(:static_catalogs?).and_return(true)

      catalog = Puppet::Resource::Catalog.new(node.name, node.environment)
      allow(Puppet::Parser::Compiler).to receive(:compile).and_return(catalog)

      expect(compiler).to receive(:inline_metadata).with(catalog, :md5).and_return(catalog)
      compiler.find(@request)
    end

    it "errors if checksum_type contains no shared checksum types" do
      allow(Puppet::Node.indirection).to receive(:find).and_return(node)
      @request.options[:static_catalog] = true
      @request.options[:checksum_type] = 'atime.md2'
      @request.options[:code_id] = 'some_code_id'
      allow(node.environment).to receive(:static_catalogs?).and_return(true)

      expect { compiler.find(@request) }.to raise_error Puppet::Error,
        "Unable to find a common checksum type between agent 'atime.md2' and master '[:sha256, :sha256lite, :md5, :md5lite, :sha1, :sha1lite, :sha512, :sha384, :sha224, :mtime, :ctime, :none]'."
    end

    it "errors if checksum_type contains no shared checksum types" do
      allow(Puppet::Node.indirection).to receive(:find).and_return(node)
      @request.options[:static_catalog] = true
      @request.options[:checksum_type] = nil
      @request.options[:code_id] = 'some_code_id'
      allow(node.environment).to receive(:static_catalogs?).and_return(true)

      expect { compiler.find(@request) }.to raise_error Puppet::Error,
        "Unable to find a common checksum type between agent '' and master '[:sha256, :sha256lite, :md5, :md5lite, :sha1, :sha1lite, :sha512, :sha384, :sha224, :mtime, :ctime, :none]'."
    end

    it "prevents the environment from being evicted during compilation" do
      Puppet[:environment_timeout] = 0

      envs = Puppet.lookup(:environments)

      expect(compiler).to receive(:compile) do
        # we should get the same object
        expect(envs.get!(:production)).to equal(envs.get!(:production))
      end

      compiler.find(@request)
    end

    context 'when checking agent and server specified environments' do
      before :each do
        FileUtils.mkdir_p(File.join(Puppet[:environmentpath], 'env_server'))
        FileUtils.mkdir_p(File.join(Puppet[:environmentpath], 'env_agent'))

        node.environment = 'env_server'
        allow(Puppet::Node.indirection).to receive(:find).and_return(node)

        @request.environment = 'env_agent'
      end

      it 'ignores mismatched environments by default' do
        catalog = compiler.find(@request)

        expect(catalog.environment).to eq('env_server')
        expect(catalog).to have_resource('Stage[main]')
      end

      # versioned environment directories can cause this
      it 'allows environments with the same name but mismatched modulepaths' do
        envs = Puppet.lookup(:environments)
        env_server = envs.get!(:env_server)
        v1_env = env_server.override_with({ modulepath: ['/code-v1/env-v1/'] })
        v2_env = env_server.override_with({ modulepath: ['/code-v2/env-v2/'] })

        @request.options[:check_environment] = "true"
        @request.environment = v1_env
        node.environment = v2_env

        catalog = compiler.find(@request)

        expect(catalog.environment).to eq('env_server')
        expect(catalog).to have_resource('Stage[main]')
      end

      it 'returns an empty catalog if asked to check the environment and they are mismatched' do
        @request.options[:check_environment] = "true"
        catalog = compiler.find(@request)

        expect(catalog.environment).to eq('env_server')
        expect(catalog.resources).to be_empty
      end
    end
  end

  describe "when handling a request with facts" do
    before do
      allow(Facter).to receive(:value).and_return("something")

      facts = Puppet::Node::Facts.new('hostname', "fact" => "value", "architecture" => "i386")
      Puppet::Node::Facts.indirection.save(facts)
    end

    def a_legacy_request_that_contains(facts, format = :pson)
      request = Puppet::Indirector::Request.new(:catalog, :find, "hostname", nil)
      request.options[:facts_format] = format.to_s
      request.options[:facts] = Puppet::Util.uri_query_encode(facts.render(format))
      request
    end

    def a_request_that_contains(facts)
      request = Puppet::Indirector::Request.new(:catalog, :find, "hostname", nil)
      request.options[:facts_format] = "application/json"
      request.options[:facts] = Puppet::Util.uri_query_encode(facts.render('json'))
      request
    end

    context "when extracting facts from the request" do
      let(:facts) { Puppet::Node::Facts.new("hostname") }

      it "should do nothing if no facts are provided" do
        request = Puppet::Indirector::Request.new(:catalog, :find, "hostname", nil)
        request.options[:facts] = nil

        expect(compiler.extract_facts_from_request(request)).to be_nil
      end

      it "should deserialize the facts without changing the timestamp" do
        time = Time.now
        facts.timestamp = time
        request = a_request_that_contains(facts)
        facts = compiler.extract_facts_from_request(request)
        expect(facts.timestamp).to eq(time)
      end

      it "accepts PSON facts from older agents", :if => Puppet.features.pson? do
        request = a_legacy_request_that_contains(facts)

        facts = compiler.extract_facts_from_request(request)
        expect(facts).to eq(facts)
      end

      it "rejects YAML facts" do
        request = a_legacy_request_that_contains(facts, :yaml)

        expect {
          compiler.extract_facts_from_request(request)
        }.to raise_error(ArgumentError, /Unsupported facts format/)
      end

      it "rejects unknown fact formats" do
        request = a_request_that_contains(facts)
        request.options[:facts_format] = 'unknown-format'

        expect {
          compiler.extract_facts_from_request(request)
        }.to raise_error(ArgumentError, /Unsupported facts format/)
      end
    end

    context "when saving facts from the request" do
      let(:facts) { Puppet::Node::Facts.new("hostname") }

      it "should save facts if they were issued by the request" do
        request = a_request_that_contains(facts)

        options = {
          :environment => request.environment,
          :transaction_uuid => request.options[:transaction_uuid],
        }

        expect(Puppet::Node::Facts.indirection).to receive(:save).with(facts, nil, options)
        compiler.find(request)
      end

      it "should skip saving facts if none were supplied" do
        request = Puppet::Indirector::Request.new(:catalog, :find, "hostname", nil)

        options = {
          :environment => request.environment,
          :transaction_uuid => request.options[:transaction_uuid],
        }

        expect(Puppet::Node::Facts.indirection).not_to receive(:save).with(facts, nil, options)
        compiler.find(request)
      end
    end
  end

  describe "when finding nodes" do
    it "should look node information up via the Node class with the provided key" do
      request = Puppet::Indirector::Request.new(:catalog, :find, node_name, nil)
      allow(compiler).to receive(:compile)

      expect(Puppet::Node.indirection).to receive(:find).with(node_name, anything).and_return(node)

      compiler.find(request)
    end

    it "should pass the transaction_uuid to the node indirection" do
      uuid = '793ff10d-89f8-4527-a645-3302cbc749f3'
      allow(compiler).to receive(:compile)
      request = Puppet::Indirector::Request.new(:catalog, :find, node_name,
                                                nil, :transaction_uuid => uuid)

      expect(Puppet::Node.indirection).to receive(:find).with(
        node_name,
        hash_including(:transaction_uuid => uuid)
      ).and_return(node)

      compiler.find(request)
    end

    it "should pass the configured_environment to the node indirection" do
      environment = 'foo'
      allow(compiler).to receive(:compile)
      request = Puppet::Indirector::Request.new(:catalog, :find, node_name,
                                                nil, :configured_environment => environment)

      expect(Puppet::Node.indirection).to receive(:find).with(
        node_name,
        hash_including(:configured_environment => environment)
      ).and_return(node)

      compiler.find(request)
    end

    it "should pass a facts object from the original request facts to the node indirection" do
      facts = Puppet::Node::Facts.new("hostname", :afact => "avalue")
      expect(compiler).to receive(:extract_facts_from_request).and_return(facts)
      expect(compiler).to receive(:save_facts_from_request)

      request = Puppet::Indirector::Request.new(:catalog, :find, "hostname",
                                                nil, :facts_format => "application/json",
                                                :facts => facts.render('json'))

      expect(Puppet::Node.indirection).to receive(:find).with("hostname", hash_including(:facts => facts)).and_return(node)

      compiler.find(request)
    end
  end

  describe "after finding nodes" do
    before do
      allow(Puppet).to receive(:version).and_return(1)
      set_facts({
        'networking.fqdn' => "my.server.com",
        'networking.ip'   => "my.ip.address",
        'networking.ip6'  => nil
        })
      @request = Puppet::Indirector::Request.new(:catalog, :find, node_name, nil)
      allow(compiler).to receive(:compile)
      allow(Puppet::Node.indirection).to receive(:find).with(node_name, anything).and_return(node)
    end

    it "should add the server's Puppet version to the node's parameters as 'serverversion'" do
      expect(node).to receive(:merge).with(hash_including("serverversion" => "1"))
      compiler.find(@request)
    end

    it "should add the server's fqdn to the node's parameters as 'servername'" do
      expect(node).to receive(:merge).with(hash_including("servername" => "my.server.com"))
      compiler.find(@request)
    end

    it "should add the server's IP address to the node's parameters as 'serverip'" do
      expect(node).to receive(:merge).with(hash_including("serverip" => "my.ip.address"))
      compiler.find(@request)
    end

    it "shouldn't warn if there is at least one ip fact" do
      expect(node).to receive(:merge).with(hash_including("serverip" => "my.ip.address"))
      compiler.find(@request)
      expect(@logs).not_to be_any {|log| log.level == :warning and log.message =~ /Could not retrieve either serverip or serverip6 fact/}
    end
  end

  describe "in an IPv6 only environment" do
    before do |example|
      allow(Puppet).to receive(:version).and_return(1)
      set_facts({
        'networking.fqdn' => "my.server.com",
        'networking.ip'   => nil,
      })
      if example.metadata[:nil_ipv6]
        set_facts({
          'networking.ip6' => nil
        })
      else
        set_facts({
          'networking.ip6' => "my.ipv6.address"
        })
      end
      @request = Puppet::Indirector::Request.new(:catalog, :find, node_name, nil)
      allow(compiler).to receive(:compile)
      allow(Puppet::Node.indirection).to receive(:find).with(node_name, anything).and_return(node)
    end

    it "should populate the :serverip6 fact" do
      expect(node).to receive(:merge).with(hash_including("serverip6" => "my.ipv6.address"))
      compiler.find(@request)
    end

    it "shouldn't warn if there is at least one ip fact" do
      expect(node).to receive(:merge).with(hash_including("serverip6" => "my.ipv6.address"))
      compiler.find(@request)
      expect(@logs).not_to be_any {|log| log.level == :warning and log.message =~ /Could not retrieve either serverip or serverip6 fact/}
    end

    it "should warn if there are no ip facts", :nil_ipv6 do
      expect(node).to receive(:merge)
      compiler.find(@request)
      expect(@logs).to be_any {|log| log.level == :warning and log.message =~ /Could not retrieve either serverip or serverip6 fact/}
    end
  end

  describe "after finding nodes when checking for a PE version" do
    include PuppetSpec::Compiler

    let(:pe_version_file) { '/opt/puppetlabs/server/pe_version' }
    let(:request) { Puppet::Indirector::Request.new(:catalog, :find, node_name, nil) }

    before :each do
      Puppet[:code] = 'notify { "PE Version: $pe_serverversion": }'
    end

    it "should not add 'pe_serverversion' when FOSS" do
      allow(Puppet::Node.indirection).to receive(:find).with(node_name, anything).and_return(node)
      expect {
        catalog = compiler.find(request)
        catalog.resource('notify', 'PE Version: 2019.2.0') 
      }.to raise_error(/Evaluation Error: Unknown variable: 'pe_serverversion'./)
    end

    it "should add 'pe_serverversion' when PE" do
      allow(File).to receive(:readable?).and_call_original
      allow(File).to receive(:readable?).with(pe_version_file).and_return(true)
      allow(File).to receive(:zero?).with(pe_version_file).and_return(false)
      allow(File).to receive(:read).and_call_original
      allow(File).to receive(:read).with(pe_version_file).and_return('2019.2.0')

      allow(Puppet::Node.indirection).to receive(:find).with(node_name, anything).and_return(node)
      catalog = compiler.find(request)

      expect(catalog.resource('notify', 'PE Version: 2019.2.0')).to be
    end
  end

  describe "when filtering resources" do
    before :each do
      @catalog = double('catalog')
      allow(@catalog).to receive(:respond_to?).with(:filter).and_return(true)
    end

    it "should delegate to the catalog instance filtering" do
      expect(@catalog).to receive(:filter)
      compiler.filter(@catalog)
    end

    it "should filter out virtual resources" do
      resource = double('resource', :virtual? => true)
      allow(@catalog).to receive(:filter).and_yield(resource)

      compiler.filter(@catalog)
    end

    it "should return the same catalog if it doesn't support filtering" do
      allow(@catalog).to receive(:respond_to?).with(:filter).and_return(false)

      expect(compiler.filter(@catalog)).to eq(@catalog)
    end

    it "should return the filtered catalog" do
      catalog = double('filtered catalog')
      allow(@catalog).to receive(:filter).and_return(catalog)

      expect(compiler.filter(@catalog)).to eq(catalog)
    end
  end

  describe "when inlining metadata" do
    include PuppetSpec::Compiler

    let(:node) { Puppet::Node.new 'me' }
    let(:checksum_type) { 'md5' }
    let(:checksum_value) { 'b1946ac92492d2347c6235b4d2611184' }
    let(:path) { File.expand_path('/foo') }
    let(:source) { 'puppet:///modules/mymodule/config_file.txt' }

    def stubs_resource_metadata(ftype, relative_path, full_path = nil)
      full_path ||=  File.join(Puppet[:environmentpath], 'production', relative_path)

      metadata = double('metadata')
      allow(metadata).to receive(:ftype).and_return(ftype)
      allow(metadata).to receive(:full_path).and_return(full_path)
      allow(metadata).to receive(:relative_path).and_return(relative_path)
      allow(metadata).to receive(:source).and_return("puppet:///#{relative_path}")
      allow(metadata).to receive(:source=)
      allow(metadata).to receive(:content_uri=)

      metadata
    end

    def stubs_file_metadata(checksum_type, sha, relative_path, full_path = nil)
      metadata = stubs_resource_metadata('file', relative_path, full_path)
      allow(metadata).to receive(:checksum).and_return("{#{checksum_type}}#{sha}")
      allow(metadata).to receive(:checksum_type).and_return(checksum_type)
      metadata
    end

    def stubs_link_metadata(relative_path, destination)
      metadata = stubs_resource_metadata('link', relative_path)
      allow(metadata).to receive(:destination).and_return(destination)
      metadata
    end

    def stubs_directory_metadata(relative_path)
      metadata = stubs_resource_metadata('directory', relative_path)
      allow(metadata).to receive(:relative_path).and_return('.')
      metadata
    end

    describe "and the environment is a symlink and versioned_environment_dirs is true" do

      let(:tmpdir) { Dir.mktmpdir }

      before(:each) do
        Puppet[:versioned_environment_dirs] = true
        prod_path = File.join(Puppet[:environmentpath], 'production')
        FileUtils.rm_rf(prod_path)
        FileUtils.symlink(tmpdir, prod_path)
      end

      it "inlines metadata for a file" do
        catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => file,
          source => '#{source}'
        }
      MANIFEST

        module_relative_path = 'modules/mymodule/files/config_file.txt'
        metadata = stubs_file_metadata(checksum_type,
                                       checksum_value,
                                       module_relative_path,
                                       File.join(tmpdir, module_relative_path) )
        expect(metadata).to receive(:source=).with(source)
        expect(metadata).to receive(:content_uri=).with("puppet:///#{module_relative_path}")

        options = {
          :environment => catalog.environment_instance,
          :links => :manage,
          :checksum_type => checksum_type.to_sym,
          :source_permissions => :ignore
        }
        expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(source, options).and_return(metadata)

        compiler.send(:inline_metadata, catalog, checksum_type)


        expect(catalog.metadata[path]).to eq(metadata)
        expect(catalog.recursive_metadata).to be_empty

      end
    end

    it "inlines metadata for a file" do
      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => file,
          source => '#{source}'
        }
      MANIFEST

      metadata = stubs_file_metadata(checksum_type, checksum_value, 'modules/mymodule/files/config_file.txt')
      expect(metadata).to receive(:source=).with(source)
      expect(metadata).to receive(:content_uri=).with('puppet:///modules/mymodule/files/config_file.txt')

      options = {
        :environment => catalog.environment_instance,
        :links => :manage,
        :checksum_type => checksum_type.to_sym,
        :source_permissions => :ignore
      }
      expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(source, options).and_return(metadata)

      compiler.send(:inline_metadata, catalog, checksum_type)

      expect(catalog.metadata[path]).to eq(metadata)
      expect(catalog.recursive_metadata).to be_empty
    end

    it "uses resource parameters when inlining metadata" do
      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => link,
          source => '#{source}',
          links  => follow,
          source_permissions => use,
        }
      MANIFEST

      metadata = stubs_link_metadata('modules/mymodule/files/config_file.txt', '/tmp/some/absolute/path')
      expect(metadata).to receive(:source=).with(source)
      expect(metadata).to receive(:content_uri=).with('puppet:///modules/mymodule/files/config_file.txt')

      options = {
        :environment        => catalog.environment_instance,
        :links              => :follow,
        :checksum_type      => checksum_type.to_sym,
        :source_permissions => :use
      }
      expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(source, options).and_return(metadata)

      compiler.send(:inline_metadata, catalog, checksum_type)

      expect(catalog.metadata[path]).to eq(metadata)
      expect(catalog.recursive_metadata).to be_empty
    end

    it "uses file parameters which match the true file type defaults" do
      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => file,
          source => '#{source}'
        }
      MANIFEST

      if Puppet::Util::Platform.windows?
        default_file = Puppet::Type.type(:file).new(:name => 'C:\defaults')
      else
        default_file = Puppet::Type.type(:file).new(:name => '/defaults')
      end

      metadata = stubs_file_metadata(checksum_type, checksum_value, 'modules/mymodule/files/config_file.txt')

      options = {
        :environment => catalog.environment_instance,
        :links => default_file[:links],
        :checksum_type => checksum_type.to_sym,
        :source_permissions => default_file[:source_permissions]
      }

      expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(source, options).and_return(metadata)

      compiler.send(:inline_metadata, catalog, checksum_type)
    end

    it "inlines metadata for the first source found" do
      alt_source = 'puppet:///modules/files/other.txt'
      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => file,
          source => ['#{alt_source}', '#{source}'],
        }
      MANIFEST

      metadata = stubs_file_metadata(checksum_type, checksum_value, 'modules/mymodule/files/config_file.txt')
      expect(metadata).to receive(:source=).with(source)
      expect(metadata).to receive(:content_uri=).with('puppet:///modules/mymodule/files/config_file.txt')
      expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(source, anything).and_return(metadata)
      expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(alt_source, anything).and_return(nil)

      compiler.send(:inline_metadata, catalog, checksum_type)

      expect(catalog.metadata[path]).to eq(metadata)
      expect(catalog.recursive_metadata).to be_empty
    end

    [['md5', 'b1946ac92492d2347c6235b4d2611184'],
     ['sha256', '5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03']].each do |checksum_type, sha|
      describe "with agent requesting checksum_type #{checksum_type}" do
        it "sets checksum and checksum_value for resources with puppet:// source URIs" do
          catalog = compile_to_catalog(<<-MANIFEST, node)
            file { '#{path}':
              ensure => file,
              source => '#{source}'
            }
          MANIFEST

          metadata = stubs_file_metadata(checksum_type, sha, 'modules/mymodule/files/config_file.txt')

          options = {
            :environment        => catalog.environment_instance,
            :links              => :manage,
            :checksum_type      => checksum_type.to_sym,
            :source_permissions => :ignore
          }
          expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(source, options).and_return(metadata)

          compiler.send(:inline_metadata, catalog, checksum_type)

          expect(catalog.metadata[path]).to eq(metadata)
          expect(catalog.recursive_metadata).to be_empty
        end
      end
    end

    it "preserves source host and port in the content_uri" do
      source = 'puppet://myhost:8888/modules/mymodule/config_file.txt'

      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => file,
          source => '#{source}'
        }
      MANIFEST

      metadata = stubs_file_metadata(checksum_type, checksum_value, 'modules/mymodule/files/config_file.txt')
      allow(metadata).to receive(:source).and_return(source)

      expect(metadata).to receive(:content_uri=).with('puppet://myhost:8888/modules/mymodule/files/config_file.txt')

      expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(source, anything).and_return(metadata)

      compiler.send(:inline_metadata, catalog, checksum_type)
    end

    it "skips absent resources" do
      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => absent,
        }
      MANIFEST

      compiler.send(:inline_metadata, catalog, checksum_type)
      expect(catalog.metadata).to be_empty
      expect(catalog.recursive_metadata).to be_empty
    end

    it "skips resources without a source" do
      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => file,
        }
      MANIFEST

      compiler.send(:inline_metadata, catalog, checksum_type)
      expect(catalog.metadata).to be_empty
      expect(catalog.recursive_metadata).to be_empty
    end

    it "skips resources with a local source" do
      local_source = File.expand_path('/tmp/source')

      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => file,
          source => '#{local_source}',
        }
      MANIFEST

      compiler.send(:inline_metadata, catalog, checksum_type)
      expect(catalog.metadata).to be_empty
      expect(catalog.recursive_metadata).to be_empty
    end

    it "skips resources with a http source" do
      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => file,
          source => ['http://foo.source.io', 'https://foo.source.io']
        }
      MANIFEST

      compiler.send(:inline_metadata, catalog, checksum_type)
      expect(catalog.metadata).to be_empty
      expect(catalog.recursive_metadata).to be_empty
    end

    it "skips resources with a source outside the environment path" do
      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => file,
          source => '#{source}'
        }
      MANIFEST

      full_path = File.join(Puppet[:codedir], "modules/mymodule/files/config_file.txt")
      metadata = stubs_file_metadata(checksum_type, checksum_value, 'modules/mymodule/files/config_file.txt', full_path)
      expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(source, anything).and_return(metadata)

      compiler.send(:inline_metadata, catalog, checksum_type)
      expect(catalog.metadata).to be_empty
      expect(catalog.recursive_metadata).to be_empty
    end

    it "skips resources whose mount point is not 'modules'" do
      source = 'puppet:///secure/data'

      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => file,
          source => '#{source}',
        }
      MANIFEST

      metadata = stubs_file_metadata(checksum_type, checksum_value, 'secure/files/data.txt')
      expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(source, anything).and_return(metadata)

      compiler.send(:inline_metadata, catalog, checksum_type)
      expect(catalog.metadata).to be_empty
      expect(catalog.recursive_metadata).to be_empty
    end

    it "skips resources with 'modules' mount point resolving to a path not in 'modules/*/files'" do
      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => file,
          source => '#{source}',
        }
      MANIFEST

      metadata = stubs_file_metadata(checksum_type, checksum_value, 'modules/mymodule/not_in_files/config_file.txt')
      expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(source, anything).and_return(metadata)

      compiler.send(:inline_metadata, catalog, checksum_type)
      expect(catalog.metadata).to be_empty
      expect(catalog.recursive_metadata).to be_empty
    end

    it "skips resources with 'modules' mount point resolving to a path with an empty module name" do
      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => file,
          source => '#{source}',
        }
      MANIFEST

      # note empty module name "modules//files"
      metadata = stubs_file_metadata(checksum_type, checksum_value, 'modules//files/config_file.txt')
      expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(source, anything).and_return(metadata)

      compiler.send(:inline_metadata, catalog, checksum_type)
      expect(catalog.metadata).to be_empty
      expect(catalog.recursive_metadata).to be_empty
    end

    it "inlines resources in 'modules' mount point resolving to a 'site' directory within the per-environment codedir" do
      # example taken from https://github.com/puppetlabs/control-repo/blob/508b9cc/site/profile/manifests/puppetmaster.pp#L45-L49
      source = 'puppet:///modules/profile/puppetmaster/update-classes.sh'

      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => file,
          source => '#{source}'
        }
      MANIFEST

      # See https://github.com/puppetlabs/control-repo/blob/508b9cc/site/profile/files/puppetmaster/update-classes.sh
      metadata = stubs_file_metadata(checksum_type, checksum_value, 'site/profile/files/puppetmaster/update-classes.sh')
      allow(metadata).to receive(:source).and_return(source)

      expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(source, anything).and_return(metadata)

      compiler.send(:inline_metadata, catalog, checksum_type)
      expect(catalog.metadata[path]).to eq(metadata)
      expect(catalog.recursive_metadata).to be_empty
    end

    # It's bizarre to strip trailing slashes for a file, but it's how
    # puppet currently behaves, so match that.
    it "inlines resources with a trailing slash" do
      source = 'puppet:///modules/mymodule/myfile'

      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { '#{path}':
          ensure => file,
          source => '#{source}/'
        }
      MANIFEST

      metadata = stubs_file_metadata(checksum_type, checksum_value, 'modules/mymodule/files/myfile')
      allow(metadata).to receive(:source).and_return(source)

      expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(source, anything).and_return(metadata)

      compiler.send(:inline_metadata, catalog, checksum_type)
      expect(catalog.metadata[path]).to eq(metadata)
      expect(catalog.recursive_metadata).to be_empty
    end

    describe "when inlining directories" do
      let(:source_dir) { 'puppet:///modules/mymodule/directory' }
      let(:metadata) { stubs_directory_metadata('modules/mymodule/files/directory') }

      describe "when recurse is false" do
        it "skips children" do
          catalog = compile_to_catalog(<<-MANIFEST, node)
            file { '#{path}':
              ensure  => directory,
              source  => '#{source_dir}'
            }
          MANIFEST

          expect(metadata).to receive(:content_uri=).with('puppet:///modules/mymodule/files/directory')
          expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(source_dir, anything).and_return(metadata)

          compiler.send(:inline_metadata, catalog, checksum_type)

          expect(catalog.metadata[path]).to eq(metadata)
          expect(catalog.recursive_metadata).to be_empty
        end
      end

      describe "when recurse is true" do
        let(:child_metadata) { stubs_file_metadata(checksum_type, checksum_value, 'myfile.txt') }

        it "inlines child metadata" do
          catalog = compile_to_catalog(<<-MANIFEST, node)
            file { '#{path}':
              ensure    => directory,
              recurse   => true,
              source    => '#{source_dir}',
              max_files => 1234,
            }
          MANIFEST

          expect(metadata).to receive(:content_uri=).with('puppet:///modules/mymodule/files/directory')
          expect(child_metadata).to receive(:content_uri=).with('puppet:///modules/mymodule/files/directory/myfile.txt')

          options = {
            :environment        => catalog.environment_instance,
            :links              => :manage,
            :checksum_type      => checksum_type.to_sym,
            :source_permissions => :ignore,
            :recurse            => true,
            :recurselimit       => nil,
            :max_files          => 1234,
            :ignore             => nil,
          }
          expect(Puppet::FileServing::Metadata.indirection).to receive(:search).with(source_dir, options).and_return([metadata, child_metadata])

          compiler.send(:inline_metadata, catalog, checksum_type)

          expect(catalog.metadata[path]).to be_nil
          expect(catalog.recursive_metadata[path][source_dir]).to eq([metadata, child_metadata])
        end

        it "uses resource parameters when inlining metadata" do
          catalog = compile_to_catalog(<<-MANIFEST, node)
            file { '#{path}':
              ensure             => directory,
              recurse            => true,
              source             => '#{source_dir}',
              checksum           => sha256,
              source_permissions => use_when_creating,
              recurselimit       => 2,
              max_files          => 4321,
              ignore             => 'foo.+',
              links              => follow,
            }
          MANIFEST

          options = {
            :environment        => catalog.environment_instance,
            :links              => :follow,
            :checksum_type      => :sha256,
            :source_permissions => :use_when_creating,
            :recurse            => true,
            :recurselimit       => 2,
            :max_files          => 4321,
            :ignore             => 'foo.+',
          }
          expect(Puppet::FileServing::Metadata.indirection).to receive(:search).with(source_dir, options).and_return([metadata, child_metadata])

          compiler.send(:inline_metadata, catalog, checksum_type)

          expect(catalog.metadata[path]).to be_nil
          expect(catalog.recursive_metadata[path][source_dir]).to eq([metadata, child_metadata])
        end

        it "inlines metadata for all sources if source_select is all" do
          alt_source_dir = 'puppet:///modules/mymodule/other_directory'
          catalog = compile_to_catalog(<<-MANIFEST, node)
            file { '#{path}':
              ensure  => directory,
              recurse => true,
              source  => ['#{source_dir}', '#{alt_source_dir}'],
              sourceselect => all,
            }
          MANIFEST

          expect(Puppet::FileServing::Metadata.indirection).to receive(:search).with(source_dir, anything).and_return([metadata, child_metadata])
          expect(Puppet::FileServing::Metadata.indirection).to receive(:search).with(alt_source_dir, anything).and_return([metadata, child_metadata])

          compiler.send(:inline_metadata, catalog, checksum_type)

          expect(catalog.metadata[path]).to be_nil
          expect(catalog.recursive_metadata[path][source_dir]).to eq([metadata, child_metadata])
          expect(catalog.recursive_metadata[path][alt_source_dir]).to eq([metadata, child_metadata])
        end

        it "inlines metadata for the first valid source if source_select is first" do
          alt_source_dir = 'puppet:///modules/mymodule/other_directory'
          catalog = compile_to_catalog(<<-MANIFEST, node)
            file { '#{path}':
              ensure  => directory,
              recurse => true,
              source  => ['#{source_dir}', '#{alt_source_dir}'],
            }
          MANIFEST

          expect(Puppet::FileServing::Metadata.indirection).to receive(:search).with(source_dir, anything).and_return(nil)
          expect(Puppet::FileServing::Metadata.indirection).to receive(:search).with(alt_source_dir, anything).and_return([metadata, child_metadata])

          compiler.send(:inline_metadata, catalog, checksum_type)

          expect(catalog.metadata[path]).to be_nil
          expect(catalog.recursive_metadata[path][source_dir]).to be_nil
          expect(catalog.recursive_metadata[path][alt_source_dir]).to eq([metadata, child_metadata])
        end

        it "skips resources whose mount point is not 'modules'" do
          source = 'puppet:///secure/data'

          catalog = compile_to_catalog(<<-MANIFEST, node)
            file { '#{path}':
              ensure  => directory,
              recurse => true,
              source  => '#{source}',
            }
          MANIFEST

          metadata = stubs_directory_metadata('secure/files/data')
          allow(metadata).to receive(:source).and_return(source)

          expect(Puppet::FileServing::Metadata.indirection).to receive(:search).with(source, anything).and_return([metadata])

          compiler.send(:inline_metadata, catalog, checksum_type)
          expect(catalog.metadata).to be_empty
          expect(catalog.recursive_metadata).to be_empty
        end

        it "skips resources with 'modules' mount point resolving to a path not in 'modules/*/files'" do
          source = 'puppet:///modules/mymodule/directory'

          catalog = compile_to_catalog(<<-MANIFEST, node)
            file { '#{path}':
              ensure  => directory,
              recurse => true,
              source  => '#{source}',
            }
          MANIFEST

          metadata = stubs_directory_metadata('modules/mymodule/not_in_files/directory')
          expect(Puppet::FileServing::Metadata.indirection).to receive(:search).with(source, anything).and_return([metadata])

          compiler.send(:inline_metadata, catalog, checksum_type)
          expect(catalog.metadata).to be_empty
          expect(catalog.recursive_metadata).to be_empty
        end

        it "inlines resources in 'modules' mount point resolving to a 'site' directory within the per-environment codedir" do
          # example adopted from https://github.com/puppetlabs/control-repo/blob/508b9cc/site/profile/manifests/puppetmaster.pp#L45-L49
          source = 'puppet:///modules/profile/puppetmaster'

          catalog = compile_to_catalog(<<-MANIFEST, node)
            file { '#{path}':
              ensure  => file,
              recurse => true,
              source  => '#{source}'
            }
          MANIFEST

          # See https://github.com/puppetlabs/control-repo/blob/508b9cc/site/profile/files/puppetmaster/update-classes.sh
          dir_metadata = stubs_directory_metadata('site/profile/files/puppetmaster')
          allow(dir_metadata).to receive(:source).and_return(source)

          child_metadata = stubs_file_metadata(checksum_type, checksum_value, './update-classes.sh')
          allow(child_metadata).to receive(:source).and_return("#{source}/update-classes.sh")

          expect(Puppet::FileServing::Metadata.indirection).to receive(:search).with(source, anything).and_return([dir_metadata, child_metadata])

          compiler.send(:inline_metadata, catalog, checksum_type)
          expect(catalog.metadata).to be_empty
          expect(catalog.recursive_metadata[path][source]).to eq([dir_metadata, child_metadata])
        end

        it "inlines resources with a trailing slash" do
          source = 'puppet:///modules/mymodule/directory'

          catalog = compile_to_catalog(<<-MANIFEST, node)
            file { '#{path}':
              ensure  => directory,
              recurse => true,
              source  => '#{source}/'
            }
          MANIFEST

          dir_metadata = stubs_directory_metadata('modules/mymodule/files/directory')
          allow(dir_metadata).to receive(:source).and_return(source)

          child_metadata = stubs_file_metadata(checksum_type, checksum_value, './file')
          allow(child_metadata).to receive(:source).and_return("#{source}/file")

          expect(Puppet::FileServing::Metadata.indirection).to receive(:search).with(source, anything).and_return([dir_metadata, child_metadata])

          compiler.send(:inline_metadata, catalog, checksum_type)

          expect(catalog.metadata).to be_empty
          expect(catalog.recursive_metadata[path][source]).to eq([dir_metadata, child_metadata])
        end
      end
    end

    it "skips non-file resources" do
      catalog = compile_to_catalog(<<-MANIFEST, node)
        notify { 'hi': }
      MANIFEST

      compiler.send(:inline_metadata, catalog, checksum_type)
      expect(catalog.metadata).to be_empty
      expect(catalog.recursive_metadata).to be_empty
    end

    it "inlines windows file paths", :if => Puppet.features.posix? do
      catalog = compile_to_catalog(<<-MANIFEST, node)
        file { 'c:/foo':
          ensure => file,
          source => '#{source}'
        }
      MANIFEST

      metadata = stubs_file_metadata(checksum_type, checksum_value, 'modules/mymodule/files/config_file.txt')
      expect(Puppet::FileServing::Metadata.indirection).to receive(:find).with(source, anything).and_return(metadata)

      compiler.send(:inline_metadata, catalog, checksum_type)
      expect(catalog.metadata['c:/foo']).to eq(metadata)
      expect(catalog.recursive_metadata).to be_empty
    end
  end
end
