require 'spec_helper'
require 'ostruct'
require 'puppet/settings/errors'
require 'puppet_spec/files'
require 'matchers/resource'

describe Puppet::Settings do
  include PuppetSpec::Files
  include Matchers::Resource

  let(:main_config_file_default_location) do
    File.join(Puppet::Util::RunMode[:server].conf_dir, "puppet.conf")
  end

  let(:user_config_file_default_location) do
    File.join(Puppet::Util::RunMode[:user].conf_dir, "puppet.conf")
  end

  # Return a given object's file metadata.
  def metadata(setting)
    if setting.is_a?(Puppet::Settings::FileSetting)
      {
        :owner => setting.owner,
        :group => setting.group,
        :mode => setting.mode
      }.delete_if { |key, value| value.nil? }
    else
      nil
    end
  end

  describe "when specifying defaults" do
    before do
      @settings = Puppet::Settings.new
    end

    it "should start with no defined sections or parameters" do
      # Note this relies on undocumented side effect that eachsection returns the Settings internal
      # configuration on which keys returns all parameters.
      expect(@settings.eachsection.keys.length).to eq(0)
    end

    it "should not allow specification of default values associated with a section as an array" do
      expect {
        @settings.define_settings(:section, :myvalue => ["defaultval", "my description"])
      }.to raise_error(ArgumentError, /setting definition for 'myvalue' is not a hash!/)
    end

    it "should not allow duplicate parameter specifications" do
      @settings.define_settings(:section, :myvalue => { :default => "a", :desc => "b" })
      expect { @settings.define_settings(:section, :myvalue => { :default => "c", :desc => "d" }) }.to raise_error(ArgumentError)
    end

    it "should allow specification of default values associated with a section as a hash" do
      @settings.define_settings(:section, :myvalue => {:default => "defaultval", :desc => "my description"})
    end

    it "should consider defined parameters to be valid" do
      @settings.define_settings(:section, :myvalue => { :default => "defaultval", :desc => "my description" })
      expect(@settings.valid?(:myvalue)).to be_truthy
    end

    it "should require a description when defaults are specified with a hash" do
      expect { @settings.define_settings(:section, :myvalue => {:default => "a value"}) }.to raise_error(ArgumentError)
    end

    it "should support specifying owner, group, and mode when specifying files" do
      @settings.define_settings(:section, :myvalue => {:type => :file, :default => "/some/file", :owner => "service", :mode => "boo", :group => "service", :desc => "whatever"})
    end

    it "should support specifying a short name" do
      @settings.define_settings(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"})
    end

    it "should support specifying the setting type" do
      @settings.define_settings(:section, :myvalue => {:default => "/w", :desc => "b", :type => :string})
      expect(@settings.setting(:myvalue)).to be_instance_of(Puppet::Settings::StringSetting)
    end

    it "should fail if an invalid setting type is specified" do
      expect { @settings.define_settings(:section, :myvalue => {:default => "w", :desc => "b", :type => :foo}) }.to raise_error(ArgumentError)
    end

    it "should fail when short names conflict" do
      @settings.define_settings(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"})
      expect { @settings.define_settings(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) }.to raise_error(ArgumentError)
    end
  end


  describe "when initializing application defaults do" do
    let(:default_values) do
      values = {}
      PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS.keys.each do |key|
        values[key] = 'default value'
      end
      values
    end

    before do
      @settings = Puppet::Settings.new
      @settings.define_settings(:main, PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS)
    end

    it "should fail if the app defaults hash is missing any required values" do
      expect {
        @settings.initialize_app_defaults(default_values.reject { |key, _| key == :confdir })
      }.to raise_error(Puppet::Settings::SettingsError)
    end

    # ultimately I'd like to stop treating "run_mode" as a normal setting, because it has so many special
    #  case behaviors / uses.  However, until that time... we need to make sure that our private run_mode=
    #  setter method gets properly called during app initialization.
    it "sets the preferred run mode when initializing the app defaults" do
      @settings.initialize_app_defaults(default_values.merge(:run_mode => :server))

      expect(@settings.preferred_run_mode).to eq(:server)
    end

    it "creates ancestor directories for all required app settings" do
      # initialize_app_defaults is called in spec_helper, before we even
      # get here, but call it here to make it explicit what we're trying
      # to do.
      @settings.initialize_app_defaults(default_values)

      Puppet::Settings::REQUIRED_APP_SETTINGS.each do |key|
        expect(File).to exist(File.dirname(Puppet[key]))
      end
    end
  end

  describe "#call_hooks_deferred_to_application_initialization" do
    let(:good_default) { "yay" }
    let(:bad_default) { "$doesntexist" }
    before(:each) do
      @settings = Puppet::Settings.new
    end

    describe "when ignoring dependency interpolation errors" do
      let(:options) { {:ignore_interpolation_dependency_errors => true} }

      describe "if interpolation error" do
        it "should not raise an error" do
          hook_values = []
          @settings.define_settings(:section, :badhook => {:default => bad_default, :desc => "boo", :call_hook => :on_initialize_and_write, :hook => lambda { |v| hook_values << v  }})
          expect do
            @settings.send(:call_hooks_deferred_to_application_initialization, options)
          end.to_not raise_error
        end
      end
      describe "if no interpolation error" do
        it "should not raise an error" do
          hook_values = []
          @settings.define_settings(:section, :goodhook => {:default => good_default, :desc => "boo", :call_hook => :on_initialize_and_write, :hook => lambda { |v| hook_values << v  }})
          expect do
            @settings.send(:call_hooks_deferred_to_application_initialization, options)
          end.to_not raise_error
        end
      end
    end

    describe "when not ignoring dependency interpolation errors" do
      [ {}, {:ignore_interpolation_dependency_errors => false}].each do |options|
        describe "if interpolation error" do
          it "should raise an error" do
            hook_values = []
            @settings.define_settings(
              :section,
              :badhook => {
                :default => bad_default,
                :desc => "boo",
                :call_hook => :on_initialize_and_write,
                :hook => lambda { |v| hook_values << v }
              }
            )
            expect do
              @settings.send(:call_hooks_deferred_to_application_initialization, options)
            end.to raise_error(Puppet::Settings::InterpolationError)
          end
          it "should contain the setting name in error message" do
            hook_values = []
            @settings.define_settings(
              :section,
              :badhook => {
                :default => bad_default,
                :desc => "boo",
                :call_hook => :on_initialize_and_write,
                :hook => lambda { |v| hook_values << v }
              }
            )
            expect do
              @settings.send(:call_hooks_deferred_to_application_initialization, options)
            end.to raise_error(Puppet::Settings::InterpolationError, /badhook/)
          end
        end
        describe "if no interpolation error" do
          it "should not raise an error" do
            hook_values = []
            @settings.define_settings(
              :section,
              :goodhook => {
                :default => good_default,
                :desc => "boo",
                :call_hook => :on_initialize_and_write,
                :hook => lambda { |v| hook_values << v }
              }
            )
            expect do
              @settings.send(:call_hooks_deferred_to_application_initialization, options)
            end.to_not raise_error
          end
        end
      end
    end
  end

  describe "when setting values" do
    before do
      @settings = Puppet::Settings.new
      @settings.define_settings :main, :myval => { :default => "val", :desc => "desc" }
      @settings.define_settings :main, :bool => { :type => :boolean, :default => true, :desc => "desc" }
    end

    it "should provide a method for setting values from other objects" do
      @settings[:myval] = "something else"
      expect(@settings[:myval]).to eq("something else")
    end

    it "should support a getopt-specific mechanism for setting values" do
      @settings.handlearg("--myval", "newval")
      expect(@settings[:myval]).to eq("newval")
    end

    it "should support a getopt-specific mechanism for turning booleans off" do
      @settings.override_default(:bool, true)
      @settings.handlearg("--no-bool", "")
      expect(@settings[:bool]).to eq(false)
    end

    it "should support a getopt-specific mechanism for turning booleans on" do
      # Turn it off first
      @settings.override_default(:bool, false)
      @settings.handlearg("--bool", "")
      expect(@settings[:bool]).to eq(true)
    end

    it "should consider a cli setting with no argument to be a boolean" do
      # Turn it off first
      @settings.override_default(:bool, false)
      @settings.handlearg("--bool")
      expect(@settings[:bool]).to eq(true)
    end

    it "should consider a cli setting with an empty string as an argument to be an empty argument, if the setting itself is not a boolean" do
      @settings.override_default(:myval, "bob")
      @settings.handlearg("--myval", "")
      expect(@settings[:myval]).to eq("")
    end

    it "should consider a cli setting with a boolean as an argument to be a boolean" do
      # Turn it off first
      @settings.override_default(:bool, false)
      @settings.handlearg("--bool", "true")
      expect(@settings[:bool]).to eq(true)
    end

    it "should not consider a cli setting of a non boolean with a boolean as an argument to be a boolean" do
      @settings.override_default(:myval, "bob")
      @settings.handlearg("--no-myval", "")
      expect(@settings[:myval]).to eq("")
    end

    it "should flag string settings from the CLI" do
      @settings.handlearg("--myval", "12")
      expect(@settings.set_by_cli?(:myval)).to be_truthy
    end

    it "should flag bool settings from the CLI" do
      @settings.handlearg("--bool")
      expect(@settings.set_by_cli?(:bool)).to be_truthy
    end

    it "should not flag settings memory as from CLI" do
      @settings[:myval] = "12"
      expect(@settings.set_by_cli?(:myval)).to be_falsey
    end

    it "should find no configured settings by default" do
      expect(@settings.set_by_config?(:myval)).to be_falsey
    end

    it "should identify configured settings in memory" do
      expect(@settings.instance_variable_get(:@value_sets)[:memory]).to receive(:lookup).with(:myval).and_return('foo')
      expect(@settings.set_by_config?(:myval)).to be_truthy
    end

    it "should identify configured settings from CLI" do
      expect(@settings.instance_variable_get(:@value_sets)[:cli]).to receive(:lookup).with(:myval).and_return('foo')
      expect(@settings.set_by_config?(:myval)).to be_truthy
    end

    it "should not identify configured settings from environment by default" do
      expect(Puppet.lookup(:environments)).not_to receive(:get_conf).with(Puppet[:environment].to_sym)
      expect(@settings.set_by_config?(:manifest)).to be_falsey
    end

    it "should identify configured settings from environment by when an environment is specified" do
      foo = double('environment', :manifest => 'foo')
      expect(Puppet.lookup(:environments)).to receive(:get_conf).with(Puppet[:environment].to_sym).and_return(foo)
      expect(@settings.set_by_config?(:manifest, Puppet[:environment])).to be_truthy
    end

    it "should identify configured settings from the preferred run mode" do
      user_config_text = "[#{@settings.preferred_run_mode}]\nmyval = foo"

      allow(Puppet.features).to receive(:root?).and_return(false)
      expect(Puppet::FileSystem).to receive(:exist?).
        with(user_config_file_default_location).
        and_return(true).ordered
      expect(@settings).to receive(:read_file).
        with(user_config_file_default_location).
        and_return(user_config_text).ordered

      @settings.send(:parse_config_files)
      expect(@settings.set_by_config?(:myval)).to be_truthy
    end

    it "should identify configured settings from the specified run mode" do
      user_config_text = "[server]\nmyval = foo"

      allow(Puppet.features).to receive(:root?).and_return(false)
      expect(Puppet::FileSystem).to receive(:exist?).
        with(user_config_file_default_location).
        and_return(true).ordered
      expect(@settings).to receive(:read_file).
        with(user_config_file_default_location).
        and_return(user_config_text).ordered

      @settings.send(:parse_config_files)
      expect(@settings.set_by_config?(:myval, nil, :server)).to be_truthy
    end

    it "should not identify configured settings from an unspecified run mode" do
      user_config_text = "[zaz]\nmyval = foo"

      allow(Puppet.features).to receive(:root?).and_return(false)
      expect(Puppet::FileSystem).to receive(:exist?).
        with(user_config_file_default_location).
        and_return(true).ordered
      expect(@settings).to receive(:read_file).
        with(user_config_file_default_location).
        and_return(user_config_text).ordered

      @settings.send(:parse_config_files)
      expect(@settings.set_by_config?(:myval)).to be_falsey
    end

    it "should identify configured settings from the main section" do
      user_config_text = "[main]\nmyval = foo"

      allow(Puppet.features).to receive(:root?).and_return(false)
      expect(Puppet::FileSystem).to receive(:exist?).
        with(user_config_file_default_location).
        and_return(true).ordered
      expect(@settings).to receive(:read_file).
        with(user_config_file_default_location).
        and_return(user_config_text).ordered

      @settings.send(:parse_config_files)
      expect(@settings.set_by_config?(:myval)).to be_truthy
    end

    it "should clear the cache when setting getopt-specific values" do
      @settings.define_settings :mysection,
          :one => { :default => "whah", :desc => "yay" },
          :two => { :default => "$one yay", :desc => "bah" }
      expect(@settings).to receive(:unsafe_flush_cache)
      expect(@settings[:two]).to eq("whah yay")
      @settings.handlearg("--one", "else")
      expect(@settings[:two]).to eq("else yay")
    end

    it "should clear the cache when the preferred_run_mode is changed" do
      expect(@settings).to receive(:flush_cache)
      @settings.preferred_run_mode = :server
    end

    it "should not clear other values when setting getopt-specific values" do
      @settings[:myval] = "yay"
      @settings.handlearg("--no-bool", "")
      expect(@settings[:myval]).to eq("yay")
    end

    it "should clear the list of used sections" do
      expect(@settings).to receive(:clearused)
      @settings[:myval] = "yay"
    end

    describe "call_hook" do
      Puppet::Settings::StringSetting.available_call_hook_values.each do |val|
        describe "when :#{val}" do
          describe "and definition invalid" do
            it "should raise error if no hook defined" do
              expect do
                @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => val})
              end.to raise_error(ArgumentError, /no :hook/)
            end
            it "should include the setting name in the error message" do
              expect do
                @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => val})
              end.to raise_error(ArgumentError, /for :hooker/)
            end
          end
          describe "and definition valid" do
            before(:each) do
              hook_values = []
              @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => val, :hook => lambda { |v| hook_values << v  }})
            end

            it "should call the hook when value written" do
              expect(@settings.setting(:hooker)).to receive(:handle).with("something").once
              @settings[:hooker] = "something"
            end
          end
        end
      end

      it "should have a default value of :on_write_only" do
        @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| hook_values << v  }})
        expect(@settings.setting(:hooker).call_hook).to eq(:on_write_only)
      end

      describe "when nil" do
        it "should generate a warning" do
          expect(Puppet).to receive(:warning)
          @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => nil, :hook => lambda { |v| hook_values << v  }})
        end
        it "should use default" do
          @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => nil, :hook => lambda { |v| hook_values << v  }})
          expect(@settings.setting(:hooker).call_hook).to eq(:on_write_only)
        end
      end

      describe "when invalid" do
        it "should raise an error" do
          expect do
            @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => :foo, :hook => lambda { |v| hook_values << v  }})
          end.to raise_error(ArgumentError, /invalid.*call_hook/i)
        end
      end

      describe "when :on_define_and_write" do
        it "should call the hook at definition" do
          hook_values = []
          @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => :on_define_and_write, :hook => lambda { |v| hook_values << v  }})
          expect(@settings.setting(:hooker).call_hook).to eq(:on_define_and_write)
          expect(hook_values).to eq(%w{yay})
        end
      end

      describe "when :on_initialize_and_write" do
        before(:each) do
          @hook_values = []
          @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => :on_initialize_and_write, :hook => lambda { |v| @hook_values << v  }})
        end

        it "should not call the hook at definition" do
          expect(@hook_values).to eq([])
          expect(@hook_values).not_to eq(%w{yay})
        end

        it "should call the hook at initialization" do
          app_defaults = {}
          Puppet::Settings::REQUIRED_APP_SETTINGS.each do |key|
            app_defaults[key] = "foo"
          end
          app_defaults[:run_mode] = :user
          @settings.define_settings(:main, PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS)

          expect(@settings.setting(:hooker)).to receive(:handle).with("yay").once

          @settings.initialize_app_defaults app_defaults
        end
      end
    end

    it "should call passed blocks when values are set" do
      values = []
      @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| values << v }})
      expect(values).to eq([])

      @settings[:hooker] = "something"
      expect(values).to eq(%w{something})
    end

    it "should call passed blocks when values are set via the command line" do
      values = []
      @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| values << v }})
      expect(values).to eq([])

      @settings.handlearg("--hooker", "yay")

      expect(values).to eq(%w{yay})
    end

    it "should provide an option to call passed blocks during definition" do
      values = []
      @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => :on_define_and_write, :hook => lambda { |v| values << v }})
      expect(values).to eq(%w{yay})
    end

    it "should pass the fully interpolated value to the hook when called on definition" do
      values = []
      @settings.define_settings(:section, :one => { :default => "test", :desc => "a" })
      @settings.define_settings(:section, :hooker => {:default => "$one/yay", :desc => "boo", :call_hook => :on_define_and_write, :hook => lambda { |v| values << v }})
      expect(values).to eq(%w{test/yay})
    end

    it "should munge values using the setting-specific methods" do
      @settings[:bool] = "false"
      expect(@settings[:bool]).to eq(false)
    end

    it "should prefer values set in ruby to values set on the cli" do
      @settings[:myval] = "memarg"
      @settings.handlearg("--myval", "cliarg")

      expect(@settings[:myval]).to eq("memarg")
    end

    it "should raise an error if we try to set a setting that hasn't been defined'" do
      expect{
        @settings[:why_so_serious] = "foo"
      }.to raise_error(ArgumentError, /unknown setting/)
    end

    it "allows overriding cli args based on the cli-set value" do
      @settings.handlearg("--myval", "cliarg")
      @settings.patch_value(:myval, "modified #{@settings[:myval]}", :cli)
      expect(@settings[:myval]).to eq("modified cliarg")
    end
  end

  describe "when returning values" do
    before do
      @settings = Puppet::Settings.new
      @settings.define_settings :section,
          :config => { :type => :file, :default => "/my/file", :desc => "eh" },
          :one    => { :default => "ONE", :desc => "a" },
          :two    => { :default => "$one TWO", :desc => "b"},
          :three  => { :default => "$one $two THREE", :desc => "c"},
          :four   => { :default => "$two $three FOUR", :desc => "d"},
          :five   => { :default => nil, :desc => "e" },
          :code   => { :default => "", :desc => "my code"}
      allow(Puppet::FileSystem).to receive(:exist?).and_return(true)
    end

    it "should provide a mechanism for returning set values" do
      @settings[:one] = "other"
      expect(@settings[:one]).to eq("other")
    end

    it "setting a value to nil causes it to return to its default" do
      default_values = { :one => "skipped value" }
      [:logdir, :confdir, :codedir, :vardir].each do |key|
        default_values[key] = 'default value'
      end
      @settings.define_settings :main, PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS
      @settings.initialize_app_defaults(default_values)
      @settings[:one] = "value will disappear"

      @settings[:one] = nil

      expect(@settings[:one]).to eq("ONE")
    end

    it "should interpolate default values for other parameters into returned parameter values" do
      expect(@settings[:one]).to eq("ONE")
      expect(@settings[:two]).to eq("ONE TWO")
      expect(@settings[:three]).to eq("ONE ONE TWO THREE")
    end

    it "should interpolate default values that themselves need to be interpolated" do
      expect(@settings[:four]).to eq("ONE TWO ONE ONE TWO THREE FOUR")
    end

    it "should provide a method for returning uninterpolated values" do
      @settings[:two] = "$one tw0"
      expect(@settings.value(:two, nil, true)).to  eq("$one tw0")
      expect(@settings.value(:four, nil, true)).to eq("$two $three FOUR")
    end

    it "should interpolate set values for other parameters into returned parameter values" do
      @settings[:one] = "on3"
      @settings[:two] = "$one tw0"
      @settings[:three] = "$one $two thr33"
      @settings[:four] = "$one $two $three f0ur"
      expect(@settings[:one]).to eq("on3")
      expect(@settings[:two]).to eq("on3 tw0")
      expect(@settings[:three]).to eq("on3 on3 tw0 thr33")
      expect(@settings[:four]).to eq("on3 on3 tw0 on3 on3 tw0 thr33 f0ur")
    end

    it "should not cache interpolated values such that stale information is returned" do
      expect(@settings[:two]).to eq("ONE TWO")
      @settings[:one] = "one"
      expect(@settings[:two]).to eq("one TWO")
    end

    it "should not interpolate the value of the :code setting" do
      @code = @settings.setting(:code)
      expect(@code).not_to receive(:munge)

      expect(@settings[:code]).to eq("")
    end

    it "should have a run_mode that defaults to user" do
      expect(@settings.preferred_run_mode).to eq(:user)
    end

    it "interpolates a boolean false without raising an error" do
      @settings.define_settings(:section,
          :trip_wire => { :type => :boolean, :default => false, :desc => "a trip wire" },
          :tripping => { :default => '$trip_wire', :desc => "once tripped if interpolated was false" })
      expect(@settings[:tripping]).to eq("false")
    end
  end

  describe "when choosing which value to return" do
    before do
      @settings = Puppet::Settings.new
      @settings.define_settings :section,
        :config => { :type => :file, :default => "/my/file", :desc => "a" },
        :one => { :default => "ONE", :desc => "a" },
        :two => { :default => "TWO", :desc => "b" }
      allow(Puppet::FileSystem).to receive(:exist?).and_return(true)
      @settings.preferred_run_mode = :agent
    end

    it "should return default values if no values have been set" do
      expect(@settings[:one]).to eq("ONE")
    end

    it "should return values set on the cli before values set in the configuration file" do
      text = "[main]\none = fileval\n"
      allow(@settings).to receive(:read_file).and_return(text)
      @settings.handlearg("--one", "clival")
      @settings.send(:parse_config_files)

      expect(@settings[:one]).to eq("clival")
    end

    it "should return values set in the mode-specific section before values set in the main section" do
      text = "[main]\none = mainval\n[agent]\none = modeval\n"
      allow(@settings).to receive(:read_file).and_return(text)
      @settings.send(:parse_config_files)

      expect(@settings[:one]).to eq("modeval")
    end

    [:master, :server].each do |run_mode|
      describe "when run mode is '#{run_mode}'" do
        before(:each) { @settings.preferred_run_mode = run_mode }

        it "returns values set in the 'master' section if the 'server' section does not exist" do
          text = "[main]\none = mainval\n[master]\none = modeval\n"
          allow(@settings).to receive(:read_file).and_return(text)
          @settings.send(:parse_config_files)

          expect(@settings[:one]).to eq("modeval")
        end

        it "prioritizes values set in the 'server' section if set" do
          text = "[main]\none = mainval\n[server]\none = serverval\n[master]\none = masterval\n"
          allow(@settings).to receive(:read_file).and_return(text)
          @settings.send(:parse_config_files)

          expect(@settings[:one]).to eq("serverval")
        end
      end
    end

    it "should not return values outside of its search path" do
      text = "[other]\none = oval\n"
      allow(@settings).to receive(:read_file).and_return(text)
      @settings.send(:parse_config_files)
      expect(@settings[:one]).to eq("ONE")
    end

    it 'should use the current environment for $environment' do
      @settings.define_settings :main, :config_version => { :default => "$environment/foo", :desc => "mydocs" }

      expect(@settings.value(:config_version, "myenv")).to eq("myenv/foo")
    end
  end

  describe "when locating config files" do
    before do
      @settings = Puppet::Settings.new
    end

    describe "when root" do
      it "should look for the main config file default location config settings haven't been overridden'" do
        allow(Puppet.features).to receive(:root?).and_return(true)
        expect(Puppet::FileSystem).to receive(:exist?).with(main_config_file_default_location).and_return(false)
        expect(Puppet::FileSystem).not_to receive(:exist?).with(user_config_file_default_location)

        @settings.send(:parse_config_files)
      end
    end

    describe "when not root" do
      it "should look for user config file default location if config settings haven't been overridden'" do
        allow(Puppet.features).to receive(:root?).and_return(false)

        expect(Puppet::FileSystem).to receive(:exist?).with(user_config_file_default_location).and_return(false)

        @settings.send(:parse_config_files)
      end
    end

    describe "when the file exists" do
      it "fails if the file is not readable" do
        expect(Puppet::FileSystem).to receive(:exist?).with(user_config_file_default_location).and_return(true)
        expect(@settings).to receive(:read_file).and_raise('Permission denied')

        expect{ @settings.send(:parse_config_files) }.to raise_error(RuntimeError, /Could not load #{user_config_file_default_location}: Permission denied/)
      end

      it "does not fail if the file is not readable and when `require_config` is false" do
        expect(Puppet::FileSystem).to receive(:exist?).with(user_config_file_default_location).and_return(true)
        expect(@settings).to receive(:read_file).and_raise('Permission denied')

        expect(@settings).not_to receive(:parse_config)
        expect(Puppet).to receive(:log_exception)

        expect{ @settings.send(:parse_config_files, false) }.not_to raise_error
      end

      it "reads the file if it is readable" do
        expect(Puppet::FileSystem).to receive(:exist?).with(user_config_file_default_location).and_return(true)
        expect(@settings).to receive(:read_file).and_return('server = host.string')
        expect(@settings).to receive(:parse_config)

        @settings.send(:parse_config_files)
      end
    end

    describe "when the file does not exist" do
      it "does not attempt to parse the config file" do
        expect(Puppet::FileSystem).to receive(:exist?).with(user_config_file_default_location).and_return(false)
        expect(@settings).not_to receive(:parse_config)

        @settings.send(:parse_config_files)
      end
    end
  end

  describe "when parsing its configuration" do
    before do
      @settings = Puppet::Settings.new
      allow(@settings).to receive(:service_user_available?).and_return(true)
      allow(@settings).to receive(:service_group_available?).and_return(true)
      @file = make_absolute("/some/file")
      @userconfig = make_absolute("/test/userconfigfile")
      @settings.define_settings :section, :user => { :default => "suser", :desc => "doc" }, :group => { :default => "sgroup", :desc => "doc" }
      @settings.define_settings :section,
          :config => { :type => :file, :default => @file, :desc => "eh" },
          :one => { :default => "ONE", :desc => "a" },
          :two => { :default => "$one TWO", :desc => "b" },
          :three => { :default => "$one $two THREE", :desc => "c" }
      allow(@settings).to receive(:user_config_file).and_return(@userconfig)
      allow(Puppet::FileSystem).to receive(:exist?).with(@file).and_return(true)
      allow(Puppet::FileSystem).to receive(:exist?).with(@userconfig).and_return(false)
    end

    it "should not ignore the report setting" do
      @settings.define_settings :section, :report => { :default => "false", :desc => "a" }
      # This is needed in order to make sure we pass on windows
      myfile = File.expand_path(@file)
      @settings[:config] = myfile
      text = <<-CONF
        [puppetd]
          report=true
      CONF
      expect(Puppet::FileSystem).to receive(:exist?).with(myfile).and_return(true)
      expect(@settings).to receive(:read_file).and_return(text)
      @settings.send(:parse_config_files)
      expect(@settings[:report]).to be_truthy
    end

    it "should use its current ':config' value for the file to parse" do
      myfile = make_absolute("/my/file")
      @settings[:config] = myfile

      expect(Puppet::FileSystem).to receive(:exist?).with(myfile).and_return(true)

      expect(Puppet::FileSystem).to receive(:read).with(myfile, :encoding => 'utf-8').and_return("[main]")

      @settings.send(:parse_config_files)
    end

    it "should not try to parse non-existent files" do
      expect(Puppet::FileSystem).to receive(:exist?).with(@file).and_return(false)

      expect(File).not_to receive(:read).with(@file)

      @settings.send(:parse_config_files)
    end

    it "should return values set in the configuration file" do
      text = "[main]
      one = fileval
      "
      expect(@settings).to receive(:read_file).and_return(text)
      @settings.send(:parse_config_files)
      expect(@settings[:one]).to eq("fileval")
    end

    #484 - this should probably be in the regression area
    it "should not throw an exception on unknown parameters" do
      text = "[main]\nnosuchparam = mval\n"
      expect(@settings).to receive(:read_file).and_return(text)
      expect { @settings.send(:parse_config_files) }.not_to raise_error
    end

    it "should convert booleans in the configuration file into Ruby booleans" do
      text = "[main]
      one = true
      two = false
      "
      expect(@settings).to receive(:read_file).and_return(text)
      @settings.send(:parse_config_files)
      expect(@settings[:one]).to eq(true)
      expect(@settings[:two]).to eq(false)
    end

    it "should convert integers in the configuration file into Ruby Integers" do
      text = "[main]
      one = 65
      "
      expect(@settings).to receive(:read_file).and_return(text)
      @settings.send(:parse_config_files)
      expect(@settings[:one]).to eq(65)
    end

    it "should support specifying all metadata (owner, group, mode) in the configuration file" do
      @settings.define_settings :section, :myfile => { :type => :file, :default => make_absolute("/myfile"), :desc => "a" }

      otherfile = make_absolute("/other/file")
      @settings.parse_config(<<-CONF)
      [main]
      myfile = #{otherfile} {owner = service, group = service, mode = 644}
      CONF

      expect(@settings[:myfile]).to eq(otherfile)
      expect(metadata(@settings.setting(:myfile))).to eq({:owner => "suser", :group => "sgroup", :mode => "644"})
    end

    it "should support specifying a single piece of metadata (owner, group, or mode) in the configuration file" do
      @settings.define_settings :section, :myfile => { :type => :file, :default => make_absolute("/myfile"), :desc => "a" }

      otherfile = make_absolute("/other/file")
      @settings.parse_config(<<-CONF)
      [main]
      myfile = #{otherfile} {owner = service}
      CONF

      expect(@settings[:myfile]).to eq(otherfile)
      expect(metadata(@settings.setting(:myfile))).to eq({:owner => "suser"})
    end

    it "should support loading metadata (owner, group, or mode) from a run_mode section in the configuration file" do
      default_values = {}
      PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS.keys.each do |key|
        default_values[key] = 'default value'
      end
      @settings.define_settings :main, PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS
      @settings.define_settings :server, :myfile => { :type => :file, :default => make_absolute("/myfile"), :desc => "a" }

      otherfile = make_absolute("/other/file")
      text = "[server]
      myfile = #{otherfile} {mode = 664}
      "
      expect(@settings).to receive(:read_file).and_return(text)

      # will start initialization as user
      expect(@settings.preferred_run_mode).to eq(:user)
      @settings.send(:parse_config_files)

      # change app run_mode to server
      @settings.initialize_app_defaults(default_values.merge(:run_mode => :server))
      expect(@settings.preferred_run_mode).to eq(:server)

      # initializing the app should have reloaded the metadata based on run_mode
      expect(@settings[:myfile]).to eq(otherfile)
      expect(metadata(@settings.setting(:myfile))).to eq({:mode => "664"})
    end

    it "does not use the metadata from the same setting in a different section" do
      default_values = {}
      PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS.keys.each do |key|
        default_values[key] = 'default value'
      end

      file = make_absolute("/file")
      default_mode = "0600"
      @settings.define_settings :main, PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS
      @settings.define_settings :server, :myfile => { :type => :file, :default => file, :desc => "a", :mode => default_mode }

      text = "[server]
      myfile = #{file}/foo
      [agent]
      myfile = #{file} {mode = 664}
      "
      expect(@settings).to receive(:read_file).and_return(text)

      # will start initialization as user
      expect(@settings.preferred_run_mode).to eq(:user)
      @settings.send(:parse_config_files)

      # change app run_mode to server
      @settings.initialize_app_defaults(default_values.merge(:run_mode => :server))
      expect(@settings.preferred_run_mode).to eq(:server)

      # initializing the app should have reloaded the metadata based on run_mode
      expect(@settings[:myfile]).to eq("#{file}/foo")
      expect(metadata(@settings.setting(:myfile))).to eq({ :mode => default_mode })
    end

    it "should call hooks associated with values set in the configuration file" do
      values = []
      @settings.define_settings :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }}

      text = "[main]
      mysetting = setval
      "
      expect(@settings).to receive(:read_file).and_return(text)
      @settings.send(:parse_config_files)
      expect(values).to eq(["setval"])
    end

    it "should not call the same hook for values set multiple times in the configuration file" do
      values = []
      @settings.define_settings :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }}

      text = "[user]
      mysetting = setval
      [main]
      mysetting = other
      "
      expect(@settings).to receive(:read_file).and_return(text)
      @settings.send(:parse_config_files)
      expect(values).to eq(["setval"])
    end

    it "should pass the interpolated value to the hook when one is available" do
      values = []
      @settings.define_settings :section, :base => {:default => "yay", :desc => "a", :hook => proc { |v| values << v }}
      @settings.define_settings :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }}

      text = "[main]
      mysetting = $base/setval
      "
      expect(@settings).to receive(:read_file).and_return(text)
      @settings.send(:parse_config_files)
      expect(values).to eq(["yay/setval"])
    end

    it "should allow hooks invoked at parse time to be deferred" do
      hook_invoked = false
      @settings.define_settings :section, :deferred  => {:desc => '',
                                                         :hook => proc { |v| hook_invoked = true },
                                                         :call_hook => :on_initialize_and_write, }

      @settings.define_settings(:main,
        :logdir       => { :type => :directory, :default => nil, :desc => "logdir" },
        :confdir      => { :type => :directory, :default => nil, :desc => "confdir" },
        :codedir      => { :type => :directory, :default => nil, :desc => "codedir" },
        :vardir       => { :type => :directory, :default => nil, :desc => "vardir" })

      text = <<-EOD
      [main]
      deferred=$confdir/goose
      EOD

      allow(@settings).to receive(:read_file).and_return(text)
      @settings.initialize_global_settings

      expect(hook_invoked).to be_falsey

      @settings.initialize_app_defaults(:logdir => '/path/to/logdir', :confdir => '/path/to/confdir', :vardir => '/path/to/vardir', :codedir => '/path/to/codedir')

      expect(hook_invoked).to be_truthy
      expect(@settings[:deferred]).to eq(File.expand_path('/path/to/confdir/goose'))
    end

    it "does not require the value for a setting without a hook to resolve during global setup" do
      @settings.define_settings :section, :can_cause_problems  => {:desc => '' }

      @settings.define_settings(:main,
        :logdir       => { :type => :directory, :default => nil, :desc => "logdir" },
        :confdir      => { :type => :directory, :default => nil, :desc => "confdir" },
        :codedir      => { :type => :directory, :default => nil, :desc => "codedir" },
        :vardir       => { :type => :directory, :default => nil, :desc => "vardir" })

      text = <<-EOD
      [main]
      can_cause_problems=$confdir/goose
      EOD

      allow(@settings).to receive(:read_file).and_return(text)
      @settings.initialize_global_settings
      @settings.initialize_app_defaults(:logdir => '/path/to/logdir', :confdir => '/path/to/confdir', :vardir => '/path/to/vardir', :codedir => '/path/to/codedir')

      expect(@settings[:can_cause_problems]).to eq(File.expand_path('/path/to/confdir/goose'))
    end

    it "should allow empty values" do
      @settings.define_settings :section, :myarg => { :default => "myfile", :desc => "a" }

      text = "[main]
      myarg =
      "
      allow(@settings).to receive(:read_file).and_return(text)
      @settings.send(:parse_config_files)
      expect(@settings[:myarg]).to eq("")
    end

    describe "deprecations" do
      let(:settings) { Puppet::Settings.new }
      let(:app_defaults) {
        {
          :logdir     => "/dev/null",
          :confdir    => "/dev/null",
          :codedir    => "/dev/null",
          :vardir     => "/dev/null",
        }
      }

      def assert_accessing_setting_is_deprecated(settings, setting)
        expect(Puppet).to receive(:deprecation_warning).with("Accessing '#{setting}' as a setting is deprecated.")
        expect(Puppet).to receive(:deprecation_warning).with("Modifying '#{setting}' as a setting is deprecated.")
        settings[setting.intern] = apath = File.expand_path('foo')
        expect(settings[setting.intern]).to eq(apath)
      end

      before(:each) do
        settings.define_settings(:main, {
          :logdir => { :default => 'a', :desc => 'a' },
          :confdir => { :default => 'b', :desc => 'b' },
          :vardir => { :default => 'c', :desc => 'c' },
          :codedir => { :default => 'd', :desc => 'd' },
        })
      end

      context "complete" do
        let(:completely_deprecated_settings) do
          settings.define_settings(:main, {
            :completely_deprecated_setting => {
              :default => 'foo',
              :desc    => 'a deprecated setting',
              :deprecated => :completely,
            }
          })
          settings
        end

        it "warns when set in puppet.conf" do
          expect(Puppet).to receive(:deprecation_warning).with(/completely_deprecated_setting is deprecated\./, 'setting-completely_deprecated_setting')

          completely_deprecated_settings.parse_config(<<-CONF)
            completely_deprecated_setting='should warn'
          CONF
          completely_deprecated_settings.initialize_app_defaults(app_defaults)
        end

        it "warns when set on the commandline" do
          expect(Puppet).to receive(:deprecation_warning).with(/completely_deprecated_setting is deprecated\./, 'setting-completely_deprecated_setting')

          args = ["--completely_deprecated_setting", "/some/value"]
          completely_deprecated_settings.send(:parse_global_options, args)
          completely_deprecated_settings.initialize_app_defaults(app_defaults)
        end

        it "warns when set in code" do
          assert_accessing_setting_is_deprecated(completely_deprecated_settings, 'completely_deprecated_setting')
        end
      end

      context "partial" do
        let(:partially_deprecated_settings) do
          settings.define_settings(:main, {
            :partially_deprecated_setting => {
              :default => 'foo',
              :desc    => 'a partially deprecated setting',
              :deprecated => :allowed_on_commandline,
            }
          })
          settings
        end

        it "warns for a deprecated setting allowed on the command line set in puppet.conf" do
          expect(Puppet).to receive(:deprecation_warning).with(/partially_deprecated_setting is deprecated in puppet\.conf/, 'puppet-conf-setting-partially_deprecated_setting')
          partially_deprecated_settings.parse_config(<<-CONF)
            partially_deprecated_setting='should warn'
          CONF
          partially_deprecated_settings.initialize_app_defaults(app_defaults)
        end

        it "does not warn when manifest is set on command line" do
          expect(Puppet).not_to receive(:deprecation_warning)

          args = ["--partially_deprecated_setting", "/some/value"]
          partially_deprecated_settings.send(:parse_global_options, args)
          partially_deprecated_settings.initialize_app_defaults(app_defaults)
        end

        it "warns when set in code" do
          assert_accessing_setting_is_deprecated(partially_deprecated_settings, 'partially_deprecated_setting')
        end
      end
    end
  end

  describe "when there are multiple config files" do
    let(:main_config_text) { "[main]\none = main\ntwo = main2" }
    let(:user_config_text) { "[main]\none = user\n" }

    before :each do
      @settings = Puppet::Settings.new
      @settings.define_settings(:section,
          { :confdir => { :default => nil,                    :desc => "Conf dir" },
            :config  => { :default => "$confdir/puppet.conf", :desc => "Config" },
            :one     => { :default => "ONE",                  :desc => "a" },
            :two     => { :default => "TWO",                  :desc => "b" }, })
    end

    context "running non-root without explicit config file" do
      before :each do
        allow(Puppet.features).to receive(:root?).and_return(false)
        expect(Puppet::FileSystem).to receive(:exist?).
          with(user_config_file_default_location).
          and_return(true).ordered
        expect(@settings).to receive(:read_file).
          with(user_config_file_default_location).
          and_return(user_config_text).ordered
      end

      it "should return values from the user config file" do
        @settings.send(:parse_config_files)
        expect(@settings[:one]).to eq("user")
      end

      it "should not return values from the main config file" do
        @settings.send(:parse_config_files)
        expect(@settings[:two]).to eq("TWO")
      end
    end

    context "running as root without explicit config file" do
      before :each do
        allow(Puppet.features).to receive(:root?).and_return(true)
        expect(Puppet::FileSystem).to receive(:exist?).
          with(main_config_file_default_location).
          and_return(true).ordered
        expect(@settings).to receive(:read_file).
          with(main_config_file_default_location).
          and_return(main_config_text).ordered
      end

      it "should return values from the main config file" do
        @settings.send(:parse_config_files)
        expect(@settings[:one]).to eq("main")
      end

      it "should not return values from the user config file" do
        @settings.send(:parse_config_files)
        expect(@settings[:two]).to eq("main2")
      end
    end

    context "running with an explicit config file as a user (e.g. Apache + Passenger)" do
      before :each do
        allow(Puppet.features).to receive(:root?).and_return(false)
        @settings[:confdir] = File.dirname(main_config_file_default_location)
        expect(Puppet::FileSystem).to receive(:exist?).
          with(main_config_file_default_location).
          and_return(true).ordered
        expect(@settings).to receive(:read_file).
          with(main_config_file_default_location).
          and_return(main_config_text).ordered
      end

      it "should return values from the main config file" do
        @settings.send(:parse_config_files)
        expect(@settings[:one]).to eq("main")
      end

      it "should not return values from the user config file" do
        @settings.send(:parse_config_files)
        expect(@settings[:two]).to eq("main2")
      end
    end
  end

  describe "when reparsing its configuration" do
    before do
      @file = make_absolute("/test/file")
      @userconfig = make_absolute("/test/userconfigfile")
      @settings = Puppet::Settings.new
      @settings.define_settings :section,
          :config => { :type => :file, :default => @file, :desc => "a" },
          :one => { :default => "ONE", :desc => "a" },
          :two => { :default => "$one TWO", :desc => "b" },
          :three => { :default => "$one $two THREE", :desc => "c" }
      allow(Puppet::FileSystem).to receive(:exist?).with(@file).and_return(true)
      allow(Puppet::FileSystem).to receive(:exist?).with(@userconfig).and_return(false)
      allow(@settings).to receive(:user_config_file).and_return(@userconfig)
    end

    it "does not create the WatchedFile instance and should not parse if the file does not exist" do
      expect(Puppet::FileSystem).to receive(:exist?).with(@file).and_return(false)
      expect(Puppet::Util::WatchedFile).not_to receive(:new)

      expect(@settings).not_to receive(:parse_config_files)

      @settings.reparse_config_files
    end

    context "and watched file exists" do
      before do
        @watched_file = Puppet::Util::WatchedFile.new(@file)
        expect(Puppet::Util::WatchedFile).to receive(:new).with(@file).and_return(@watched_file)
      end

      it "uses a WatchedFile instance to determine if the file has changed" do
        expect(@watched_file).to receive(:changed?)

        @settings.reparse_config_files
      end

      it "does not reparse if the file has not changed" do
        expect(@watched_file).to receive(:changed?).and_return(false)

        expect(@settings).not_to receive(:parse_config_files)

        @settings.reparse_config_files
      end

      it "reparses if the file has changed" do
        expect(@watched_file).to receive(:changed?).and_return(true)

        expect(@settings).to receive(:parse_config_files)

        @settings.reparse_config_files
      end

      it "replaces in-memory values with on-file values" do
        allow(@watched_file).to receive(:changed?).and_return(true)
        @settings[:one] = "init"

        # Now replace the value
        text = "[main]\none = disk-replace\n"
        allow(@settings).to receive(:read_file).and_return(text)
        @settings.reparse_config_files
        expect(@settings[:one]).to eq("disk-replace")
      end
    end

    it "should retain parameters set by cli when configuration files are reparsed" do
      @settings.handlearg("--one", "clival")

      text = "[main]\none = on-disk\n"
      allow(@settings).to receive(:read_file).and_return(text)
      @settings.send(:parse_config_files)

      expect(@settings[:one]).to eq("clival")
    end

    it "should remove in-memory values that are no longer set in the file" do
      # Init the value
      text = "[main]\none = disk-init\n"
      expect(@settings).to receive(:read_file).and_return(text)
      @settings.send(:parse_config_files)
      expect(@settings[:one]).to eq("disk-init")

      # Now replace the value
      text = "[main]\ntwo = disk-replace\n"
      expect(@settings).to receive(:read_file).and_return(text)
      @settings.send(:parse_config_files)

      # The originally-overridden value should be replaced with the default
      expect(@settings[:one]).to eq("ONE")

      # and we should now have the new value in memory
      expect(@settings[:two]).to eq("disk-replace")
    end

    it "should retain in-memory values if the file has a syntax error" do
      # Init the value
      text = "[main]\none = initial-value\n"
      expect(@settings).to receive(:read_file).with(@file).and_return(text)
      @settings.send(:parse_config_files)
      expect(@settings[:one]).to eq("initial-value")

      # Now replace the value with something bogus
      text = "[main]\nkenny = killed-by-what-follows\n1 is 2, blah blah florp\n"
      expect(@settings).to receive(:read_file).with(@file).and_return(text)
      @settings.send(:parse_config_files)

      # The originally-overridden value should not be replaced with the default
      expect(@settings[:one]).to eq("initial-value")

      # and we should not have the new value in memory
      expect(@settings[:kenny]).to be_nil
    end
  end

  it "should provide a method for creating a catalog of resources from its configuration" do
    expect(Puppet::Settings.new).to respond_to(:to_catalog)
  end

  describe "when creating a catalog" do
    before do
      @settings = Puppet::Settings.new
      allow(@settings).to receive(:service_user_available?).and_return(true)
      @prefix = Puppet.features.posix? ? "" : "C:"
    end

    it "should add all file resources to the catalog if no sections have been specified" do
      @settings.define_settings :main,
          :maindir => { :type => :directory, :default => @prefix+"/maindir", :desc => "a"},
          :seconddir => { :type => :directory, :default => @prefix+"/seconddir", :desc => "a"}
      @settings.define_settings :other,
          :otherdir => { :type => :directory, :default => @prefix+"/otherdir", :desc => "a" }

      catalog = @settings.to_catalog

      [@prefix+"/maindir", @prefix+"/seconddir", @prefix+"/otherdir"].each do |path|
        expect(catalog.resource(:file, path)).to be_instance_of(Puppet::Resource)
      end
    end

    it "should add only files in the specified sections if section names are provided" do
      @settings.define_settings :main, :maindir => { :type => :directory, :default => @prefix+"/maindir", :desc => "a" }
      @settings.define_settings :other, :otherdir => { :type => :directory, :default => @prefix+"/otherdir", :desc => "a" }
      catalog = @settings.to_catalog(:main)
      expect(catalog.resource(:file, @prefix+"/otherdir")).to be_nil
      expect(catalog.resource(:file, @prefix+"/maindir")).to be_instance_of(Puppet::Resource)
    end

    it "should not try to add the same file twice" do
      @settings.define_settings :main, :maindir => { :type => :directory, :default => @prefix+"/maindir", :desc => "a" }
      @settings.define_settings :other, :otherdir => { :type => :directory, :default => @prefix+"/maindir", :desc => "a" }
      expect { @settings.to_catalog }.not_to raise_error
    end

    it "should ignore files whose :to_resource method returns nil" do
      @settings.define_settings :main, :maindir => { :type => :directory, :default => @prefix+"/maindir", :desc => "a" }
      expect(@settings.setting(:maindir)).to receive(:to_resource).and_return(nil)

      expect_any_instance_of(Puppet::Resource::Catalog).not_to receive(:add_resource)
      @settings.to_catalog
    end

    describe "on Microsoft Windows", :if => Puppet::Util::Platform.windows? do
      before :each do
        allow(Puppet.features).to receive(:root?).and_return(true)

        @settings.define_settings :foo,
            :mkusers => { :type => :boolean, :default => true, :desc => "e" },
            :user => { :default => "suser", :desc => "doc" },
            :group => { :default => "sgroup", :desc => "doc" }
        @settings.define_settings :other,
            :otherdir => { :type => :directory, :default => "/otherdir", :desc => "a", :owner => "service", :group => "service"}

        @catalog = @settings.to_catalog
      end

      it "it should not add users and groups to the catalog" do
        expect(@catalog.resource(:user, "suser")).to be_nil
        expect(@catalog.resource(:group, "sgroup")).to be_nil
      end
    end

    describe "adding default directory environment to the catalog" do
      let(:tmpenv) { tmpdir("envs") }
      let(:default_path) { "#{tmpenv}/environments" }
      before(:each) do
        @settings.define_settings :main,
          :environment     => { :default => "production", :desc => "env"},
          :environmentpath => { :type => :path, :default => default_path, :desc => "envpath"}
      end

      it "adds if environmentpath exists" do
        envpath = "#{tmpenv}/custom_envpath"
        @settings[:environmentpath] = envpath
        Dir.mkdir(envpath)
        catalog = @settings.to_catalog
        expect(catalog.resource_keys).to include(["File", "#{envpath}/production"])
      end

      it "adds the first directory of environmentpath" do
        envdir = "#{tmpenv}/custom_envpath"
        envpath = "#{envdir}#{File::PATH_SEPARATOR}/some/other/envdir"
        @settings[:environmentpath] = envpath
        Dir.mkdir(envdir)
        catalog = @settings.to_catalog
        expect(catalog.resource_keys).to include(["File", "#{envdir}/production"])
      end

      it 'adds the creation of the production directory when not run as root' do
        envdir = "#{tmpenv}/custom_envpath"
        envpath = "#{envdir}#{File::PATH_SEPARATOR}/some/other/envdir"
        @settings[:environmentpath] = envpath
        Dir.mkdir(envdir)
        allow(Puppet.features).to receive(:root?).and_return(false)
        catalog = @settings.to_catalog
        resource = catalog.resource('File', File.join(envdir, 'production'))
        expect(resource[:mode]).to eq('0750')
        expect(resource[:owner]).to be_nil
        expect(resource[:group]).to be_nil
      end

      it 'adds the creation of the production directory with service owner and group information when available' do
        envdir = "#{tmpenv}/custom_envpath"
        envpath = "#{envdir}#{File::PATH_SEPARATOR}/some/other/envdir"
        @settings[:environmentpath] = envpath
        Dir.mkdir(envdir)
        allow(Puppet.features).to receive(:root?).and_return(true)
        allow(@settings).to receive(:service_user_available?).and_return(true)
        allow(@settings).to receive(:service_group_available?).and_return(true)
        catalog = @settings.to_catalog
        resource = catalog.resource('File', File.join(envdir, 'production'))
        expect(resource[:mode]).to eq('0750')
        expect(resource[:owner]).to eq('puppet')
        expect(resource[:group]).to eq('puppet')
      end

      it 'adds the creation of the production directory without service owner and group when not available' do
        envdir = "#{tmpenv}/custom_envpath"
        envpath = "#{envdir}#{File::PATH_SEPARATOR}/some/other/envdir"
        @settings[:environmentpath] = envpath
        Dir.mkdir(envdir)
        allow(Puppet.features).to receive(:root?).and_return(true)
        allow(@settings).to receive(:service_user_available?).and_return(false)
        allow(@settings).to receive(:service_group_available?).and_return(false)
        catalog = @settings.to_catalog
        resource = catalog.resource('File', File.join(envdir, 'production'))
        expect(resource[:mode]).to eq('0750')
        expect(resource[:owner]).to be_nil
        expect(resource[:group]).to be_nil
      end

      it "handles a non-existent environmentpath" do
        catalog = @settings.to_catalog
        expect(catalog.resource_keys).to be_empty
      end

      it "handles a default environmentpath" do
        Dir.mkdir(default_path)
        catalog = @settings.to_catalog
        expect(catalog.resource_keys).to include(["File", "#{default_path}/production"])
      end

      it "does not add if the path to the default directory environment exists as a symlink", :if => Puppet.features.manages_symlinks? do
        Dir.mkdir(default_path)
        Puppet::FileSystem.symlink("#{tmpenv}/nowhere", File.join(default_path, 'production'))
        catalog = @settings.to_catalog
        expect(catalog.resource_keys).to_not include(["File", "#{default_path}/production"])
      end
    end

    describe "when adding users and groups to the catalog" do
      before :all do
        # when this spec is run in isolation to build a settings catalog
        # it will not be able to autorequire and load types for the first time
        # on Windows with windows? stubbed to false, because
        # Puppet::Util.path_to_uri is called to generate a URI to load code
        # and it manipulates the path based on OS
        # so instead we forcefully "prime" the cached types
        Puppet::Type.type(:user).new(:name => 'foo')
        Puppet::Type.type(:group).new(:name => 'bar')
        Puppet::Type.type(:file).new(:name => Dir.pwd) # appropriate for OS
      end

      before do
        allow(Puppet.features).to receive(:root?).and_return(true)
        # stubbed to false, as Windows catalogs don't add users / groups
        allow(Puppet::Util::Platform).to receive(:windows?).and_return(false)

        @settings.define_settings :foo,
            :mkusers => { :type => :boolean, :default => true, :desc => "e" },
            :user => { :default => "suser", :desc => "doc" },
            :group => { :default => "sgroup", :desc => "doc" }
        @settings.define_settings :other, :otherdir => {:type => :directory, :default => "/otherdir", :desc => "a", :owner => "service", :group => "service"}

        @catalog = @settings.to_catalog
      end

      it "should add each specified user and group to the catalog if :mkusers is a valid setting, is enabled, and we're running as root" do
        expect(@catalog.resource(:user, "suser")).to be_instance_of(Puppet::Resource)
        expect(@catalog.resource(:group, "sgroup")).to be_instance_of(Puppet::Resource)
      end

      it "should only add users and groups to the catalog from specified sections" do
        @settings.define_settings :yay, :yaydir => { :type => :directory, :default => "/yaydir", :desc => "a", :owner => "service", :group => "service"}
        catalog = @settings.to_catalog(:other)
        expect(catalog.resource(:user, "jane")).to be_nil
        expect(catalog.resource(:group, "billy")).to be_nil
      end

      it "should not add users or groups to the catalog if :mkusers not running as root" do
        allow(Puppet.features).to receive(:root?).and_return(false)

        catalog = @settings.to_catalog
        expect(catalog.resource(:user, "suser")).to be_nil
        expect(catalog.resource(:group, "sgroup")).to be_nil
      end

      it "should not add users or groups to the catalog if :mkusers is not a valid setting" do
        allow(Puppet.features).to receive(:root?).and_return(true)
        settings = Puppet::Settings.new
        settings.define_settings :other, :otherdir => {:type => :directory, :default => "/otherdir", :desc => "a", :owner => "service", :group => "service"}

        catalog = settings.to_catalog
        expect(catalog.resource(:user, "suser")).to be_nil
        expect(catalog.resource(:group, "sgroup")).to be_nil
      end

      it "should not add users or groups to the catalog if :mkusers is a valid setting but is disabled" do
        @settings[:mkusers] = false
        allow(@settings).to receive(:service_user_available?).and_return(false)
        allow(@settings).to receive(:service_group_available?).and_return(false)

        catalog = @settings.to_catalog
        expect(catalog.resource(:user, "suser")).to be_nil
        expect(catalog.resource(:group, "sgroup")).to be_nil
      end

      it "should not try to add users or groups to the catalog twice" do
        @settings.define_settings :yay, :yaydir => {:type => :directory, :default => "/yaydir", :desc => "a", :owner => "service", :group => "service"}

        # This would fail if users/groups were added twice
        expect { @settings.to_catalog }.not_to raise_error
      end

      it "should set :ensure to :present on each created user and group" do
        expect(@catalog.resource(:user, "suser")[:ensure]).to eq(:present)
        expect(@catalog.resource(:group, "sgroup")[:ensure]).to eq(:present)
      end

      it "should set each created user's :gid to the service group" do
        expect(@settings.to_catalog.resource(:user, "suser")[:gid]).to eq("sgroup")
      end

      it "should not attempt to manage the root user" do
        allow(Puppet.features).to receive(:root?).and_return(true)
        @settings.define_settings :foo, :foodir => {:type => :directory, :default => "/foodir", :desc => "a", :owner => "root", :group => "service"}

        expect(@settings.to_catalog.resource(:user, "root")).to be_nil
      end
    end
  end

  it "should be able to be converted to a manifest" do
    expect(Puppet::Settings.new).to respond_to(:to_manifest)
  end

  describe "when being converted to a manifest" do
    it "should produce a string with the code for each resource joined by two carriage returns" do
      @settings = Puppet::Settings.new
      @settings.define_settings :main,
          :maindir => { :type => :directory, :default => "/maindir", :desc => "a"},
          :seconddir => { :type => :directory, :default => "/seconddir", :desc => "a"}

      main = double('main_resource', :ref => "File[/maindir]")
      expect(main).to receive(:to_manifest).and_return("maindir")
      expect(main).to receive(:'[]').with(:alias).and_return(nil)
      second = double('second_resource', :ref => "File[/seconddir]")
      expect(second).to receive(:to_manifest).and_return("seconddir")
      expect(second).to receive(:'[]').with(:alias).and_return(nil)

      expect(@settings.setting(:maindir)).to receive(:to_resource).and_return(main)
      expect(@settings.setting(:seconddir)).to receive(:to_resource).and_return(second)

      expect(@settings.to_manifest.split("\n\n").sort).to eq(%w{maindir seconddir})
    end
  end

  describe "when using sections of the configuration to manage the local host" do
    before do
      @settings = Puppet::Settings.new
      allow(@settings).to receive(:service_user_available?).and_return(true)
      allow(@settings).to receive(:service_group_available?).and_return(true)
      @settings.define_settings :main, :noop => { :default => false, :desc => "", :type => :boolean }
      @settings.define_settings :main,
          :maindir => { :type => :directory, :default => make_absolute("/maindir"), :desc => "a" },
          :seconddir => { :type => :directory, :default => make_absolute("/seconddir"), :desc => "a"}
      @settings.define_settings :main, :user => { :default => "suser", :desc => "doc" }, :group => { :default => "sgroup", :desc => "doc" }
      @settings.define_settings :other, :otherdir => {:type => :directory, :default => make_absolute("/otherdir"), :desc => "a", :owner => "service", :group => "service", :mode => '0755'}
      @settings.define_settings :third, :thirddir => { :type => :directory, :default => make_absolute("/thirddir"), :desc => "b"}
      @settings.define_settings :files, :myfile => {:type => :file, :default => make_absolute("/myfile"), :desc => "a", :mode => '0755'}
    end

    it "should create a catalog with the specified sections" do
      expect(@settings).to receive(:to_catalog).with(:main, :other).and_return(Puppet::Resource::Catalog.new("foo"))
      @settings.use(:main, :other)
    end

    it "should canonicalize the sections" do
      expect(@settings).to receive(:to_catalog).with(:main, :other).and_return(Puppet::Resource::Catalog.new("foo"))
      @settings.use("main", "other")
    end

    it "should ignore sections that have already been used" do
      expect(@settings).to receive(:to_catalog).with(:main).and_return(Puppet::Resource::Catalog.new("foo"))
      @settings.use(:main)
      expect(@settings).to receive(:to_catalog).with(:other).and_return(Puppet::Resource::Catalog.new("foo"))
      @settings.use(:main, :other)
    end

    it "should convert the created catalog to a RAL catalog" do
      @catalog = Puppet::Resource::Catalog.new("foo")
      expect(@settings).to receive(:to_catalog).with(:main).and_return(@catalog)

      expect(@catalog).to receive(:to_ral).and_return(@catalog)
      @settings.use(:main)
    end

    it "should specify that it is not managing a host catalog" do
      catalog = Puppet::Resource::Catalog.new("foo")
      expect(catalog).to receive(:apply)
      expect(@settings).to receive(:to_catalog).and_return(catalog)

      allow(catalog).to receive(:to_ral).and_return(catalog)

      expect(catalog).to receive(:host_config=).with(false)

      @settings.use(:main)
    end

    it "should support a method for re-using all currently used sections" do
      expect(@settings).to receive(:to_catalog).with(:main, :third).exactly(2).times.and_return(Puppet::Resource::Catalog.new("foo"))

      @settings.use(:main, :third)
      @settings.reuse
    end

    it "should fail with an appropriate message if any resources fail" do
      @catalog = Puppet::Resource::Catalog.new("foo")
      allow(@catalog).to receive(:to_ral).and_return(@catalog)
      expect(@settings).to receive(:to_catalog).and_return(@catalog)

      @trans = double("transaction")
      expect(@catalog).to receive(:apply).and_yield(@trans)

      expect(@trans).to receive(:any_failed?).and_return(true)

      resource = Puppet::Type.type(:notify).new(:title => 'failed')
      status = Puppet::Resource::Status.new(resource)
      event = Puppet::Transaction::Event.new(
        :name => 'failure',
        :status => 'failure',
        :message => 'My failure')
      status.add_event(event)

      report = Puppet::Transaction::Report.new('apply')
      report.add_resource_status(status)

      expect(@trans).to receive(:report).and_return(report)

      expect(@settings).to receive(:raise).with(/My failure/)
      @settings.use(:whatever)
    end
  end

  describe "when dealing with printing configs" do
    before do
      @settings = Puppet::Settings.new
      #these are the magic default values
      allow(@settings).to receive(:value).with(:configprint).and_return("")
      allow(@settings).to receive(:value).with(:genconfig).and_return(false)
      allow(@settings).to receive(:value).with(:genmanifest).and_return(false)
      allow(@settings).to receive(:value).with(:environment).and_return(nil)
    end

    describe "when checking print_config?" do
      it "should return false when the :configprint, :genconfig and :genmanifest are not set" do
        expect(@settings.print_configs?).to be_falsey
      end

      it "should return true when :configprint has a value" do
        allow(@settings).to receive(:value).with(:configprint).and_return("something")
        expect(@settings.print_configs?).to be_truthy
      end

      it "should return true when :genconfig has a value" do
        allow(@settings).to receive(:value).with(:genconfig).and_return(true)
        expect(@settings.print_configs?).to be_truthy
      end

      it "should return true when :genmanifest has a value" do
        allow(@settings).to receive(:value).with(:genmanifest).and_return(true)
        expect(@settings.print_configs?).to be_truthy
      end
    end

    describe "when printing configs" do
      describe "when :configprint has a value" do
        it "should call print_config_options" do
          allow(@settings).to receive(:value).with(:configprint).and_return("something")
          expect(@settings).to receive(:print_config_options)
          @settings.print_configs
        end

        it "should get the value of the option using the environment" do
          allow(@settings).to receive(:value).with(:configprint).and_return("something")
          allow(@settings).to receive(:include?).with("something").and_return(true)
          expect(@settings).to receive(:value).with(:environment).and_return("env")
          expect(@settings).to receive(:value).with("something", "env").and_return("foo")
          allow(@settings).to receive(:puts).with("foo")
          @settings.print_configs
        end

        it "should print the value of the option" do
          allow(@settings).to receive(:value).with(:configprint).and_return("something")
          allow(@settings).to receive(:include?).with("something").and_return(true)
          allow(@settings).to receive(:value).with("something", nil).and_return("foo")
          expect(@settings).to receive(:puts).with("foo")
          @settings.print_configs
        end

        it "should print the value pairs if there are multiple options" do
          allow(@settings).to receive(:value).with(:configprint).and_return("bar,baz")
          allow(@settings).to receive(:include?).with("bar").and_return(true)
          allow(@settings).to receive(:include?).with("baz").and_return(true)
          allow(@settings).to receive(:value).with("bar", nil).and_return("foo")
          allow(@settings).to receive(:value).with("baz", nil).and_return("fud")
          expect(@settings).to receive(:puts).with("bar = foo")
          expect(@settings).to receive(:puts).with("baz = fud")
          @settings.print_configs
        end

        it "should return true after printing" do
          allow(@settings).to receive(:value).with(:configprint).and_return("something")
          allow(@settings).to receive(:include?).with("something").and_return(true)
          allow(@settings).to receive(:value).with("something", nil).and_return("foo")
          allow(@settings).to receive(:puts).with("foo")
          expect(@settings.print_configs).to be_truthy
        end

        it "should return false if a config param is not found" do
          allow(@settings).to receive(:puts)
          allow(@settings).to receive(:value).with(:configprint).and_return("something")
          allow(@settings).to receive(:include?).with("something").and_return(false)
          expect(@settings.print_configs).to be_falsey
        end
      end

      describe "when genconfig is true" do
        before do
          allow(@settings).to receive(:puts)
        end

        it "should call to_config" do
          allow(@settings).to receive(:value).with(:genconfig).and_return(true)
          expect(@settings).to receive(:to_config)
          @settings.print_configs
        end

        it "should return true from print_configs" do
          allow(@settings).to receive(:value).with(:genconfig).and_return(true)
          allow(@settings).to receive(:to_config)
          expect(@settings.print_configs).to be_truthy
        end
      end

      describe "when genmanifest is true" do
        before do
          allow(@settings).to receive(:puts)
        end

        it "should call to_config" do
          allow(@settings).to receive(:value).with(:genmanifest).and_return(true)
          expect(@settings).to receive(:to_manifest)
          @settings.print_configs
        end

        it "should return true from print_configs" do
          allow(@settings).to receive(:value).with(:genmanifest).and_return(true)
          allow(@settings).to receive(:to_manifest)
          expect(@settings.print_configs).to be_truthy
        end
      end
    end
  end

  describe "when determining if the service user is available" do
    let(:settings) do
      settings = Puppet::Settings.new
      settings.define_settings :main, :user => { :default => nil, :desc => "doc" }
      settings
    end

    def a_user_type_for(username)
      user = double('user')
      expect(Puppet::Type.type(:user)).to receive(:new).with(hash_including(name: username)).and_return(user)
      user
    end

    it "should return false if there is no user setting" do
      expect(settings).not_to be_service_user_available
    end

    it "should return false if the user provider says the user is missing" do
      settings[:user] = "foo"

      expect(a_user_type_for("foo")).to receive(:exists?).and_return(false)

      expect(settings).not_to be_service_user_available
    end

    it "should return true if the user provider says the user is present" do
      settings[:user] = "foo"

      expect(a_user_type_for("foo")).to receive(:exists?).and_return(true)

      expect(settings).to be_service_user_available
    end

    it "caches the result of determining if the user is present" do
      settings[:user] = "foo"

      expect(a_user_type_for("foo")).to receive(:exists?).and_return(true)
      expect(settings).to be_service_user_available

      expect(settings).to be_service_user_available
    end
  end

  describe "when determining if the service group is available" do
    let(:settings) do
      settings = Puppet::Settings.new
      settings.define_settings :main, :group => { :default => nil, :desc => "doc" }
      settings
    end

    def a_group_type_for(groupname)
      group = double('group')
      expect(Puppet::Type.type(:group)).to receive(:new).with(hash_including(name: groupname)).and_return(group)
      group
    end

    it "should return false if there is no group setting" do
      expect(settings).not_to be_service_group_available
    end

    it "should return false if the group provider says the group is missing" do
      settings[:group] = "foo"

      expect(a_group_type_for("foo")).to receive(:exists?).and_return(false)

      expect(settings).not_to be_service_group_available
    end

    it "should return true if the group provider says the group is present" do
      settings[:group] = "foo"

      expect(a_group_type_for("foo")).to receive(:exists?).and_return(true)

      expect(settings).to be_service_group_available
    end

    it "caches the result of determining if the group is present" do
      settings[:group] = "foo"

      expect(a_group_type_for("foo")).to receive(:exists?).and_return(true)
      expect(settings).to be_service_group_available

      expect(settings).to be_service_group_available
    end
  end

  describe "when dealing with command-line options" do
    let(:settings) { Puppet::Settings.new }

    it "should get options from Puppet.settings.optparse_addargs" do
      expect(settings).to receive(:optparse_addargs).and_return([])

      settings.send(:parse_global_options, [])
    end

    it "should add options to OptionParser" do
      allow(settings).to receive(:optparse_addargs).and_return([["--option","-o", "Funny Option", :NONE]])
      expect(settings).to receive(:handlearg).with("--option", true)
      settings.send(:parse_global_options, ["--option"])
    end

    it "should not die if it sees an unrecognized option, because the app/face may handle it later" do
      expect { settings.send(:parse_global_options, ["--topuppet", "value"]) } .to_not raise_error
    end

    it "should not pass an unrecognized option to handleargs" do
      expect(settings).not_to receive(:handlearg).with("--topuppet", "value")
      expect { settings.send(:parse_global_options, ["--topuppet", "value"]) } .to_not raise_error
    end

    it "should pass valid puppet settings options to handlearg even if they appear after an unrecognized option" do
      allow(settings).to receive(:optparse_addargs).and_return([["--option","-o", "Funny Option", :NONE]])
      expect(settings).to receive(:handlearg).with("--option", true)
      settings.send(:parse_global_options, ["--invalidoption", "--option"])
    end

    it "should transform boolean option to normal form" do
      expect(Puppet::Settings.clean_opt("--[no-]option", true)).to eq(["--option", true])
    end

    it "should transform boolean option to no- form" do
      expect(Puppet::Settings.clean_opt("--[no-]option", false)).to eq(["--no-option", false])
    end

    it "should set preferred run mode from --run_mode <foo> string without error" do
      args = ["--run_mode", "server"]
      expect(settings).not_to receive(:handlearg).with("--run_mode", "server")
      expect { settings.send(:parse_global_options, args) } .to_not raise_error
      expect(Puppet.settings.preferred_run_mode).to eq(:server)
      expect(args.empty?).to eq(true)
    end

    it "should set preferred run mode from --run_mode=<foo> string without error" do
      args = ["--run_mode=server"]
      expect(settings).not_to receive(:handlearg).with("--run_mode", "server")
      expect { settings.send(:parse_global_options, args) }.to_not raise_error
      expect(Puppet.settings.preferred_run_mode).to eq(:server)
      expect(args.empty?).to eq(true)
    end
  end

  describe "default_certname" do
    describe "using hostname and domain" do
      before :each do
        allow(Puppet::Settings).to receive(:hostname_fact).and_return("testhostname")
        allow(Puppet::Settings).to receive(:domain_fact).and_return("domain.test.")
      end

      it "should use both to generate fqdn" do
        expect(Puppet::Settings.default_certname).to match(/testhostname\.domain\.test/)
      end
      it "should remove trailing dots from fqdn" do
        expect(Puppet::Settings.default_certname).to eq('testhostname.domain.test')
      end
    end

    describe "using just hostname" do
      before :each do
        allow(Puppet::Settings).to receive(:hostname_fact).and_return("testhostname")
        allow(Puppet::Settings).to receive(:domain_fact).and_return("")
      end

      it "should use only hostname to generate fqdn" do
        expect(Puppet::Settings.default_certname).to eq("testhostname")
      end
      it "should removing trailing dots from fqdn" do
        expect(Puppet::Settings.default_certname).to eq("testhostname")
      end
    end
  end
end